@swarmclawai/swarmclaw 0.7.1 → 0.7.3

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 (237) hide show
  1. package/README.md +155 -150
  2. package/package.json +1 -1
  3. package/src/app/api/agents/[id]/route.ts +26 -0
  4. package/src/app/api/agents/[id]/thread/route.ts +37 -9
  5. package/src/app/api/agents/route.ts +13 -2
  6. package/src/app/api/auth/route.ts +76 -7
  7. package/src/app/api/chatrooms/[id]/chat/route.ts +7 -2
  8. package/src/app/api/{sessions → chats}/[id]/browser/route.ts +5 -1
  9. package/src/app/api/{sessions → chats}/[id]/chat/route.ts +7 -3
  10. package/src/app/api/{sessions → chats}/[id]/checkpoints/route.ts +1 -1
  11. package/src/app/api/chats/[id]/main-loop/route.ts +13 -0
  12. package/src/app/api/{sessions → chats}/[id]/messages/route.ts +19 -13
  13. package/src/app/api/{sessions → chats}/[id]/restore/route.ts +1 -1
  14. package/src/app/api/{sessions → chats}/[id]/route.ts +22 -52
  15. package/src/app/api/{sessions → chats}/[id]/stop/route.ts +6 -1
  16. package/src/app/api/{sessions → chats}/route.ts +21 -7
  17. package/src/app/api/connectors/[id]/doctor/route.ts +26 -0
  18. package/src/app/api/connectors/doctor/route.ts +13 -0
  19. package/src/app/api/files/open/route.ts +16 -14
  20. package/src/app/api/memory/maintenance/route.ts +11 -1
  21. package/src/app/api/openclaw/agent-files/route.ts +27 -4
  22. package/src/app/api/openclaw/skills/route.ts +11 -3
  23. package/src/app/api/plugins/dependencies/route.ts +24 -0
  24. package/src/app/api/plugins/install/route.ts +15 -92
  25. package/src/app/api/plugins/route.ts +6 -26
  26. package/src/app/api/plugins/settings/route.ts +40 -0
  27. package/src/app/api/plugins/ui/route.ts +1 -0
  28. package/src/app/api/settings/route.ts +49 -7
  29. package/src/app/api/tasks/[id]/route.ts +15 -6
  30. package/src/app/api/tasks/bulk/route.ts +2 -2
  31. package/src/app/api/tasks/route.ts +9 -4
  32. package/src/app/api/usage/route.ts +30 -0
  33. package/src/app/api/webhooks/[id]/route.ts +8 -1
  34. package/src/app/page.tsx +9 -2
  35. package/src/cli/index.js +39 -33
  36. package/src/cli/index.ts +43 -49
  37. package/src/cli/spec.js +29 -27
  38. package/src/components/agents/agent-card.tsx +16 -13
  39. package/src/components/agents/agent-chat-list.tsx +104 -4
  40. package/src/components/agents/agent-list.tsx +54 -22
  41. package/src/components/agents/agent-sheet.tsx +209 -18
  42. package/src/components/agents/cron-job-form.tsx +3 -3
  43. package/src/components/agents/inspector-panel.tsx +110 -50
  44. package/src/components/auth/access-key-gate.tsx +36 -97
  45. package/src/components/auth/setup-wizard.tsx +5 -38
  46. package/src/components/chat/chat-area.tsx +39 -27
  47. package/src/components/{sessions/session-card.tsx → chat/chat-card.tsx} +7 -23
  48. package/src/components/chat/chat-header.tsx +299 -314
  49. package/src/components/{sessions/session-list.tsx → chat/chat-list.tsx} +11 -14
  50. package/src/components/chat/chat-tool-toggles.tsx +26 -17
  51. package/src/components/chat/checkpoint-timeline.tsx +4 -4
  52. package/src/components/chat/message-bubble.tsx +4 -1
  53. package/src/components/chat/message-list.tsx +5 -3
  54. package/src/components/chat/session-debug-panel.tsx +1 -1
  55. package/src/components/chat/tool-request-banner.tsx +3 -3
  56. package/src/components/chatrooms/agent-hover-card.tsx +3 -3
  57. package/src/components/chatrooms/chatroom-tool-request-banner.tsx +2 -2
  58. package/src/components/chatrooms/chatroom-view.tsx +347 -205
  59. package/src/components/connectors/connector-list.tsx +265 -127
  60. package/src/components/connectors/connector-sheet.tsx +218 -1
  61. package/src/components/home/home-view.tsx +129 -5
  62. package/src/components/layout/app-layout.tsx +392 -182
  63. package/src/components/layout/mobile-header.tsx +26 -8
  64. package/src/components/plugins/plugin-list.tsx +487 -254
  65. package/src/components/plugins/plugin-sheet.tsx +236 -13
  66. package/src/components/projects/project-detail.tsx +183 -0
  67. package/src/components/settings/gateway-connection-panel.tsx +1 -1
  68. package/src/components/shared/agent-picker-list.tsx +2 -2
  69. package/src/components/shared/command-palette.tsx +111 -25
  70. package/src/components/shared/settings/plugin-manager.tsx +20 -4
  71. package/src/components/shared/settings/section-capability-policy.tsx +105 -0
  72. package/src/components/shared/settings/section-heartbeat.tsx +78 -1
  73. package/src/components/shared/settings/section-orchestrator.tsx +3 -3
  74. package/src/components/shared/settings/section-providers.tsx +1 -1
  75. package/src/components/shared/settings/section-runtime-loop.tsx +5 -5
  76. package/src/components/shared/settings/section-secrets.tsx +6 -6
  77. package/src/components/shared/settings/section-user-preferences.tsx +1 -1
  78. package/src/components/shared/settings/section-voice.tsx +5 -1
  79. package/src/components/shared/settings/section-web-search.tsx +10 -2
  80. package/src/components/shared/settings/settings-page.tsx +244 -56
  81. package/src/components/tasks/approvals-panel.tsx +205 -18
  82. package/src/components/tasks/task-board.tsx +242 -46
  83. package/src/components/usage/metrics-dashboard.tsx +147 -1
  84. package/src/components/wallets/wallet-panel.tsx +17 -5
  85. package/src/components/webhooks/webhook-sheet.tsx +8 -8
  86. package/src/lib/auth.ts +17 -0
  87. package/src/lib/chat-streaming-state.test.ts +108 -0
  88. package/src/lib/chat-streaming-state.ts +108 -0
  89. package/src/lib/chat.ts +1 -1
  90. package/src/lib/{sessions.ts → chats.ts} +28 -18
  91. package/src/lib/openclaw-agent-id.test.ts +14 -0
  92. package/src/lib/openclaw-agent-id.ts +31 -0
  93. package/src/lib/providers/claude-cli.ts +1 -1
  94. package/src/lib/server/agent-assignment.test.ts +112 -0
  95. package/src/lib/server/agent-assignment.ts +169 -0
  96. package/src/lib/server/approval-connector-notify.test.ts +253 -0
  97. package/src/lib/server/approvals-auto-approve.test.ts +205 -0
  98. package/src/lib/server/approvals.ts +483 -75
  99. package/src/lib/server/autonomy-runtime.test.ts +341 -0
  100. package/src/lib/server/browser-state.test.ts +118 -0
  101. package/src/lib/server/browser-state.ts +123 -0
  102. package/src/lib/server/build-llm.test.ts +36 -0
  103. package/src/lib/server/build-llm.ts +11 -4
  104. package/src/lib/server/builtin-plugins.ts +34 -0
  105. package/src/lib/server/capability-router.ts +10 -8
  106. package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
  107. package/src/lib/server/chat-execution-tool-events.test.ts +134 -0
  108. package/src/lib/server/chat-execution.ts +285 -165
  109. package/src/lib/server/chatroom-health.test.ts +26 -0
  110. package/src/lib/server/chatroom-health.ts +2 -3
  111. package/src/lib/server/chatroom-helpers.test.ts +67 -2
  112. package/src/lib/server/chatroom-helpers.ts +48 -8
  113. package/src/lib/server/connectors/discord.ts +175 -11
  114. package/src/lib/server/connectors/doctor.test.ts +80 -0
  115. package/src/lib/server/connectors/doctor.ts +116 -0
  116. package/src/lib/server/connectors/manager.ts +948 -112
  117. package/src/lib/server/connectors/policy.test.ts +222 -0
  118. package/src/lib/server/connectors/policy.ts +452 -0
  119. package/src/lib/server/connectors/slack.ts +188 -9
  120. package/src/lib/server/connectors/telegram.ts +65 -15
  121. package/src/lib/server/connectors/thread-context.test.ts +44 -0
  122. package/src/lib/server/connectors/thread-context.ts +72 -0
  123. package/src/lib/server/connectors/types.ts +41 -11
  124. package/src/lib/server/cost.ts +34 -1
  125. package/src/lib/server/daemon-state.ts +61 -3
  126. package/src/lib/server/data-dir.ts +13 -0
  127. package/src/lib/server/delegation-jobs.test.ts +140 -0
  128. package/src/lib/server/delegation-jobs.ts +248 -0
  129. package/src/lib/server/document-utils.test.ts +47 -0
  130. package/src/lib/server/document-utils.ts +397 -0
  131. package/src/lib/server/heartbeat-service.ts +14 -40
  132. package/src/lib/server/heartbeat-source.test.ts +22 -0
  133. package/src/lib/server/heartbeat-source.ts +7 -0
  134. package/src/lib/server/identity-continuity.test.ts +77 -0
  135. package/src/lib/server/identity-continuity.ts +127 -0
  136. package/src/lib/server/mailbox-utils.ts +347 -0
  137. package/src/lib/server/main-agent-loop.ts +28 -1103
  138. package/src/lib/server/memory-db.ts +4 -6
  139. package/src/lib/server/memory-tiers.ts +40 -0
  140. package/src/lib/server/openclaw-agent-resolver.test.ts +70 -0
  141. package/src/lib/server/openclaw-agent-resolver.ts +128 -0
  142. package/src/lib/server/openclaw-exec-config.ts +5 -6
  143. package/src/lib/server/openclaw-skills-normalize.test.ts +56 -0
  144. package/src/lib/server/openclaw-skills-normalize.ts +136 -0
  145. package/src/lib/server/openclaw-sync.ts +3 -2
  146. package/src/lib/server/orchestrator-lg.ts +20 -9
  147. package/src/lib/server/orchestrator.ts +7 -7
  148. package/src/lib/server/playwright-proxy.mjs +27 -3
  149. package/src/lib/server/plugins.test.ts +207 -0
  150. package/src/lib/server/plugins.ts +927 -66
  151. package/src/lib/server/provider-health.ts +38 -6
  152. package/src/lib/server/queue.ts +13 -28
  153. package/src/lib/server/scheduler.ts +2 -0
  154. package/src/lib/server/session-archive-memory.test.ts +85 -0
  155. package/src/lib/server/session-archive-memory.ts +230 -0
  156. package/src/lib/server/session-mailbox.ts +8 -18
  157. package/src/lib/server/session-reset-policy.test.ts +99 -0
  158. package/src/lib/server/session-reset-policy.ts +311 -0
  159. package/src/lib/server/session-run-manager.ts +33 -82
  160. package/src/lib/server/session-tools/autonomy-tools.test.ts +105 -0
  161. package/src/lib/server/session-tools/calendar.ts +366 -0
  162. package/src/lib/server/session-tools/canvas.ts +1 -1
  163. package/src/lib/server/session-tools/chatroom.ts +4 -2
  164. package/src/lib/server/session-tools/connector.ts +114 -10
  165. package/src/lib/server/session-tools/context.ts +21 -5
  166. package/src/lib/server/session-tools/crawl.ts +447 -0
  167. package/src/lib/server/session-tools/crud.ts +74 -28
  168. package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
  169. package/src/lib/server/session-tools/delegate.ts +497 -24
  170. package/src/lib/server/session-tools/discovery.ts +24 -6
  171. package/src/lib/server/session-tools/document.ts +283 -0
  172. package/src/lib/server/session-tools/edit_file.ts +4 -2
  173. package/src/lib/server/session-tools/email.ts +320 -0
  174. package/src/lib/server/session-tools/extract.ts +137 -0
  175. package/src/lib/server/session-tools/file-normalize.test.ts +93 -0
  176. package/src/lib/server/session-tools/file-send.test.ts +84 -1
  177. package/src/lib/server/session-tools/file.ts +241 -25
  178. package/src/lib/server/session-tools/git.ts +1 -1
  179. package/src/lib/server/session-tools/http.ts +1 -1
  180. package/src/lib/server/session-tools/human-loop.ts +227 -0
  181. package/src/lib/server/session-tools/image-gen.ts +380 -0
  182. package/src/lib/server/session-tools/index.ts +130 -50
  183. package/src/lib/server/session-tools/mailbox.ts +276 -0
  184. package/src/lib/server/session-tools/memory.ts +172 -3
  185. package/src/lib/server/session-tools/monitor.ts +151 -8
  186. package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
  187. package/src/lib/server/session-tools/openclaw-nodes.ts +1 -1
  188. package/src/lib/server/session-tools/openclaw-workspace.ts +1 -1
  189. package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
  190. package/src/lib/server/session-tools/platform.ts +148 -7
  191. package/src/lib/server/session-tools/plugin-creator.ts +89 -26
  192. package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
  193. package/src/lib/server/session-tools/replicate.ts +301 -0
  194. package/src/lib/server/session-tools/sample-ui.ts +1 -1
  195. package/src/lib/server/session-tools/sandbox.ts +4 -2
  196. package/src/lib/server/session-tools/schedule.ts +24 -12
  197. package/src/lib/server/session-tools/session-info.ts +43 -7
  198. package/src/lib/server/session-tools/session-tools-wiring.test.ts +31 -17
  199. package/src/lib/server/session-tools/shell.ts +5 -2
  200. package/src/lib/server/session-tools/subagent.ts +194 -28
  201. package/src/lib/server/session-tools/table.ts +587 -0
  202. package/src/lib/server/session-tools/wallet.ts +42 -12
  203. package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
  204. package/src/lib/server/session-tools/web.ts +926 -91
  205. package/src/lib/server/storage.ts +255 -16
  206. package/src/lib/server/stream-agent-chat.ts +116 -268
  207. package/src/lib/server/structured-extract.test.ts +72 -0
  208. package/src/lib/server/structured-extract.ts +373 -0
  209. package/src/lib/server/task-mention.test.ts +16 -2
  210. package/src/lib/server/task-mention.ts +61 -10
  211. package/src/lib/server/tool-aliases.ts +66 -18
  212. package/src/lib/server/tool-capability-policy.test.ts +9 -9
  213. package/src/lib/server/tool-capability-policy.ts +38 -27
  214. package/src/lib/server/tool-retry.ts +2 -0
  215. package/src/lib/server/watch-jobs.test.ts +173 -0
  216. package/src/lib/server/watch-jobs.ts +532 -0
  217. package/src/lib/server/ws-hub.ts +5 -3
  218. package/src/lib/tool-definitions.ts +4 -0
  219. package/src/lib/validation/schemas.test.ts +26 -0
  220. package/src/lib/validation/schemas.ts +10 -1
  221. package/src/lib/ws-client.ts +14 -12
  222. package/src/proxy.ts +5 -5
  223. package/src/stores/use-app-store.ts +5 -11
  224. package/src/stores/use-chat-store.ts +38 -9
  225. package/src/types/index.ts +352 -47
  226. package/src/app/api/sessions/[id]/main-loop/route.ts +0 -94
  227. package/src/components/sessions/new-session-sheet.tsx +0 -253
  228. package/src/lib/server/main-session.ts +0 -24
  229. package/src/lib/server/session-run-manager.test.ts +0 -23
  230. /package/src/app/api/{sessions → chats}/[id]/clear/route.ts +0 -0
  231. /package/src/app/api/{sessions → chats}/[id]/deploy/route.ts +0 -0
  232. /package/src/app/api/{sessions → chats}/[id]/devserver/route.ts +0 -0
  233. /package/src/app/api/{sessions → chats}/[id]/edit-resend/route.ts +0 -0
  234. /package/src/app/api/{sessions → chats}/[id]/fork/route.ts +0 -0
  235. /package/src/app/api/{sessions → chats}/[id]/mailbox/route.ts +0 -0
  236. /package/src/app/api/{sessions → chats}/[id]/retry/route.ts +0 -0
  237. /package/src/app/api/{sessions → chats}/heartbeat/route.ts +0 -0
@@ -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
  )