@swarmclawai/swarmclaw 0.7.7 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (281) hide show
  1. package/README.md +12 -14
  2. package/next.config.ts +13 -2
  3. package/package.json +4 -2
  4. package/src/app/api/agents/[id]/thread/route.ts +9 -0
  5. package/src/app/api/agents/route.ts +4 -0
  6. package/src/app/api/agents/thread-route.test.ts +133 -0
  7. package/src/app/api/approvals/route.test.ts +148 -0
  8. package/src/app/api/canvas/[sessionId]/route.ts +3 -1
  9. package/src/app/api/chatrooms/[id]/chat/route.ts +4 -2
  10. package/src/app/api/chats/[id]/devserver/route.ts +48 -7
  11. package/src/app/api/chats/[id]/messages/route.ts +42 -18
  12. package/src/app/api/chats/[id]/route.ts +1 -1
  13. package/src/app/api/chats/[id]/stop/route.ts +5 -4
  14. package/src/app/api/chats/route.ts +23 -2
  15. package/src/app/api/clawhub/install/route.ts +28 -8
  16. package/src/app/api/connectors/[id]/route.ts +46 -3
  17. package/src/app/api/connectors/route.ts +12 -8
  18. package/src/app/api/external-agents/route.test.ts +165 -0
  19. package/src/app/api/gateways/[id]/health/route.ts +27 -12
  20. package/src/app/api/gateways/[id]/route.ts +2 -0
  21. package/src/app/api/gateways/health-route.test.ts +135 -0
  22. package/src/app/api/gateways/route.ts +2 -0
  23. package/src/app/api/mcp-servers/route.test.ts +130 -0
  24. package/src/app/api/openclaw/deploy/route.ts +38 -5
  25. package/src/app/api/plugins/install/route.ts +46 -6
  26. package/src/app/api/plugins/marketplace/route.ts +48 -15
  27. package/src/app/api/preview-server/route.ts +26 -11
  28. package/src/app/api/projects/[id]/route.ts +6 -2
  29. package/src/app/api/projects/route.ts +4 -3
  30. package/src/app/api/schedules/[id]/run/route.ts +4 -0
  31. package/src/app/api/schedules/route.test.ts +86 -0
  32. package/src/app/api/schedules/route.ts +6 -1
  33. package/src/app/api/secrets/[id]/route.ts +1 -0
  34. package/src/app/api/secrets/route.ts +2 -1
  35. package/src/app/api/settings/route.ts +2 -0
  36. package/src/app/api/setup/check-provider/route.test.ts +19 -0
  37. package/src/app/api/setup/check-provider/route.ts +40 -10
  38. package/src/app/api/skills/[id]/route.ts +12 -0
  39. package/src/app/api/skills/import/route.ts +14 -12
  40. package/src/app/api/skills/route.ts +13 -1
  41. package/src/app/api/tasks/[id]/route.ts +10 -1
  42. package/src/app/api/tasks/import/github/route.test.ts +65 -0
  43. package/src/app/api/tasks/import/github/route.ts +337 -0
  44. package/src/app/api/wallets/[id]/approve/route.ts +17 -3
  45. package/src/app/api/wallets/[id]/route.ts +79 -33
  46. package/src/app/api/wallets/[id]/send/route.ts +19 -33
  47. package/src/app/api/wallets/route.ts +78 -61
  48. package/src/app/api/webhooks/[id]/route.ts +33 -6
  49. package/src/app/api/webhooks/route.test.ts +272 -0
  50. package/src/cli/index.js +1 -0
  51. package/src/cli/spec.js +1 -0
  52. package/src/components/agents/agent-card.tsx +9 -2
  53. package/src/components/agents/agent-chat-list.tsx +18 -2
  54. package/src/components/agents/agent-list.tsx +1 -0
  55. package/src/components/agents/agent-sheet.tsx +257 -38
  56. package/src/components/agents/inspector-panel.tsx +41 -0
  57. package/src/components/canvas/canvas-panel.tsx +236 -65
  58. package/src/components/chat/chat-area.tsx +36 -19
  59. package/src/components/chat/chat-card.tsx +36 -13
  60. package/src/components/chat/chat-header.tsx +48 -16
  61. package/src/components/chat/chat-list.tsx +28 -4
  62. package/src/components/chat/checkpoint-timeline.tsx +50 -34
  63. package/src/components/chat/delegation-banner.test.ts +14 -1
  64. package/src/components/chat/delegation-banner.tsx +1 -1
  65. package/src/components/chat/message-bubble.tsx +208 -145
  66. package/src/components/chat/message-list.tsx +48 -19
  67. package/src/components/chatrooms/chatroom-message.tsx +2 -2
  68. package/src/components/chatrooms/chatroom-sheet.tsx +16 -2
  69. package/src/components/connectors/connector-health.tsx +1 -1
  70. package/src/components/connectors/connector-list.tsx +7 -2
  71. package/src/components/connectors/connector-sheet.tsx +337 -148
  72. package/src/components/gateways/gateway-sheet.tsx +2 -2
  73. package/src/components/layout/app-layout.tsx +40 -23
  74. package/src/components/mcp-servers/mcp-server-list.tsx +26 -5
  75. package/src/components/mcp-servers/mcp-server-sheet.tsx +19 -2
  76. package/src/components/openclaw/openclaw-deploy-panel.tsx +269 -21
  77. package/src/components/plugins/plugin-list.tsx +45 -9
  78. package/src/components/plugins/plugin-sheet.tsx +55 -7
  79. package/src/components/projects/project-detail.tsx +217 -0
  80. package/src/components/projects/project-sheet.tsx +176 -4
  81. package/src/components/providers/provider-list.tsx +2 -1
  82. package/src/components/providers/provider-sheet.tsx +21 -2
  83. package/src/components/schedules/schedule-card.tsx +25 -1
  84. package/src/components/schedules/schedule-sheet.tsx +44 -2
  85. package/src/components/secrets/secret-sheet.tsx +21 -2
  86. package/src/components/shared/agent-switch-dialog.tsx +12 -1
  87. package/src/components/shared/bottom-sheet.tsx +13 -3
  88. package/src/components/shared/command-palette.tsx +8 -1
  89. package/src/components/shared/confirm-dialog.tsx +19 -4
  90. package/src/components/shared/connector-platform-icon.test.ts +28 -0
  91. package/src/components/shared/connector-platform-icon.tsx +39 -6
  92. package/src/components/shared/settings/plugin-manager.tsx +29 -6
  93. package/src/components/shared/settings/section-capability-policy.tsx +45 -3
  94. package/src/components/shared/settings/section-voice.tsx +11 -3
  95. package/src/components/skills/skill-list.tsx +25 -0
  96. package/src/components/skills/skill-sheet.tsx +84 -12
  97. package/src/components/tasks/approvals-panel.tsx +289 -34
  98. package/src/components/tasks/task-board.tsx +410 -25
  99. package/src/components/tasks/task-card.tsx +66 -8
  100. package/src/components/tasks/task-sheet.tsx +16 -4
  101. package/src/components/ui/dialog.tsx +2 -2
  102. package/src/components/wallets/wallet-approval-dialog.tsx +4 -2
  103. package/src/components/wallets/wallet-panel.tsx +435 -90
  104. package/src/components/wallets/wallet-section.tsx +198 -48
  105. package/src/components/webhooks/webhook-sheet.tsx +22 -2
  106. package/src/lib/approval-display.ts +20 -0
  107. package/src/lib/canvas-content.ts +198 -0
  108. package/src/lib/chat-artifact-summary.ts +165 -0
  109. package/src/lib/chat-display.test.ts +91 -0
  110. package/src/lib/chat-display.ts +58 -0
  111. package/src/lib/chat-streaming-state.test.ts +47 -1
  112. package/src/lib/chat-streaming-state.ts +42 -0
  113. package/src/lib/ollama-model.ts +10 -0
  114. package/src/lib/openclaw-endpoint.test.ts +8 -0
  115. package/src/lib/openclaw-endpoint.ts +6 -1
  116. package/src/lib/plugin-install-cors.ts +46 -0
  117. package/src/lib/plugin-sources.test.ts +43 -0
  118. package/src/lib/plugin-sources.ts +77 -0
  119. package/src/lib/providers/ollama.ts +16 -6
  120. package/src/lib/providers/openclaw.test.ts +54 -0
  121. package/src/lib/providers/openclaw.ts +127 -11
  122. package/src/lib/schedule-dedupe-advanced.test.ts +1335 -0
  123. package/src/lib/schedule-dedupe.test.ts +66 -1
  124. package/src/lib/schedule-dedupe.ts +169 -12
  125. package/src/lib/schedule-origin.test.ts +20 -0
  126. package/src/lib/schedule-origin.ts +15 -0
  127. package/src/lib/server/__fixtures__/fake-mcp-stdio-server.mjs +27 -0
  128. package/src/lib/server/agent-availability.ts +16 -0
  129. package/src/lib/server/agent-runtime-config.ts +12 -4
  130. package/src/lib/server/agent-thread-session.test.ts +51 -0
  131. package/src/lib/server/agent-thread-session.ts +7 -0
  132. package/src/lib/server/approval-match.ts +205 -0
  133. package/src/lib/server/approvals-auto-approve.test.ts +538 -1
  134. package/src/lib/server/approvals.ts +214 -1
  135. package/src/lib/server/assistant-control.test.ts +29 -0
  136. package/src/lib/server/assistant-control.ts +23 -0
  137. package/src/lib/server/build-llm.test.ts +79 -0
  138. package/src/lib/server/build-llm.ts +14 -4
  139. package/src/lib/server/canvas-content.test.ts +32 -0
  140. package/src/lib/server/canvas-content.ts +6 -0
  141. package/src/lib/server/capability-router.test.ts +33 -0
  142. package/src/lib/server/capability-router.ts +80 -19
  143. package/src/lib/server/chat-execution-advanced.test.ts +651 -0
  144. package/src/lib/server/chat-execution-disabled.test.ts +94 -0
  145. package/src/lib/server/chat-execution-tool-events.test.ts +157 -0
  146. package/src/lib/server/chat-execution.ts +378 -73
  147. package/src/lib/server/clawhub-client.test.ts +14 -8
  148. package/src/lib/server/connectors/manager-reconnect.test.ts +47 -0
  149. package/src/lib/server/connectors/manager.test.ts +1147 -0
  150. package/src/lib/server/connectors/manager.ts +461 -137
  151. package/src/lib/server/connectors/pairing.ts +26 -5
  152. package/src/lib/server/connectors/types.ts +2 -0
  153. package/src/lib/server/connectors/whatsapp.test.ts +134 -0
  154. package/src/lib/server/connectors/whatsapp.ts +271 -47
  155. package/src/lib/server/context-manager.ts +6 -1
  156. package/src/lib/server/daemon-state.ts +84 -47
  157. package/src/lib/server/data-dir.test.ts +37 -0
  158. package/src/lib/server/data-dir.ts +20 -1
  159. package/src/lib/server/delegation-jobs-advanced.test.ts +513 -0
  160. package/src/lib/server/devserver-launch.test.ts +60 -0
  161. package/src/lib/server/devserver-launch.ts +85 -0
  162. package/src/lib/server/elevenlabs.test.ts +247 -1
  163. package/src/lib/server/elevenlabs.ts +147 -43
  164. package/src/lib/server/ethereum.ts +590 -0
  165. package/src/lib/server/eval/agent-regression-advanced.test.ts +302 -0
  166. package/src/lib/server/eval/agent-regression.test.ts +18 -1
  167. package/src/lib/server/eval/agent-regression.ts +383 -11
  168. package/src/lib/server/evm-swap.ts +475 -0
  169. package/src/lib/server/execution-log.ts +1 -0
  170. package/src/lib/server/heartbeat-service-timer.test.ts +173 -0
  171. package/src/lib/server/heartbeat-service.ts +20 -11
  172. package/src/lib/server/heartbeat-wake.test.ts +112 -0
  173. package/src/lib/server/heartbeat-wake.ts +338 -57
  174. package/src/lib/server/main-agent-loop-advanced.test.ts +538 -0
  175. package/src/lib/server/main-agent-loop.test.ts +260 -0
  176. package/src/lib/server/main-agent-loop.ts +559 -14
  177. package/src/lib/server/mcp-client.test.ts +16 -0
  178. package/src/lib/server/mcp-client.ts +25 -0
  179. package/src/lib/server/memory-integration.test.ts +719 -0
  180. package/src/lib/server/memory-policy.test.ts +43 -0
  181. package/src/lib/server/memory-policy.ts +132 -0
  182. package/src/lib/server/memory-tiers.test.ts +60 -0
  183. package/src/lib/server/memory-tiers.ts +16 -0
  184. package/src/lib/server/ollama-runtime.ts +58 -0
  185. package/src/lib/server/openclaw-deploy.test.ts +109 -1
  186. package/src/lib/server/openclaw-deploy.ts +557 -81
  187. package/src/lib/server/openclaw-gateway.test.ts +131 -0
  188. package/src/lib/server/openclaw-gateway.ts +10 -4
  189. package/src/lib/server/openclaw-health.test.ts +35 -0
  190. package/src/lib/server/openclaw-health.ts +215 -47
  191. package/src/lib/server/orchestrator-lg.ts +3 -2
  192. package/src/lib/server/orchestrator.ts +2 -0
  193. package/src/lib/server/plugins-advanced.test.ts +351 -0
  194. package/src/lib/server/plugins.ts +211 -6
  195. package/src/lib/server/project-context.ts +162 -0
  196. package/src/lib/server/project-utils.ts +150 -0
  197. package/src/lib/server/queue-advanced.test.ts +528 -0
  198. package/src/lib/server/queue-followups.test.ts +409 -2
  199. package/src/lib/server/queue-reconcile.test.ts +128 -0
  200. package/src/lib/server/queue.ts +527 -68
  201. package/src/lib/server/scheduler.ts +29 -1
  202. package/src/lib/server/session-note.test.ts +36 -0
  203. package/src/lib/server/session-note.ts +42 -0
  204. package/src/lib/server/session-run-manager.ts +83 -4
  205. package/src/lib/server/session-tools/canvas.ts +14 -12
  206. package/src/lib/server/session-tools/connector-inputs.test.ts +37 -0
  207. package/src/lib/server/session-tools/connector.test.ts +138 -0
  208. package/src/lib/server/session-tools/connector.ts +366 -54
  209. package/src/lib/server/session-tools/context.ts +17 -3
  210. package/src/lib/server/session-tools/crud.ts +484 -84
  211. package/src/lib/server/session-tools/delegate-fallback.test.ts +103 -0
  212. package/src/lib/server/session-tools/delegate-resume.test.ts +50 -0
  213. package/src/lib/server/session-tools/delegate.ts +102 -10
  214. package/src/lib/server/session-tools/discovery-approvals.test.ts +142 -0
  215. package/src/lib/server/session-tools/discovery.ts +80 -12
  216. package/src/lib/server/session-tools/file-normalize.test.ts +36 -0
  217. package/src/lib/server/session-tools/file.ts +43 -4
  218. package/src/lib/server/session-tools/human-loop.ts +35 -5
  219. package/src/lib/server/session-tools/index.ts +44 -9
  220. package/src/lib/server/session-tools/manage-connectors.test.ts +139 -0
  221. package/src/lib/server/session-tools/manage-schedules-advanced.test.ts +564 -0
  222. package/src/lib/server/session-tools/manage-schedules.test.ts +283 -0
  223. package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +852 -0
  224. package/src/lib/server/session-tools/manage-tasks.test.ts +114 -0
  225. package/src/lib/server/session-tools/memory.test.ts +93 -0
  226. package/src/lib/server/session-tools/memory.ts +554 -75
  227. package/src/lib/server/session-tools/normalize-tool-args.ts +1 -1
  228. package/src/lib/server/session-tools/platform-access.test.ts +58 -0
  229. package/src/lib/server/session-tools/platform.ts +60 -19
  230. package/src/lib/server/session-tools/plugin-creator.ts +57 -1
  231. package/src/lib/server/session-tools/primitive-tools.test.ts +6 -0
  232. package/src/lib/server/session-tools/schedule.ts +6 -1
  233. package/src/lib/server/session-tools/shell-normalize.test.ts +25 -1
  234. package/src/lib/server/session-tools/shell.ts +22 -3
  235. package/src/lib/server/session-tools/wallet-tool.test.ts +254 -0
  236. package/src/lib/server/session-tools/wallet.ts +1374 -139
  237. package/src/lib/server/session-tools/web-inputs.test.ts +178 -0
  238. package/src/lib/server/session-tools/web.ts +621 -70
  239. package/src/lib/server/skill-discovery.ts +128 -0
  240. package/src/lib/server/skill-eligibility.test.ts +84 -0
  241. package/src/lib/server/skill-eligibility.ts +95 -0
  242. package/src/lib/server/skill-prompt-budget.test.ts +102 -0
  243. package/src/lib/server/skill-prompt-budget.ts +125 -0
  244. package/src/lib/server/skills-normalize.test.ts +54 -0
  245. package/src/lib/server/skills-normalize.ts +372 -26
  246. package/src/lib/server/solana.ts +214 -29
  247. package/src/lib/server/storage.ts +65 -36
  248. package/src/lib/server/stream-agent-chat.test.ts +437 -2
  249. package/src/lib/server/stream-agent-chat.ts +957 -79
  250. package/src/lib/server/system-events.ts +1 -1
  251. package/src/lib/server/tool-aliases.ts +2 -0
  252. package/src/lib/server/tool-capability-policy-advanced.test.ts +502 -0
  253. package/src/lib/server/tool-capability-policy.test.ts +24 -0
  254. package/src/lib/server/tool-capability-policy.ts +29 -1
  255. package/src/lib/server/tool-loop-detection.test.ts +105 -0
  256. package/src/lib/server/tool-loop-detection.ts +260 -0
  257. package/src/lib/server/tool-planning.test.ts +44 -0
  258. package/src/lib/server/tool-planning.ts +271 -0
  259. package/src/lib/server/wallet-execution.test.ts +198 -0
  260. package/src/lib/server/wallet-portfolio.test.ts +98 -0
  261. package/src/lib/server/wallet-portfolio.ts +724 -0
  262. package/src/lib/server/wallet-service.test.ts +57 -0
  263. package/src/lib/server/wallet-service.ts +213 -0
  264. package/src/lib/server/watch-jobs-advanced.test.ts +594 -0
  265. package/src/lib/server/watch-jobs.ts +17 -2
  266. package/src/lib/server/workspace-context.ts +111 -0
  267. package/src/lib/skill-save-payload.test.ts +39 -0
  268. package/src/lib/skill-save-payload.ts +37 -0
  269. package/src/lib/tasks.ts +28 -0
  270. package/src/lib/tool-definitions.ts +2 -1
  271. package/src/lib/tool-event-summary.test.ts +30 -0
  272. package/src/lib/tool-event-summary.ts +37 -0
  273. package/src/lib/validation/schemas.ts +1 -0
  274. package/src/lib/wallet-transactions.test.ts +75 -0
  275. package/src/lib/wallet-transactions.ts +43 -0
  276. package/src/lib/wallet.test.ts +17 -0
  277. package/src/lib/wallet.ts +183 -0
  278. package/src/proxy.test.ts +31 -0
  279. package/src/proxy.ts +34 -2
  280. package/src/stores/use-chat-store.ts +15 -1
  281. package/src/types/index.ts +249 -14
@@ -13,6 +13,7 @@ import { ChatroomPickerList } from '@/components/shared/chatroom-picker-list'
13
13
  import { SheetFooter } from '@/components/shared/sheet-footer'
14
14
  import { SectionLabel } from '@/components/shared/section-label'
15
15
  import { HintTip } from '@/components/shared/hint-tip'
16
+ import { ConfirmDialog } from '@/components/shared/confirm-dialog'
16
17
  import { useChatroomStore } from '@/stores/use-chatroom-store'
17
18
  import { ConnectorHealth } from '@/components/connectors/connector-health'
18
19
 
@@ -53,6 +54,82 @@ interface ConnectorDoctorResponse {
53
54
  policy?: ConnectorDoctorPolicyPreview | null
54
55
  }
55
56
 
57
+ interface ConnectorConfigOption {
58
+ value: string
59
+ label: string
60
+ }
61
+
62
+ interface ConnectorConfigField {
63
+ key: string
64
+ label: string
65
+ placeholder: string
66
+ help?: string
67
+ type?: 'text' | 'select' | 'tags'
68
+ options?: ConnectorConfigOption[]
69
+ emptyLabel?: string
70
+ }
71
+
72
+ const FIELD_HINTS: Record<string, string> = {
73
+ channelIds: "Find these in your platform's developer settings. Leave empty to allow all channels",
74
+ chatIds: "Find these in your platform's developer settings. Leave empty to allow all chats",
75
+ roomIds: 'Leave empty to allow all rooms visible to the bot',
76
+ spaceIds: 'Leave empty to allow all configured spaces',
77
+ allowedJids: 'Phone numbers in international format, or WhatsApp JIDs. Leave empty to allow all',
78
+ allowFrom: 'Only needed for allowlist or pairing DM policy modes',
79
+ scopes: 'Press Enter after each scope to add it',
80
+ }
81
+
82
+ const BOOLEAN_SELECT_OPTIONS: ConnectorConfigOption[] = [
83
+ { value: 'true', label: 'Enabled' },
84
+ { value: 'false', label: 'Disabled' },
85
+ ]
86
+
87
+ const THINKING_LEVEL_OPTIONS: ConnectorConfigOption[] = [
88
+ { value: 'minimal', label: 'Minimal' },
89
+ { value: 'low', label: 'Low' },
90
+ { value: 'medium', label: 'Medium' },
91
+ { value: 'high', label: 'High' },
92
+ ]
93
+
94
+ const DM_POLICY_OPTIONS: ConnectorConfigOption[] = [
95
+ { value: 'open', label: 'Open' },
96
+ { value: 'allowlist', label: 'Allowlist' },
97
+ { value: 'pairing', label: 'Pairing approval' },
98
+ { value: 'disabled', label: 'Disabled' },
99
+ ]
100
+
101
+ const SESSION_SCOPE_OPTIONS: ConnectorConfigOption[] = [
102
+ { value: 'main', label: 'Main thread' },
103
+ { value: 'channel', label: 'Per channel' },
104
+ { value: 'peer', label: 'Per sender' },
105
+ { value: 'channel-peer', label: 'Per channel + sender' },
106
+ { value: 'thread', label: 'Per thread/topic' },
107
+ ]
108
+
109
+ const REPLY_MODE_OPTIONS: ConnectorConfigOption[] = [
110
+ { value: 'off', label: 'Off' },
111
+ { value: 'first', label: 'Reply to first inbound' },
112
+ { value: 'all', label: 'Reply to every inbound' },
113
+ ]
114
+
115
+ const THREAD_BINDING_OPTIONS: ConnectorConfigOption[] = [
116
+ { value: 'off', label: 'Off' },
117
+ { value: 'prefer', label: 'Prefer thread context' },
118
+ { value: 'strict', label: 'Require thread context' },
119
+ ]
120
+
121
+ const GROUP_POLICY_OPTIONS: ConnectorConfigOption[] = [
122
+ { value: 'open', label: 'Open' },
123
+ { value: 'mention', label: 'Mention only' },
124
+ { value: 'reply-or-mention', label: 'Reply or mention' },
125
+ { value: 'disabled', label: 'Disabled' },
126
+ ]
127
+
128
+ const RESET_MODE_OPTIONS: ConnectorConfigOption[] = [
129
+ { value: 'idle', label: 'Idle reset' },
130
+ { value: 'daily', label: 'Daily reset' },
131
+ ]
132
+
56
133
  const PLATFORMS: {
57
134
  id: ConnectorPlatform
58
135
  label: string
@@ -60,7 +137,7 @@ const PLATFORMS: {
60
137
  setupSteps: string[]
61
138
  tokenLabel: string
62
139
  tokenHelp: string
63
- configFields: { key: string; label: string; placeholder: string; help?: string }[]
140
+ configFields: ConnectorConfigField[]
64
141
  }[] = [
65
142
  {
66
143
  id: 'discord',
@@ -77,7 +154,7 @@ const PLATFORMS: {
77
154
  tokenLabel: 'Bot Token',
78
155
  tokenHelp: 'From Discord Developer Portal > Your App > Bot > Token',
79
156
  configFields: [
80
- { key: 'channelIds', label: 'Channel IDs', placeholder: '123456789,987654321', help: 'Leave empty to listen in all channels the bot can see' },
157
+ { key: 'channelIds', label: 'Channel IDs', placeholder: '123456789,987654321', help: 'Leave empty to listen in all channels the bot can see', type: 'tags' },
81
158
  ],
82
159
  },
83
160
  {
@@ -92,7 +169,7 @@ const PLATFORMS: {
92
169
  tokenLabel: 'Bot Token',
93
170
  tokenHelp: 'From @BotFather after creating your bot',
94
171
  configFields: [
95
- { key: 'chatIds', label: 'Chat IDs', placeholder: '-100123456789', help: 'Leave empty to respond in all chats. Use negative IDs for groups.' },
172
+ { key: 'chatIds', label: 'Chat IDs', placeholder: '-100123456789', help: 'Leave empty to respond in all chats. Use negative IDs for groups.', type: 'tags' },
96
173
  ],
97
174
  },
98
175
  {
@@ -111,7 +188,7 @@ const PLATFORMS: {
111
188
  tokenHelp: 'From Slack App > OAuth & Permissions > Bot User OAuth Token',
112
189
  configFields: [
113
190
  { key: 'appToken', label: 'App-Level Token (xapp-...)', placeholder: 'xapp-1-...', help: 'Required for Socket Mode. From Slack App > Basic Information > App-Level Tokens' },
114
- { key: 'channelIds', label: 'Channel IDs', placeholder: 'C0123456789', help: 'Leave empty to listen in all channels the bot is in' },
191
+ { key: 'channelIds', label: 'Channel IDs', placeholder: 'C0123456789', help: 'Leave empty to listen in all channels the bot is in', type: 'tags' },
115
192
  ],
116
193
  },
117
194
  {
@@ -127,7 +204,9 @@ const PLATFORMS: {
127
204
  tokenLabel: '',
128
205
  tokenHelp: '',
129
206
  configFields: [
130
- { key: 'allowedJids', label: 'Allowed Numbers/Groups', placeholder: '1234567890,MyGroup', help: 'Leave empty to respond to all messages' },
207
+ { key: 'dmPolicy', label: 'DM Policy', placeholder: 'open', help: 'How new direct-message senders are handled. Leave unset to use the platform default.', type: 'select', options: DM_POLICY_OPTIONS, emptyLabel: 'Not set (default: open)' },
208
+ { key: 'allowFrom', label: 'Approved DM Senders', placeholder: '15551234567,447700900123', help: 'Optional allowlist used by allowlist/pairing mode.', type: 'tags' },
209
+ { key: 'allowedJids', label: 'Allowed Numbers/Groups', placeholder: '1234567890,MyGroup', help: 'Leave empty to respond to all messages', type: 'tags' },
131
210
  { key: 'outboundJid', label: 'Default Outbound Recipient', placeholder: '15551234567 or 15551234567@s.whatsapp.net', help: 'Used by connector_message_tool when the agent sends proactive WhatsApp updates without an explicit "to" value' },
132
211
  ],
133
212
  },
@@ -147,8 +226,8 @@ const PLATFORMS: {
147
226
  { key: 'sessionKey', label: 'Chat Key Filter', placeholder: 'main', help: 'Optional. If set, only inbound events for this OpenClaw chat are processed.' },
148
227
  { key: 'nodeId', label: 'Client Label', placeholder: 'swarmclaw', help: 'Optional display label shown in OpenClaw presence metadata.' },
149
228
  { key: 'role', label: 'Gateway Role', placeholder: 'operator', help: 'Optional role claim for connect handshake. Default is operator.' },
150
- { key: 'scopes', label: 'Scopes (CSV)', placeholder: 'operator.read,operator.write', help: 'Optional comma-separated scopes for OpenClaw connect.' },
151
- { key: 'tickWatchdog', label: 'Tick Watchdog', placeholder: 'true', help: 'Set false to disable stale-tick reconnect watchdog.' },
229
+ { key: 'scopes', label: 'Scopes (CSV)', placeholder: 'operator.read,operator.write', help: 'Optional comma-separated scopes for OpenClaw connect.', type: 'tags' },
230
+ { key: 'tickWatchdog', label: 'Tick Watchdog', placeholder: 'true', help: 'Enable or disable stale-tick reconnect watchdog.', type: 'select', options: BOOLEAN_SELECT_OPTIONS, emptyLabel: 'Not set (default: enabled)' },
152
231
  { key: 'tickIntervalMs', label: 'Tick Interval Override (ms)', placeholder: '30000', help: 'Optional watchdog interval override when policy tick is unavailable.' },
153
232
  ],
154
233
  },
@@ -166,9 +245,9 @@ const PLATFORMS: {
166
245
  tokenHelp: 'Server password used for /api/v1/ping and /api/v1/message/text',
167
246
  configFields: [
168
247
  { key: 'serverUrl', label: 'Server URL', placeholder: 'http://127.0.0.1:1234', help: 'BlueBubbles server URL (no trailing /api path needed)' },
169
- { key: 'chatIds', label: 'Allowed Chat IDs', placeholder: 'iMessage;-;+15551234567', help: 'Optional comma-separated chat IDs/guid fragments. Leave empty for all chats.' },
170
- { key: 'dmPolicy', label: 'DM Policy', placeholder: 'open | allowlist | pairing | disabled', help: 'Access policy for direct-message senders. Default: open.' },
171
- { key: 'allowFrom', label: 'Allowed Sender IDs', placeholder: '+15551234567,test@example.com', help: 'Optional comma-separated sender IDs for allowlist/pairing mode.' },
248
+ { key: 'chatIds', label: 'Allowed Chat IDs', placeholder: 'iMessage;-;+15551234567', help: 'Optional comma-separated chat IDs/guid fragments. Leave empty for all chats.', type: 'tags' },
249
+ { key: 'dmPolicy', label: 'DM Policy', placeholder: 'open', help: 'Access policy for direct-message senders. Leave unset to use the platform default.', type: 'select', options: DM_POLICY_OPTIONS, emptyLabel: 'Not set (default: open)' },
250
+ { key: 'allowFrom', label: 'Allowed Sender IDs', placeholder: '+15551234567,test@example.com', help: 'Optional sender allowlist used by allowlist/pairing mode.', type: 'tags' },
172
251
  { key: 'outboundTarget', label: 'Default Outbound Target', placeholder: 'iMessage;-;+15551234567', help: 'Used when proactive sends omit "to".' },
173
252
  { key: 'webhookSecret', label: 'Webhook Secret', placeholder: 'optional-shared-secret', help: 'Optional secret required by /api/connectors/{id}/webhook (header: x-connector-secret or ?secret=...)' },
174
253
  { key: 'timeoutMs', label: 'Request Timeout (ms)', placeholder: '10000', help: 'Optional BlueBubbles API timeout in milliseconds.' },
@@ -188,7 +267,7 @@ const PLATFORMS: {
188
267
  tokenHelp: 'Matrix access token for the bot user',
189
268
  configFields: [
190
269
  { key: 'homeserverUrl', label: 'Homeserver URL', placeholder: 'https://matrix.org', help: 'The Matrix homeserver URL' },
191
- { key: 'roomIds', label: 'Room IDs', placeholder: '!abc123:matrix.org', help: 'Comma-separated room IDs. Leave empty for all rooms.' },
270
+ { key: 'roomIds', label: 'Room IDs', placeholder: '!abc123:matrix.org', help: 'Comma-separated room IDs. Leave empty for all rooms.', type: 'tags' },
192
271
  ],
193
272
  },
194
273
  {
@@ -204,7 +283,7 @@ const PLATFORMS: {
204
283
  tokenLabel: 'Service Account JSON',
205
284
  tokenHelp: 'Paste the full service account JSON key file contents',
206
285
  configFields: [
207
- { key: 'spaceIds', label: 'Space IDs', placeholder: 'spaces/AAAA123', help: 'Comma-separated Google Chat space IDs' },
286
+ { key: 'spaceIds', label: 'Space IDs', placeholder: 'spaces/AAAA123', help: 'Comma-separated Google Chat space IDs', type: 'tags' },
208
287
  { key: 'webhookSecret', label: 'Webhook Secret', placeholder: 'optional-shared-secret', help: 'Optional secret required by /api/connectors/{id}/webhook (header: x-connector-secret or ?secret=...)' },
209
288
  ],
210
289
  },
@@ -241,18 +320,21 @@ const PLATFORMS: {
241
320
  configFields: [
242
321
  { key: 'phoneNumber', label: 'Phone Number', placeholder: '+1234567890', help: 'Pre-registered Signal phone number' },
243
322
  { key: 'signalCliPath', label: 'signal-cli Path', placeholder: 'signal-cli', help: 'Path to signal-cli binary (defaults to signal-cli)' },
244
- { key: 'signalCliMode', label: 'Mode', placeholder: 'stdio', help: 'stdio (default) or http' },
323
+ { key: 'signalCliMode', label: 'Mode', placeholder: 'stdio', help: 'How SwarmClaw talks to signal-cli.', type: 'select', options: [{ value: 'stdio', label: 'stdio' }, { value: 'http', label: 'HTTP API' }], emptyLabel: 'Not set (default: stdio)' },
245
324
  { key: 'signalCliHttpUrl', label: 'HTTP API URL', placeholder: 'http://localhost:8080', help: 'Only needed for http mode' },
246
325
  ],
247
326
  },
248
327
  ]
249
328
 
250
- const COMMON_CONFIG_FIELDS: { key: string; label: string; placeholder: string; help?: string }[] = [
329
+ const COMMON_CONFIG_FIELDS: ConnectorConfigField[] = [
251
330
  {
252
331
  key: 'thinkingLevel',
253
332
  label: 'Thinking Level',
254
333
  placeholder: 'minimal | low | medium | high',
255
334
  help: 'Default reasoning depth for new/reset direct connector sessions.',
335
+ type: 'select',
336
+ options: THINKING_LEVEL_OPTIONS,
337
+ emptyLabel: 'Not set (agent default)',
256
338
  },
257
339
  {
258
340
  key: 'providerOverride',
@@ -271,24 +353,36 @@ const COMMON_CONFIG_FIELDS: { key: string; label: string; placeholder: string; h
271
353
  label: 'Session Scope',
272
354
  placeholder: 'main | channel | peer | channel-peer | thread',
273
355
  help: 'Conversation identity policy. Defaults to channel-peer for DMs and channel for groups.',
356
+ type: 'select',
357
+ options: SESSION_SCOPE_OPTIONS,
358
+ emptyLabel: 'Not set (platform default)',
274
359
  },
275
360
  {
276
361
  key: 'replyMode',
277
362
  label: 'Reply Mode',
278
363
  placeholder: 'off | first | all',
279
364
  help: 'Whether outbound replies should attach to the triggering inbound message.',
365
+ type: 'select',
366
+ options: REPLY_MODE_OPTIONS,
367
+ emptyLabel: 'Not set (platform default)',
280
368
  },
281
369
  {
282
370
  key: 'threadBinding',
283
371
  label: 'Thread Binding',
284
372
  placeholder: 'off | prefer | strict',
285
373
  help: 'Prefer or require thread/topic-specific sessions when the platform exposes thread IDs.',
374
+ type: 'select',
375
+ options: THREAD_BINDING_OPTIONS,
376
+ emptyLabel: 'Not set (platform default)',
286
377
  },
287
378
  {
288
379
  key: 'groupPolicy',
289
380
  label: 'Group Policy',
290
381
  placeholder: 'open | mention | reply-or-mention | disabled',
291
382
  help: 'Controls whether the agent speaks in group chats without being mentioned or replied to.',
383
+ type: 'select',
384
+ options: GROUP_POLICY_OPTIONS,
385
+ emptyLabel: 'Not set (platform default)',
292
386
  },
293
387
  {
294
388
  key: 'idleTimeoutSec',
@@ -307,6 +401,9 @@ const COMMON_CONFIG_FIELDS: { key: string; label: string; placeholder: string; h
307
401
  label: 'Reset Mode',
308
402
  placeholder: 'idle | daily',
309
403
  help: 'Freshness policy for connector sessions. Daily resets use the fields below.',
404
+ type: 'select',
405
+ options: RESET_MODE_OPTIONS,
406
+ emptyLabel: 'Not set (default: idle)',
310
407
  },
311
408
  {
312
409
  key: 'sessionDailyResetAt',
@@ -331,18 +428,27 @@ const COMMON_CONFIG_FIELDS: { key: string; label: string; placeholder: string; h
331
428
  label: 'Status Reactions',
332
429
  placeholder: 'true | false',
333
430
  help: 'When supported, add lightweight platform-native reactions for processing/sent/silent states.',
431
+ type: 'select',
432
+ options: BOOLEAN_SELECT_OPTIONS,
433
+ emptyLabel: 'Not set (default: enabled)',
334
434
  },
335
435
  {
336
436
  key: 'typingIndicators',
337
437
  label: 'Typing Indicators',
338
438
  placeholder: 'true | false',
339
439
  help: 'When supported, keep a native typing/working indicator alive while the agent is running.',
440
+ type: 'select',
441
+ options: BOOLEAN_SELECT_OPTIONS,
442
+ emptyLabel: 'Not set (default: enabled)',
340
443
  },
341
444
  {
342
445
  key: 'taskFollowups',
343
446
  label: 'Task Follow-ups',
344
447
  placeholder: 'true | false',
345
448
  help: 'Enable automatic connector follow-up messages when this agent completes or fails a task.',
449
+ type: 'select',
450
+ options: BOOLEAN_SELECT_OPTIONS,
451
+ emptyLabel: 'Not set (default: enabled)',
346
452
  },
347
453
  {
348
454
  key: 'taskFollowupTemplate',
@@ -411,6 +517,9 @@ export function ConnectorSheet() {
411
517
  const [doctorWarnings, setDoctorWarnings] = useState<string[]>([])
412
518
  const [doctorPolicy, setDoctorPolicy] = useState<ConnectorDoctorPolicyPreview | null>(null)
413
519
  const [doctorLoading, setDoctorLoading] = useState(false)
520
+ const [confirmDelete, setConfirmDelete] = useState(false)
521
+ const [confirmWhatsAppAction, setConfirmWhatsAppAction] = useState<'unlink' | 'repair' | null>(null)
522
+ const [deleting, setDeleting] = useState(false)
414
523
 
415
524
  const editing = editingId ? connectors[editingId] as Connector | undefined : null
416
525
 
@@ -503,6 +612,9 @@ export function ConnectorSheet() {
503
612
  setDoctorWarnings([])
504
613
  setDoctorPolicy(null)
505
614
  setDoctorLoading(false)
615
+ setConfirmDelete(false)
616
+ setConfirmWhatsAppAction(null)
617
+ setDeleting(false)
506
618
  return
507
619
  }
508
620
  const timer = window.setTimeout(() => {
@@ -560,11 +672,37 @@ export function ConnectorSheet() {
560
672
  }
561
673
 
562
674
  const handleDelete = async () => {
563
- if (!editing || !confirm('Delete this connector?')) return
564
- await api('DELETE', `/connectors/${editing.id}`)
565
- await loadConnectors()
566
- setOpen(false)
567
- setEditingId(null)
675
+ if (!editing) return
676
+ setDeleting(true)
677
+ try {
678
+ await api('DELETE', `/connectors/${editing.id}`)
679
+ await loadConnectors()
680
+ setConfirmDelete(false)
681
+ setOpen(false)
682
+ setEditingId(null)
683
+ } catch (err: unknown) {
684
+ toast.error(`Failed to delete connector: ${err instanceof Error ? err.message : String(err)}`)
685
+ } finally {
686
+ setDeleting(false)
687
+ }
688
+ }
689
+
690
+ const handleWhatsAppRepair = async (mode: 'unlink' | 'repair') => {
691
+ if (!editing) return
692
+ setActionLoading(true)
693
+ try {
694
+ await api('PUT', `/connectors/${editing.id}`, { action: 'repair' })
695
+ setWaAuthenticated(false)
696
+ setWaHasCreds(false)
697
+ setQrDataUrl(null)
698
+ setWaConnecting(true)
699
+ setConfirmWhatsAppAction(null)
700
+ await loadConnectors()
701
+ } catch (err: unknown) {
702
+ toast.error(`Failed to ${mode === 'unlink' ? 'unlink' : 're-pair'}: ${err instanceof Error ? err.message : String(err)}`)
703
+ } finally {
704
+ setActionLoading(false)
705
+ }
568
706
  }
569
707
 
570
708
  const platformConfig = ALL_PLATFORMS.find((p) => p.id === platform) || ALL_PLATFORMS[0]
@@ -573,6 +711,120 @@ export function ConnectorSheet() {
573
711
 
574
712
  const inputClass = "w-full px-4 py-3 rounded-[12px] border border-white/[0.08] bg-surface text-text text-[14px] outline-none transition-all placeholder:text-text-3/50 focus:border-white/[0.15]"
575
713
 
714
+ const updateConfigValue = useCallback((key: string, value: string) => {
715
+ setConfig((prev) => {
716
+ const next = { ...prev }
717
+ if (value.trim() === '') delete next[key]
718
+ else next[key] = value
719
+ return next
720
+ })
721
+ }, [])
722
+
723
+ const renderConfigField = useCallback((field: ConnectorConfigField) => {
724
+ const isTagField = field.type === 'tags'
725
+ if (isTagField) {
726
+ const tags = (config[field.key] || '').split(',').map((s) => s.trim()).filter(Boolean)
727
+ return (
728
+ <div key={field.key} className="mb-6">
729
+ <label className="flex items-center gap-2 font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
730
+ {field.label} <span className="normal-case tracking-normal font-normal text-text-3">(optional)</span>
731
+ {FIELD_HINTS[field.key] && <HintTip text={FIELD_HINTS[field.key]} />}
732
+ </label>
733
+ {field.help && <p className="text-[12px] text-text-3/60 mb-2">{field.help}</p>}
734
+ <div className="flex flex-wrap gap-2 mb-2">
735
+ {tags.map((tag, i) => (
736
+ <span key={i} className="flex items-center gap-1.5 px-3 py-1.5 rounded-[8px] bg-accent-soft/50 border border-accent-bright/20 text-[12px] font-mono text-accent-bright">
737
+ {tag}
738
+ <button
739
+ aria-label={`Remove ${tag}`}
740
+ onClick={() => updateConfigValue(field.key, tags.filter((_, j) => j !== i).join(','))}
741
+ className="ml-0.5 w-4 h-4 flex items-center justify-center rounded-full hover:bg-white/10 transition-colors cursor-pointer text-accent-bright/50 hover:text-accent-bright"
742
+ >
743
+ <svg width="8" height="8" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round">
744
+ <line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" />
745
+ </svg>
746
+ </button>
747
+ </span>
748
+ ))}
749
+ </div>
750
+ <div className="flex gap-2">
751
+ <input
752
+ id={`tag-input-${field.key}`}
753
+ placeholder={field.placeholder}
754
+ className={`${inputClass} font-mono text-[13px] flex-1`}
755
+ style={{ fontFamily: undefined }}
756
+ onKeyDown={(e) => {
757
+ if (e.key === 'Enter' || e.key === ',') {
758
+ e.preventDefault()
759
+ const input = e.currentTarget
760
+ const val = input.value.trim().replace(/,/g, '')
761
+ if (val) {
762
+ const next = tags.length > 0 ? `${tags.join(',')},${val}` : val
763
+ updateConfigValue(field.key, next)
764
+ input.value = ''
765
+ }
766
+ }
767
+ }}
768
+ />
769
+ <button
770
+ type="button"
771
+ onClick={() => {
772
+ const input = document.getElementById(`tag-input-${field.key}`) as HTMLInputElement | null
773
+ const val = input?.value.trim().replace(/,/g, '')
774
+ if (val) {
775
+ const next = tags.length > 0 ? `${tags.join(',')},${val}` : val
776
+ updateConfigValue(field.key, next)
777
+ if (input) input.value = ''
778
+ }
779
+ }}
780
+ className="px-4 py-2.5 rounded-[10px] bg-accent-soft/50 text-accent-bright text-[12px] font-600 hover:bg-accent-soft transition-colors cursor-pointer border border-accent-bright/20"
781
+ >
782
+ Add
783
+ </button>
784
+ </div>
785
+ </div>
786
+ )
787
+ }
788
+
789
+ if (field.type === 'select' && field.options?.length) {
790
+ return (
791
+ <div key={field.key} className="mb-6">
792
+ <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
793
+ {field.label} <span className="normal-case tracking-normal font-normal text-text-3">(optional)</span>
794
+ </label>
795
+ {field.help && <p className="text-[12px] text-text-3/60 mb-2">{field.help}</p>}
796
+ <select
797
+ value={config[field.key] || ''}
798
+ onChange={(e) => updateConfigValue(field.key, e.target.value)}
799
+ className={`${inputClass} appearance-none cursor-pointer`}
800
+ style={{ fontFamily: 'inherit' }}
801
+ >
802
+ <option value="">{field.emptyLabel || 'Not set'}</option>
803
+ {field.options.map((option) => (
804
+ <option key={option.value} value={option.value}>{option.label}</option>
805
+ ))}
806
+ </select>
807
+ </div>
808
+ )
809
+ }
810
+
811
+ return (
812
+ <div key={field.key} className="mb-6">
813
+ <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
814
+ {field.label} <span className="normal-case tracking-normal font-normal text-text-3">(optional)</span>
815
+ </label>
816
+ {field.help && <p className="text-[12px] text-text-3/60 mb-2">{field.help}</p>}
817
+ <input
818
+ value={config[field.key] || ''}
819
+ onChange={(e) => updateConfigValue(field.key, e.target.value)}
820
+ placeholder={field.placeholder}
821
+ className={`${inputClass} ${field.key.toLowerCase().includes('token') || field.key.toLowerCase().includes('secret') ? 'font-mono text-[13px]' : ''}`}
822
+ style={{ fontFamily: field.key.toLowerCase().includes('token') || field.key.toLowerCase().includes('secret') ? undefined : 'inherit' }}
823
+ />
824
+ </div>
825
+ )
826
+ }, [config, inputClass, updateConfigValue])
827
+
576
828
  return (
577
829
  <BottomSheet open={open} onClose={() => { setOpen(false); setEditingId(null) }} wide>
578
830
  <div className="mb-8">
@@ -816,95 +1068,23 @@ export function ConnectorSheet() {
816
1068
  )}
817
1069
 
818
1070
  {/* Platform-specific config */}
819
- {[...platformConfig.configFields, ...COMMON_CONFIG_FIELDS].map((field) => {
820
- const isTagField = field.key === 'allowedJids' || field.key === 'channelIds' || field.key === 'chatIds' || field.key === 'allowFrom'
821
- const fieldHint: Record<string, string> = {
822
- channelIds: "Find these in your platform's developer settings. Leave empty to allow all channels",
823
- chatIds: "Find these in your platform's developer settings. Leave empty to allow all channels",
824
- allowedJids: "Phone numbers in international format (e.g. 447xxx). Leave empty to allow all",
825
- }
826
- if (isTagField) {
827
- const tags = (config[field.key] || '').split(',').map((s) => s.trim()).filter(Boolean)
828
- return (
829
- <div key={field.key} className="mb-6">
830
- <label className="flex items-center gap-2 font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
831
- {field.label} <span className="normal-case tracking-normal font-normal text-text-3">(optional)</span>
832
- {fieldHint[field.key] && <HintTip text={fieldHint[field.key]} />}
833
- </label>
834
- {field.help && <p className="text-[12px] text-text-3/60 mb-2">{field.help}</p>}
835
- <div className="flex flex-wrap gap-2 mb-2">
836
- {tags.map((tag, i) => (
837
- <span key={i} className="flex items-center gap-1.5 px-3 py-1.5 rounded-[8px] bg-accent-soft/50 border border-accent-bright/20 text-[12px] font-mono text-accent-bright">
838
- {tag}
839
- <button
840
- aria-label={`Remove ${tag}`}
841
- onClick={() => {
842
- const next = tags.filter((_, j) => j !== i).join(',')
843
- setConfig({ ...config, [field.key]: next })
844
- }}
845
- className="ml-0.5 w-4 h-4 flex items-center justify-center rounded-full hover:bg-white/10 transition-colors cursor-pointer text-accent-bright/50 hover:text-accent-bright"
846
- >
847
- <svg width="8" height="8" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round">
848
- <line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" />
849
- </svg>
850
- </button>
851
- </span>
852
- ))}
853
- </div>
854
- <div className="flex gap-2">
855
- <input
856
- id={`tag-input-${field.key}`}
857
- placeholder={field.placeholder}
858
- className={`${inputClass} font-mono text-[13px] flex-1`}
859
- style={{ fontFamily: undefined }}
860
- onKeyDown={(e) => {
861
- if (e.key === 'Enter' || e.key === ',') {
862
- e.preventDefault()
863
- const input = e.currentTarget
864
- const val = input.value.trim().replace(/,/g, '')
865
- if (val) {
866
- const next = tags.length > 0 ? `${tags.join(',')},${val}` : val
867
- setConfig({ ...config, [field.key]: next })
868
- input.value = ''
869
- }
870
- }
871
- }}
872
- />
873
- <button
874
- type="button"
875
- onClick={() => {
876
- const input = document.getElementById(`tag-input-${field.key}`) as HTMLInputElement
877
- const val = input?.value.trim().replace(/,/g, '')
878
- if (val) {
879
- const next = tags.length > 0 ? `${tags.join(',')},${val}` : val
880
- setConfig({ ...config, [field.key]: next })
881
- input.value = ''
882
- }
883
- }}
884
- className="px-4 py-2.5 rounded-[10px] bg-accent-soft/50 text-accent-bright text-[12px] font-600 hover:bg-accent-soft transition-colors cursor-pointer border border-accent-bright/20"
885
- >
886
- Add
887
- </button>
888
- </div>
889
- </div>
890
- )
891
- }
892
- return (
893
- <div key={field.key} className="mb-6">
894
- <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
895
- {field.label} <span className="normal-case tracking-normal font-normal text-text-3">(optional)</span>
896
- </label>
897
- {field.help && <p className="text-[12px] text-text-3/60 mb-2">{field.help}</p>}
898
- <input
899
- value={config[field.key] || ''}
900
- onChange={(e) => setConfig({ ...config, [field.key]: e.target.value })}
901
- placeholder={field.placeholder}
902
- className={`${inputClass} font-mono text-[13px]`}
903
- style={{ fontFamily: undefined }}
904
- />
905
- </div>
906
- )
907
- })}
1071
+ {platformConfig.configFields.length > 0 && (
1072
+ <div className="mb-2">
1073
+ <SectionLabel>Platform Settings</SectionLabel>
1074
+ <p className="text-[12px] text-text-3/60 mb-4">
1075
+ Settings specific to {platformConfig.label}. Leave optional values unset unless you need to override the defaults.
1076
+ </p>
1077
+ {platformConfig.configFields.map((field) => renderConfigField(field))}
1078
+ </div>
1079
+ )}
1080
+
1081
+ <div className="mb-2">
1082
+ <SectionLabel>Routing &amp; Autonomy</SectionLabel>
1083
+ <p className="text-[12px] text-text-3/60 mb-4">
1084
+ Conversation identity, reply behavior, reset policy, and other connector runtime overrides.
1085
+ </p>
1086
+ {COMMON_CONFIG_FIELDS.map((field) => renderConfigField(field))}
1087
+ </div>
908
1088
 
909
1089
  {/* Start/Stop controls for editing */}
910
1090
  {editing && (() => {
@@ -973,9 +1153,15 @@ export function ConnectorSheet() {
973
1153
  · Thread: <span className="text-text-2">{doctorPolicy.threadBinding || 'prefer'}</span>
974
1154
  </div>
975
1155
  <div className="rounded-[10px] border border-white/[0.06] bg-white/[0.02] px-3 py-2 text-[12px] text-text-3/80">
976
- Group: <span className="text-text-2">{doctorPolicy.groupPolicy || 'reply-or-mention'}</span>{' '}
1156
+ DMs: <span className="text-text-2">{config.dmPolicy || 'open'}</span>{' '}
1157
+ · Group: <span className="text-text-2">{doctorPolicy.groupPolicy || 'reply-or-mention'}</span>{' '}
977
1158
  · Debounce: <span className="text-text-2">{doctorPolicy.inboundDebounceMs ?? 700}ms</span>
978
1159
  </div>
1160
+ <div className="rounded-[10px] border border-white/[0.06] bg-white/[0.02] px-3 py-2 text-[12px] text-text-3/80">
1161
+ Allowlist: <span className="text-text-2">{config.allowFrom ? config.allowFrom.split(',').map((entry) => entry.trim()).filter(Boolean).length : 0}</span>{' '}
1162
+ · Reactions: <span className="text-text-2">{doctorPolicy.statusReactions === false ? 'off' : 'on'}</span>{' '}
1163
+ · Typing: <span className="text-text-2">{doctorPolicy.typingIndicators === false ? 'off' : 'on'}</span>
1164
+ </div>
979
1165
  <div className="rounded-[10px] border border-white/[0.06] bg-white/[0.02] px-3 py-2 text-[12px] text-text-3/80">
980
1166
  Reset: <span className="text-text-2">{doctorPolicy.resetMode || 'idle'}</span>{' '}
981
1167
  {doctorPolicy.resetMode === 'daily'
@@ -1007,12 +1193,6 @@ export function ConnectorSheet() {
1007
1193
  </p>
1008
1194
  </div>
1009
1195
 
1010
- {editing && (
1011
- <div className="mb-6 p-4 rounded-[14px] border border-white/[0.06] bg-white/[0.01]">
1012
- <ConnectorHealth connectorId={editing.id} />
1013
- </div>
1014
- )}
1015
-
1016
1196
  {/* WhatsApp QR code */}
1017
1197
  {editing && editing.platform === 'whatsapp' && (editing.status === 'running' || waConnecting) && qrDataUrl && (
1018
1198
  <div className="mb-6 p-5 rounded-[14px] border border-white/[0.06] bg-white/[0.01] text-center"
@@ -1034,22 +1214,7 @@ export function ConnectorSheet() {
1034
1214
  <div className="text-[13px] font-600 text-green-400 mb-1">Connected</div>
1035
1215
  <p className="text-[11px] text-text-3 mb-3">WhatsApp is paired and listening for messages</p>
1036
1216
  <button
1037
- onClick={async () => {
1038
- if (!confirm('Unlink this device? You will need to scan a new QR code.')) return
1039
- setActionLoading(true)
1040
- try {
1041
- await api('PUT', `/connectors/${editing.id}`, { action: 'repair' })
1042
- setWaAuthenticated(false)
1043
- setWaHasCreds(false)
1044
- setQrDataUrl(null)
1045
- setWaConnecting(true)
1046
- await loadConnectors()
1047
- } catch (err: unknown) {
1048
- toast.error(`Failed to unlink: ${err instanceof Error ? err.message : String(err)}`)
1049
- } finally {
1050
- setActionLoading(false)
1051
- }
1052
- }}
1217
+ onClick={() => setConfirmWhatsAppAction('unlink')}
1053
1218
  disabled={actionLoading}
1054
1219
  className="text-[12px] text-text-3 hover:text-red-400 transition-colors cursor-pointer bg-transparent border-none underline underline-offset-2"
1055
1220
  style={{ fontFamily: 'inherit' }}
@@ -1075,22 +1240,7 @@ export function ConnectorSheet() {
1075
1240
  </p>
1076
1241
  {waHasCreds && (
1077
1242
  <button
1078
- onClick={async () => {
1079
- if (!confirm('Force re-pair? This will clear saved credentials and show a new QR code.')) return
1080
- setActionLoading(true)
1081
- try {
1082
- await api('PUT', `/connectors/${editing.id}`, { action: 'repair' })
1083
- setWaAuthenticated(false)
1084
- setWaHasCreds(false)
1085
- setQrDataUrl(null)
1086
- setWaConnecting(true)
1087
- await loadConnectors()
1088
- } catch (err: unknown) {
1089
- toast.error(`Failed to re-pair: ${err instanceof Error ? err.message : String(err)}`)
1090
- } finally {
1091
- setActionLoading(false)
1092
- }
1093
- }}
1243
+ onClick={() => setConfirmWhatsAppAction('repair')}
1094
1244
  disabled={actionLoading}
1095
1245
  className="mt-3 text-[12px] text-text-3 hover:text-amber-400 transition-colors cursor-pointer bg-transparent border-none underline underline-offset-2"
1096
1246
  style={{ fontFamily: 'inherit' }}
@@ -1123,11 +1273,50 @@ export function ConnectorSheet() {
1123
1273
  saveLabel={saving ? 'Saving...' : editing ? 'Save' : 'Create Connector'}
1124
1274
  saveDisabled={saving || (routeMode === 'agent' ? !agentId : !chatroomId)}
1125
1275
  left={editing && (
1126
- <button onClick={handleDelete} className="py-3.5 px-6 rounded-[14px] border border-red-500/20 bg-transparent text-red-400 text-[15px] font-600 cursor-pointer hover:bg-red-500/10 transition-all" style={{ fontFamily: 'inherit' }}>
1276
+ <button
1277
+ onClick={() => setConfirmDelete(true)}
1278
+ disabled={deleting}
1279
+ className="py-3.5 px-6 rounded-[14px] border border-red-500/20 bg-transparent text-red-400 text-[15px] font-600 cursor-pointer hover:bg-red-500/10 transition-all disabled:cursor-not-allowed disabled:opacity-60"
1280
+ style={{ fontFamily: 'inherit' }}
1281
+ >
1127
1282
  Delete
1128
1283
  </button>
1129
1284
  )}
1130
1285
  />
1286
+ <ConfirmDialog
1287
+ open={confirmDelete}
1288
+ title="Delete Connector?"
1289
+ message={editing ? `Delete "${editing.name}"? This will stop the connector and remove its configuration from the app.` : 'Delete this connector?'}
1290
+ confirmLabel={deleting ? 'Deleting...' : 'Delete'}
1291
+ confirmDisabled={deleting}
1292
+ cancelDisabled={deleting}
1293
+ danger
1294
+ onConfirm={() => { void handleDelete() }}
1295
+ onCancel={() => { if (!deleting) setConfirmDelete(false) }}
1296
+ />
1297
+ <ConfirmDialog
1298
+ open={!!confirmWhatsAppAction}
1299
+ title={confirmWhatsAppAction === 'unlink' ? 'Unlink Device?' : 'Force Re-pair?'}
1300
+ message={
1301
+ confirmWhatsAppAction === 'unlink'
1302
+ ? 'Unlink this device? You will need to scan a new QR code.'
1303
+ : 'Force re-pair? This will clear saved credentials and show a new QR code.'
1304
+ }
1305
+ confirmLabel={
1306
+ actionLoading
1307
+ ? confirmWhatsAppAction === 'unlink'
1308
+ ? 'Unlinking...'
1309
+ : 'Re-pairing...'
1310
+ : confirmWhatsAppAction === 'unlink'
1311
+ ? 'Unlink'
1312
+ : 'Re-pair'
1313
+ }
1314
+ confirmDisabled={actionLoading}
1315
+ cancelDisabled={actionLoading}
1316
+ danger
1317
+ onConfirm={() => { if (confirmWhatsAppAction) void handleWhatsAppRepair(confirmWhatsAppAction) }}
1318
+ onCancel={() => { if (!actionLoading) setConfirmWhatsAppAction(null) }}
1319
+ />
1131
1320
  </BottomSheet>
1132
1321
  )
1133
1322
  }