@swarmclawai/swarmclaw 0.3.1 → 0.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (203) hide show
  1. package/README.md +33 -13
  2. package/bin/server-cmd.js +14 -7
  3. package/bin/swarmclaw.js +3 -1
  4. package/bin/update-cmd.js +120 -0
  5. package/next.config.ts +10 -0
  6. package/package.json +4 -1
  7. package/src/app/api/agents/[id]/route.ts +20 -18
  8. package/src/app/api/agents/[id]/thread/route.ts +4 -3
  9. package/src/app/api/agents/route.ts +8 -3
  10. package/src/app/api/auth/route.ts +3 -1
  11. package/src/app/api/claude-skills/route.ts +3 -1
  12. package/src/app/api/clawhub/install/route.ts +2 -2
  13. package/src/app/api/connectors/[id]/route.ts +14 -3
  14. package/src/app/api/connectors/[id]/webhook/route.ts +99 -0
  15. package/src/app/api/connectors/route.ts +12 -4
  16. package/src/app/api/credentials/[id]/route.ts +2 -1
  17. package/src/app/api/credentials/route.ts +5 -3
  18. package/src/app/api/daemon/route.ts +6 -1
  19. package/src/app/api/documents/route.ts +2 -2
  20. package/src/app/api/files/serve/route.ts +8 -0
  21. package/src/app/api/ip/route.ts +3 -1
  22. package/src/app/api/knowledge/[id]/route.ts +5 -4
  23. package/src/app/api/knowledge/upload/route.ts +2 -2
  24. package/src/app/api/mcp-servers/[id]/route.ts +11 -14
  25. package/src/app/api/mcp-servers/[id]/test/route.ts +2 -1
  26. package/src/app/api/mcp-servers/[id]/tools/route.ts +2 -1
  27. package/src/app/api/mcp-servers/route.ts +5 -3
  28. package/src/app/api/memory/[id]/route.ts +9 -8
  29. package/src/app/api/memory/route.ts +2 -2
  30. package/src/app/api/memory-images/[filename]/route.ts +2 -1
  31. package/src/app/api/openclaw/directory/route.ts +26 -0
  32. package/src/app/api/openclaw/discover/route.ts +61 -0
  33. package/src/app/api/openclaw/sync/route.ts +30 -0
  34. package/src/app/api/orchestrator/graph/route.ts +25 -0
  35. package/src/app/api/orchestrator/run/route.ts +2 -2
  36. package/src/app/api/plugins/marketplace/route.ts +3 -1
  37. package/src/app/api/plugins/route.ts +3 -1
  38. package/src/app/api/projects/[id]/route.ts +55 -0
  39. package/src/app/api/projects/route.ts +27 -0
  40. package/src/app/api/providers/[id]/models/route.ts +2 -1
  41. package/src/app/api/providers/[id]/route.ts +13 -12
  42. package/src/app/api/providers/configs/route.ts +3 -1
  43. package/src/app/api/providers/route.ts +7 -3
  44. package/src/app/api/schedules/[id]/route.ts +16 -15
  45. package/src/app/api/schedules/[id]/run/route.ts +4 -3
  46. package/src/app/api/schedules/route.ts +8 -3
  47. package/src/app/api/secrets/[id]/route.ts +16 -17
  48. package/src/app/api/secrets/route.ts +5 -3
  49. package/src/app/api/sessions/[id]/chat/route.ts +5 -2
  50. package/src/app/api/sessions/[id]/clear/route.ts +2 -1
  51. package/src/app/api/sessions/[id]/deploy/route.ts +2 -1
  52. package/src/app/api/sessions/[id]/devserver/route.ts +2 -1
  53. package/src/app/api/sessions/[id]/messages/route.ts +2 -1
  54. package/src/app/api/sessions/[id]/retry/route.ts +2 -1
  55. package/src/app/api/sessions/[id]/route.ts +2 -1
  56. package/src/app/api/sessions/route.ts +11 -4
  57. package/src/app/api/settings/route.ts +3 -1
  58. package/src/app/api/setup/doctor/route.ts +1 -0
  59. package/src/app/api/setup/openclaw-device/route.ts +3 -1
  60. package/src/app/api/skills/[id]/route.ts +23 -21
  61. package/src/app/api/skills/import/route.ts +2 -2
  62. package/src/app/api/skills/route.ts +5 -3
  63. package/src/app/api/tasks/[id]/approve/route.ts +74 -0
  64. package/src/app/api/tasks/[id]/route.ts +9 -5
  65. package/src/app/api/tasks/route.ts +5 -2
  66. package/src/app/api/tts/stream/route.ts +48 -0
  67. package/src/app/api/upload/route.ts +2 -2
  68. package/src/app/api/uploads/[filename]/route.ts +4 -1
  69. package/src/app/api/usage/route.ts +3 -1
  70. package/src/app/api/version/route.ts +3 -1
  71. package/src/app/api/webhooks/[id]/route.ts +31 -32
  72. package/src/app/api/webhooks/route.ts +5 -3
  73. package/src/app/icon.svg +58 -0
  74. package/src/app/page.tsx +11 -26
  75. package/src/cli/index.js +28 -9
  76. package/src/cli/index.ts +45 -2
  77. package/src/cli/spec.js +2 -8
  78. package/src/components/agents/agent-card.tsx +1 -1
  79. package/src/components/agents/agent-list.tsx +3 -1
  80. package/src/components/agents/agent-sheet.tsx +166 -81
  81. package/src/components/chat/chat-area.tsx +71 -34
  82. package/src/components/chat/chat-header.tsx +141 -29
  83. package/src/components/chat/chat-tool-toggles.tsx +12 -53
  84. package/src/components/chat/message-bubble.tsx +110 -42
  85. package/src/components/chat/tool-call-bubble.tsx +50 -6
  86. package/src/components/chat/tool-request-banner.tsx +1 -9
  87. package/src/components/chat/voice-overlay.tsx +80 -0
  88. package/src/components/connectors/connector-list.tsx +9 -10
  89. package/src/components/connectors/connector-sheet.tsx +55 -36
  90. package/src/components/input/chat-input.tsx +72 -56
  91. package/src/components/knowledge/knowledge-list.tsx +27 -31
  92. package/src/components/layout/app-layout.tsx +133 -90
  93. package/src/components/layout/daemon-indicator.tsx +3 -5
  94. package/src/components/logs/log-list.tsx +5 -9
  95. package/src/components/mcp-servers/mcp-server-list.tsx +24 -2
  96. package/src/components/memory/memory-detail.tsx +1 -1
  97. package/src/components/plugins/plugin-list.tsx +227 -27
  98. package/src/components/projects/project-list.tsx +122 -0
  99. package/src/components/projects/project-sheet.tsx +135 -0
  100. package/src/components/providers/provider-list.tsx +46 -13
  101. package/src/components/providers/provider-sheet.tsx +0 -45
  102. package/src/components/runs/run-list.tsx +6 -15
  103. package/src/components/schedules/schedule-card.tsx +54 -4
  104. package/src/components/schedules/schedule-list.tsx +9 -4
  105. package/src/components/schedules/schedule-sheet.tsx +0 -47
  106. package/src/components/secrets/secrets-list.tsx +20 -2
  107. package/src/components/sessions/new-session-sheet.tsx +14 -15
  108. package/src/components/sessions/session-card.tsx +1 -1
  109. package/src/components/sessions/session-list.tsx +7 -7
  110. package/src/components/shared/connector-platform-icon.tsx +26 -20
  111. package/src/components/shared/model-combobox.tsx +148 -0
  112. package/src/components/shared/settings/section-heartbeat.tsx +8 -40
  113. package/src/components/shared/settings/section-orchestrator.tsx +9 -11
  114. package/src/components/shared/settings/section-web-search.tsx +56 -0
  115. package/src/components/shared/settings/settings-page.tsx +73 -0
  116. package/src/components/skills/skill-list.tsx +262 -35
  117. package/src/components/skills/skill-sheet.tsx +0 -45
  118. package/src/components/tasks/task-board.tsx +3 -6
  119. package/src/components/tasks/task-card.tsx +43 -1
  120. package/src/components/tasks/task-list.tsx +8 -7
  121. package/src/components/tasks/task-sheet.tsx +0 -44
  122. package/src/components/usage/usage-list.tsx +12 -4
  123. package/src/hooks/use-continuous-speech.ts +144 -0
  124. package/src/hooks/use-view-router.ts +52 -0
  125. package/src/hooks/use-voice-conversation.ts +80 -0
  126. package/src/hooks/use-ws.ts +66 -0
  127. package/src/instrumentation.ts +2 -0
  128. package/src/lib/chat.ts +14 -2
  129. package/src/lib/id.ts +6 -0
  130. package/src/lib/projects.ts +13 -0
  131. package/src/lib/provider-sets.ts +5 -0
  132. package/src/lib/providers/anthropic.ts +15 -2
  133. package/src/lib/providers/index.ts +8 -0
  134. package/src/lib/providers/ollama.ts +10 -2
  135. package/src/lib/providers/openai.ts +42 -13
  136. package/src/lib/providers/openclaw.ts +11 -0
  137. package/src/lib/server/api-routes.test.ts +5 -6
  138. package/src/lib/server/build-llm.ts +17 -4
  139. package/src/lib/server/chat-execution.ts +57 -8
  140. package/src/lib/server/collection-helpers.ts +54 -0
  141. package/src/lib/server/connectors/bluebubbles.test.ts +208 -0
  142. package/src/lib/server/connectors/bluebubbles.ts +357 -0
  143. package/src/lib/server/connectors/connector-routing.test.ts +1 -1
  144. package/src/lib/server/connectors/googlechat.ts +46 -7
  145. package/src/lib/server/connectors/manager.ts +401 -6
  146. package/src/lib/server/connectors/media.ts +2 -2
  147. package/src/lib/server/connectors/openclaw.ts +64 -0
  148. package/src/lib/server/connectors/pairing.test.ts +99 -0
  149. package/src/lib/server/connectors/pairing.ts +256 -0
  150. package/src/lib/server/connectors/signal.ts +1 -0
  151. package/src/lib/server/connectors/teams.ts +5 -5
  152. package/src/lib/server/connectors/types.ts +10 -0
  153. package/src/lib/server/context-manager.ts +1 -1
  154. package/src/lib/server/daemon-state.ts +3 -0
  155. package/src/lib/server/data-dir.ts +1 -0
  156. package/src/lib/server/execution-log.ts +3 -3
  157. package/src/lib/server/heartbeat-service.ts +67 -3
  158. package/src/lib/server/knowledge-db.test.ts +2 -33
  159. package/src/lib/server/langgraph-checkpoint.ts +274 -0
  160. package/src/lib/server/main-agent-loop.ts +67 -8
  161. package/src/lib/server/memory-db.ts +6 -6
  162. package/src/lib/server/openclaw-approvals.ts +105 -0
  163. package/src/lib/server/openclaw-sync.ts +496 -0
  164. package/src/lib/server/orchestrator-lg.ts +422 -20
  165. package/src/lib/server/orchestrator.ts +29 -9
  166. package/src/lib/server/process-manager.ts +2 -2
  167. package/src/lib/server/queue.ts +39 -13
  168. package/src/lib/server/scheduler.ts +2 -2
  169. package/src/lib/server/session-mailbox.ts +2 -2
  170. package/src/lib/server/session-run-manager.ts +8 -3
  171. package/src/lib/server/session-tools/connector.ts +51 -4
  172. package/src/lib/server/session-tools/crud.ts +3 -3
  173. package/src/lib/server/session-tools/delegate.ts +5 -5
  174. package/src/lib/server/session-tools/file.ts +176 -3
  175. package/src/lib/server/session-tools/index.ts +4 -0
  176. package/src/lib/server/session-tools/memory.ts +2 -2
  177. package/src/lib/server/session-tools/openclaw-nodes.ts +112 -0
  178. package/src/lib/server/session-tools/sandbox.ts +197 -0
  179. package/src/lib/server/session-tools/search-providers.ts +270 -0
  180. package/src/lib/server/session-tools/session-info.ts +2 -2
  181. package/src/lib/server/session-tools/web.ts +47 -66
  182. package/src/lib/server/storage-mcp.test.ts +25 -2
  183. package/src/lib/server/storage.ts +36 -7
  184. package/src/lib/server/stream-agent-chat.ts +106 -22
  185. package/src/lib/server/task-result.test.ts +44 -0
  186. package/src/lib/server/task-result.ts +14 -0
  187. package/src/lib/server/task-validation.test.ts +23 -0
  188. package/src/lib/server/task-validation.ts +5 -3
  189. package/src/lib/server/ws-hub.ts +85 -0
  190. package/src/lib/tool-definitions.ts +44 -0
  191. package/src/lib/tts-stream.ts +130 -0
  192. package/src/lib/upload.ts +7 -1
  193. package/src/lib/view-routes.ts +28 -0
  194. package/src/lib/ws-client.ts +124 -0
  195. package/src/proxy.ts +3 -0
  196. package/src/stores/use-app-store.ts +28 -1
  197. package/src/stores/use-chat-store.ts +42 -14
  198. package/src/types/index.ts +34 -2
  199. package/src/app/api/agents/generate/route.ts +0 -42
  200. package/src/app/api/generate/info/route.ts +0 -12
  201. package/src/app/api/generate/route.ts +0 -106
  202. package/src/app/favicon.ico +0 -0
  203. package/src/components/shared/ai-gen-block.tsx +0 -77
@@ -168,6 +168,115 @@ function heartbeatSummary(text: string): string {
168
168
  return clean.length > 180 ? `${clean.slice(0, 180)}...` : clean
169
169
  }
170
170
 
171
+ const IMAGE_ATTACH_RE = /\.(png|jpg|jpeg|gif|webp|svg|bmp|ico)$/i
172
+ const PREVIEWABLE_ATTACH_RE = /\.(html?|svg)$/i
173
+ const FILE_TYPE_COLORS: Record<string, string> = {
174
+ html: 'text-orange-400', htm: 'text-orange-400', svg: 'text-emerald-400',
175
+ js: 'text-yellow-400', jsx: 'text-yellow-400', ts: 'text-blue-400', tsx: 'text-blue-400',
176
+ py: 'text-green-400', json: 'text-amber-300', css: 'text-purple-400', scss: 'text-pink-400',
177
+ md: 'text-text-2', txt: 'text-text-3', pdf: 'text-red-400',
178
+ }
179
+
180
+ function parseAttachmentUrl(filePath?: string, fileUrl?: string) {
181
+ const url = fileUrl || (filePath ? `/api/uploads/${filePath.split('/').pop()}` : '')
182
+ const rawName = filePath?.split('/').pop() || fileUrl?.split('/').pop() || 'file'
183
+ const filename = rawName.replace(/^[a-f0-9]+-/, '').split('?')[0]
184
+ return { url, filename }
185
+ }
186
+
187
+ function AttachmentChip({ url, filename, isUserMsg }: { url: string; filename: string; isUserMsg?: boolean }) {
188
+ const isImage = IMAGE_ATTACH_RE.test(filename)
189
+ if (isImage) {
190
+ return (
191
+ <img src={url} alt="Attached" className="max-w-[240px] rounded-[12px] mb-2 border border-white/10"
192
+ onError={(e) => { (e.target as HTMLImageElement).style.display = 'none' }} />
193
+ )
194
+ }
195
+
196
+ const ext = filename.split('.').pop()?.toLowerCase() || ''
197
+ const colorClass = FILE_TYPE_COLORS[ext] || 'text-text-3'
198
+ const isPreviewable = PREVIEWABLE_ATTACH_RE.test(filename)
199
+
200
+ // Solid bg so chip is readable on both user (purple) and assistant bubbles
201
+ const chipBg = isUserMsg
202
+ ? 'bg-[rgba(0,0,0,0.25)] border-white/[0.12]'
203
+ : 'bg-[rgba(255,255,255,0.04)] border-white/[0.08]'
204
+ const iconBg = isUserMsg ? 'bg-white/[0.12]' : 'bg-white/[0.05]'
205
+ const btnBg = isUserMsg
206
+ ? 'bg-white/[0.12] hover:bg-white/[0.18] text-white/80'
207
+ : 'bg-white/[0.06] hover:bg-white/[0.10] text-text-3'
208
+
209
+ return (
210
+ <div className={`flex items-center gap-3 px-4 py-2.5 mb-2 rounded-[12px] border ${chipBg}`}>
211
+ <div className={`flex items-center justify-center w-8 h-8 rounded-[8px] shrink-0 ${iconBg} ${colorClass}`}>
212
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round">
213
+ <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
214
+ <polyline points="14 2 14 8 20 8" />
215
+ </svg>
216
+ </div>
217
+ <div className="flex flex-col flex-1 min-w-0">
218
+ <span className={`text-[13px] font-500 truncate ${isUserMsg ? 'text-white' : 'text-text'}`}>{filename}</span>
219
+ <span className={`text-[11px] uppercase tracking-wide ${isUserMsg ? 'text-white/50' : 'text-text-3/70'}`}>{ext || 'file'}</span>
220
+ </div>
221
+ {isPreviewable && (
222
+ <a href={url} target="_blank" rel="noopener noreferrer"
223
+ className={`flex items-center gap-1.5 px-2.5 py-1 rounded-[8px] text-[11px] font-600 no-underline transition-colors shrink-0 ${
224
+ isUserMsg ? 'bg-white/[0.15] hover:bg-white/[0.22] text-white' : 'bg-accent-soft hover:bg-accent-soft/80 text-accent-bright'
225
+ }`}
226
+ title="Preview in new tab">
227
+ <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
228
+ <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
229
+ <circle cx="12" cy="12" r="3" />
230
+ </svg>
231
+ Preview
232
+ </a>
233
+ )}
234
+ <a href={url} download={filename}
235
+ className={`flex items-center gap-1.5 px-2.5 py-1 rounded-[8px] text-[11px] font-600 no-underline transition-colors shrink-0 ${btnBg}`}>
236
+ <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
237
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
238
+ <polyline points="7 10 12 15 17 10" />
239
+ <line x1="12" y1="15" x2="12" y2="3" />
240
+ </svg>
241
+ Download
242
+ </a>
243
+ </div>
244
+ )
245
+ }
246
+
247
+ function renderAttachments(message: Message) {
248
+ const isUser = message.role === 'user'
249
+ const seen = new Set<string>()
250
+ const chips: { url: string; filename: string }[] = []
251
+
252
+ // Primary attachment
253
+ if (message.imagePath || message.imageUrl) {
254
+ const primary = parseAttachmentUrl(message.imagePath, message.imageUrl)
255
+ if (primary.url) {
256
+ seen.add(primary.url)
257
+ chips.push(primary)
258
+ }
259
+ }
260
+
261
+ // Additional attached files
262
+ if (message.attachedFiles?.length) {
263
+ for (const fp of message.attachedFiles) {
264
+ const att = parseAttachmentUrl(fp)
265
+ if (att.url && !seen.has(att.url)) {
266
+ seen.add(att.url)
267
+ chips.push(att)
268
+ }
269
+ }
270
+ }
271
+
272
+ if (!chips.length) return null
273
+ return (
274
+ <div className="flex flex-col">
275
+ {chips.map((c) => <AttachmentChip key={c.url} url={c.url} filename={c.filename} isUserMsg={isUser} />)}
276
+ </div>
277
+ )
278
+ }
279
+
171
280
  interface Props {
172
281
  message: Message
173
282
  assistantName?: string
@@ -240,48 +349,7 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
240
349
 
241
350
  {/* Message bubble */}
242
351
  <div className={`max-w-[85%] md:max-w-[72%] ${isUser ? 'bubble-user px-5 py-3.5' : isHeartbeat ? 'bubble-ai px-4 py-3' : 'bubble-ai px-5 py-3.5'}`}>
243
- {(message.imagePath || message.imageUrl) && (() => {
244
- const url = message.imageUrl || `/api/uploads/${message.imagePath?.split('/').pop()}`
245
- const rawName = message.imagePath?.split('/').pop() || message.imageUrl?.split('/').pop() || 'file'
246
- const filename = rawName.replace(/^[a-f0-9]+-/, '').split('?')[0]
247
- const isImage = /\.(png|jpg|jpeg|gif|webp|svg|bmp|ico)$/i.test(filename)
248
- if (isImage) {
249
- return (
250
- <img src={url} alt="Attached" className="max-w-[240px] rounded-[12px] mb-3 border border-white/10"
251
- onError={(e) => { (e.target as HTMLImageElement).style.display = 'none' }} />
252
- )
253
- }
254
- const isPreviewable = /\.(html?|svg)$/i.test(filename)
255
- return (
256
- <div className="flex items-center gap-3 px-4 py-3 mb-3 rounded-[12px] border border-white/10 bg-white/[0.03]">
257
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" className="text-text-3 shrink-0">
258
- <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
259
- <polyline points="14 2 14 8 20 8" />
260
- </svg>
261
- <span className="text-[13px] text-text-2 font-500 truncate flex-1">{filename}</span>
262
- {isPreviewable && (
263
- <a href={url} target="_blank" rel="noopener noreferrer"
264
- className="flex items-center gap-1.5 px-2.5 py-1 rounded-[8px] bg-accent-soft hover:bg-accent-soft/80 text-accent-bright text-[11px] font-600 no-underline transition-colors shrink-0"
265
- title="Preview in new tab">
266
- <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
267
- <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
268
- <circle cx="12" cy="12" r="3" />
269
- </svg>
270
- Preview
271
- </a>
272
- )}
273
- <a href={url} download={filename}
274
- className="flex items-center gap-1.5 px-2.5 py-1 rounded-[8px] bg-white/[0.06] hover:bg-white/[0.10] text-text-3 text-[11px] font-600 no-underline transition-colors shrink-0">
275
- <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
276
- <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
277
- <polyline points="7 10 12 15 17 10" />
278
- <line x1="12" y1="15" x2="12" y2="3" />
279
- </svg>
280
- Download
281
- </a>
282
- </div>
283
- )
284
- })()}
352
+ {renderAttachments(message)}
285
353
 
286
354
  {isHeartbeat ? (
287
355
  <div className="flex flex-col gap-2">
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { useState, useMemo } from 'react'
4
4
  import type { ToolEvent } from '@/stores/use-chat-store'
5
+ import { useAppStore } from '@/stores/use-app-store'
5
6
 
6
7
  const TOOL_COLORS: Record<string, string> = {
7
8
  execute_command: '#F59E0B',
@@ -13,8 +14,11 @@ const TOOL_COLORS: Record<string, string> = {
13
14
  delete_file: '#EF4444',
14
15
  edit_file: '#10B981',
15
16
  send_file: '#10B981',
17
+ create_document: '#10B981',
18
+ create_spreadsheet: '#10B981',
16
19
  web_search: '#3B82F6',
17
20
  web_fetch: '#3B82F6',
21
+ delegate_to_agent: '#6366F1',
18
22
  delegate_to_claude_code: '#6366F1',
19
23
  delegate_to_codex_cli: '#0EA5E9',
20
24
  delegate_to_opencode_cli: '#14B8A6',
@@ -58,11 +62,14 @@ export const TOOL_LABELS: Record<string, string> = {
58
62
  delete_file: 'Delete File',
59
63
  edit_file: 'Edit File',
60
64
  send_file: 'Send File',
65
+ create_document: 'Create Document',
66
+ create_spreadsheet: 'Create Spreadsheet',
61
67
  web_search: 'Web Search',
62
68
  web_fetch: 'Web Fetch',
63
69
  claude_code: 'Claude Code',
64
70
  codex_cli: 'Codex CLI',
65
71
  opencode_cli: 'OpenCode CLI',
72
+ delegate_to_agent: 'Agent Delegation',
66
73
  delegate_to_claude_code: 'Claude Code',
67
74
  delegate_to_codex_cli: 'Codex CLI',
68
75
  delegate_to_opencode_cli: 'OpenCode CLI',
@@ -76,7 +83,7 @@ export const TOOL_LABELS: Record<string, string> = {
76
83
  manage_documents: 'Documents',
77
84
  manage_webhooks: 'Webhooks',
78
85
  manage_connectors: 'Connectors',
79
- manage_sessions: 'Sessions',
86
+ manage_sessions: 'Chats',
80
87
  memory: 'Memory',
81
88
  browser: 'Browser',
82
89
  }
@@ -91,15 +98,18 @@ export const TOOL_DESCRIPTIONS: Record<string, string> = {
91
98
  delete_file: 'Delete files or directories (when explicitly enabled)',
92
99
  edit_file: 'Edit existing files with find-and-replace',
93
100
  send_file: 'Send files to the user (images, PDFs, videos, documents, etc.)',
101
+ create_document: 'Render markdown content into PDF, HTML, or image',
102
+ create_spreadsheet: 'Create Excel or CSV files from structured data',
94
103
  web_search: 'Search the web for information',
95
104
  web_fetch: 'Fetch and read web page content',
96
105
  claude_code: 'Enable delegation to Claude Code CLI',
97
106
  codex_cli: 'Enable delegation to OpenAI Codex CLI',
98
107
  opencode_cli: 'Enable delegation to OpenCode CLI',
108
+ delegate_to_agent: 'Delegate a task to another agent',
99
109
  delegate_to_claude_code: 'Delegate complex coding tasks to Claude Code',
100
110
  delegate_to_codex_cli: 'Delegate complex coding tasks to Codex CLI',
101
111
  delegate_to_opencode_cli: 'Delegate complex coding tasks to OpenCode CLI',
102
- whoami_tool: 'Reveal the current session and agent identity context',
112
+ whoami_tool: 'Reveal the current agent and chat context',
103
113
  connector_message_tool: 'Send proactive outbound messages via running connectors',
104
114
  search_history_tool: 'Search chat history for relevant prior context',
105
115
  manage_tasks: 'Create, update, and manage tasks on the board',
@@ -109,7 +119,7 @@ export const TOOL_DESCRIPTIONS: Record<string, string> = {
109
119
  manage_documents: 'Upload and search indexed documents',
110
120
  manage_webhooks: 'Register and manage inbound webhooks',
111
121
  manage_connectors: 'Manage chat platform connectors (Slack, Discord, etc.)',
112
- manage_sessions: 'Create and manage chat sessions',
122
+ manage_sessions: 'Create and manage agent chats',
113
123
  memory: 'Store and recall information across conversations',
114
124
  browser: 'Browse the web, take screenshots, and interact with pages',
115
125
  }
@@ -181,6 +191,7 @@ function getInputPreview(name: string, input: string): string {
181
191
  return ''
182
192
  }
183
193
  if (name === 'send_file') return parsed.filePath || ''
194
+ if (name === 'delegate_to_agent') return `${parsed.agentName}: ${(parsed.task || '').slice(0, 80)}`
184
195
 
185
196
  if (parsed.command) return parsed.command
186
197
  if (parsed.filePath) return parsed.filePath
@@ -280,6 +291,24 @@ export function ToolCallBubble({ event }: { event: ToolEvent }) {
280
291
 
281
292
  const hasMedia = media.images.length > 0 || media.videos.length > 0 || media.pdfs.length > 0 || media.files.length > 0
282
293
 
294
+ // Parse delegation info for clickable agent link
295
+ const delegationInfo = useMemo(() => {
296
+ if (event.name !== 'delegate_to_agent') return null
297
+ try {
298
+ const parsed = JSON.parse(event.input)
299
+ return { agentName: parsed.agentName || '', agentId: parsed.agentId || '', task: parsed.task || '' }
300
+ } catch { return null }
301
+ }, [event.name, event.input])
302
+
303
+ const handleAgentClick = (e: React.MouseEvent) => {
304
+ e.stopPropagation()
305
+ if (delegationInfo?.agentId) {
306
+ const store = useAppStore.getState()
307
+ store.setActiveView('agents')
308
+ store.setCurrentAgent(delegationInfo.agentId)
309
+ }
310
+ }
311
+
283
312
  return (
284
313
  <div className="w-full text-left">
285
314
  <button
@@ -303,9 +332,24 @@ export function ToolCallBubble({ event }: { event: ToolEvent }) {
303
332
  <span className="text-[12px] font-700 uppercase tracking-wider shrink-0" style={{ color }}>
304
333
  {label}
305
334
  </span>
306
- <span className="text-[12px] text-text-2 font-mono truncate flex-1">
307
- {inputPreview}
308
- </span>
335
+ {delegationInfo ? (
336
+ <span className="text-[12px] text-text-2 font-mono truncate flex-1">
337
+ <span
338
+ role="link"
339
+ tabIndex={0}
340
+ onClick={handleAgentClick}
341
+ onKeyDown={(e) => e.key === 'Enter' && handleAgentClick(e as any)}
342
+ className="text-accent-bright hover:underline cursor-pointer font-600"
343
+ >
344
+ {delegationInfo.agentName}
345
+ </span>
346
+ {delegationInfo.task && <span className="text-text-3">: {delegationInfo.task.slice(0, 80)}</span>}
347
+ </span>
348
+ ) : (
349
+ <span className="text-[12px] text-text-2 font-mono truncate flex-1">
350
+ {inputPreview}
351
+ </span>
352
+ )}
309
353
  {hasMedia && !expanded && (
310
354
  <span className="text-[10px] text-text-3/50 font-500 shrink-0">
311
355
  {media.images.length > 0 && `${media.images.length} image${media.images.length > 1 ? 's' : ''}`}
@@ -3,15 +3,7 @@
3
3
  import { useState } from 'react'
4
4
  import { useAppStore } from '@/stores/use-app-store'
5
5
  import { api } from '@/lib/api-client'
6
-
7
- const TOOL_LABELS: Record<string, string> = {
8
- shell: 'Shell', files: 'Files', edit_file: 'Edit File', process: 'Process',
9
- web_search: 'Web Search', web_fetch: 'Web Fetch', browser: 'Browser', memory: 'Memory',
10
- claude_code: 'Claude Code', codex_cli: 'Codex CLI', opencode_cli: 'OpenCode CLI',
11
- orchestrator: 'Orchestrator', manage_agents: 'Agents', manage_tasks: 'Tasks', manage_schedules: 'Schedules',
12
- manage_skills: 'Skills', manage_documents: 'Documents', manage_webhooks: 'Webhooks',
13
- manage_connectors: 'Connectors', manage_sessions: 'Sessions', manage_secrets: 'Secrets',
14
- }
6
+ import { TOOL_LABELS } from '@/lib/tool-definitions'
15
7
 
16
8
  interface Props {
17
9
  text: string
@@ -0,0 +1,80 @@
1
+ 'use client'
2
+
3
+ import type { VoiceConversationState } from '@/hooks/use-voice-conversation'
4
+
5
+ interface VoiceOverlayProps {
6
+ state: VoiceConversationState
7
+ interimText: string
8
+ transcript: string
9
+ onStop: () => void
10
+ }
11
+
12
+ const STATE_LABELS: Record<VoiceConversationState, string> = {
13
+ idle: '',
14
+ listening: 'Listening...',
15
+ processing: 'Processing...',
16
+ speaking: 'Speaking...',
17
+ }
18
+
19
+ export function VoiceOverlay({ state, interimText, transcript, onStop }: VoiceOverlayProps) {
20
+ if (state === 'idle') return null
21
+
22
+ return (
23
+ <div className="absolute inset-0 z-20 flex flex-col items-center justify-center gap-4 bg-bg/90 backdrop-blur-sm">
24
+ {/* Animated indicator */}
25
+ <div className="relative">
26
+ <div className={`w-20 h-20 rounded-full flex items-center justify-center ${
27
+ state === 'listening'
28
+ ? 'bg-accent/20 animate-pulse'
29
+ : state === 'speaking'
30
+ ? 'bg-green-500/20'
31
+ : 'bg-yellow-500/20'
32
+ }`}>
33
+ <div className={`w-12 h-12 rounded-full flex items-center justify-center ${
34
+ state === 'listening'
35
+ ? 'bg-accent/30'
36
+ : state === 'speaking'
37
+ ? 'bg-green-500/30'
38
+ : 'bg-yellow-500/30'
39
+ }`}>
40
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className={
41
+ state === 'listening' ? 'text-accent-bright' : state === 'speaking' ? 'text-green-400' : 'text-yellow-400'
42
+ }>
43
+ {state === 'speaking' ? (
44
+ <>
45
+ <polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5" />
46
+ <path d="M15.54 8.46a5 5 0 0 1 0 7.07" />
47
+ <path d="M19.07 4.93a10 10 0 0 1 0 14.14" />
48
+ </>
49
+ ) : (
50
+ <>
51
+ <path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z" />
52
+ <path d="M19 10v2a7 7 0 0 1-14 0v-2" />
53
+ <line x1="12" x2="12" y1="19" y2="22" />
54
+ </>
55
+ )}
56
+ </svg>
57
+ </div>
58
+ </div>
59
+ </div>
60
+
61
+ <div className="text-[14px] font-500 text-text-2">{STATE_LABELS[state]}</div>
62
+
63
+ {/* Transcript display */}
64
+ {(transcript || interimText) && (
65
+ <div className="max-w-md px-6 text-center">
66
+ {transcript && <p className="text-[14px] text-text-1 mb-1">{transcript}</p>}
67
+ {interimText && <p className="text-[13px] text-text-3/60 italic">{interimText}</p>}
68
+ </div>
69
+ )}
70
+
71
+ {/* Stop button */}
72
+ <button
73
+ onClick={onStop}
74
+ className="mt-2 px-5 py-2 rounded-lg bg-red-500/10 text-red-400 text-[13px] font-600 hover:bg-red-500/20 transition-colors"
75
+ >
76
+ Stop Voice
77
+ </button>
78
+ </div>
79
+ )
80
+ }
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { useCallback, useEffect, useState } from 'react'
4
4
  import { useAppStore } from '@/stores/use-app-store'
5
+ import { useWs } from '@/hooks/use-ws'
5
6
  import { api } from '@/lib/api-client'
6
7
  import type { Connector } from '@/types'
7
8
  import { ConnectorPlatformBadge, getConnectorPlatformLabel } from '@/components/shared/connector-platform-icon'
@@ -24,14 +25,8 @@ export function ConnectorList({ inSidebar: _inSidebar }: { inSidebar?: boolean }
24
25
  setLoaded(true)
25
26
  }, [loadConnectors, loadAgents])
26
27
 
27
- useEffect(() => {
28
- const bootstrap = setTimeout(() => { void refresh() }, 0)
29
- const poll = setInterval(() => { void loadConnectors() }, 15_000)
30
- return () => {
31
- clearTimeout(bootstrap)
32
- clearInterval(poll)
33
- }
34
- }, [refresh, loadConnectors])
28
+ useEffect(() => { void refresh() }, [refresh])
29
+ useWs('connectors', loadConnectors, 15_000)
35
30
 
36
31
  // Auto-clear error after 5s
37
32
  useEffect(() => {
@@ -109,8 +104,12 @@ export function ConnectorList({ inSidebar: _inSidebar }: { inSidebar?: boolean }
109
104
  const agent = agents[c.agentId]
110
105
  const isRunning = c.status === 'running'
111
106
  const isToggling = toggling === c.id
112
- // Can only toggle if connector has credentials (or is WhatsApp which uses QR)
113
- const hasCredentials = c.platform === 'whatsapp' || !!c.credentialId
107
+ // Can only toggle if connector has credentials (or uses non-token auth modes).
108
+ const hasCredentials = c.platform === 'whatsapp'
109
+ || c.platform === 'openclaw'
110
+ || c.platform === 'signal'
111
+ || (c.platform === 'bluebubbles' && (!!c.credentialId || !!c.config?.password))
112
+ || !!c.credentialId
114
113
  return (
115
114
  <div
116
115
  key={c.id}
@@ -1,9 +1,10 @@
1
1
  'use client'
2
2
 
3
- import { useState, useEffect } from 'react'
3
+ import { useState, useEffect, useCallback } from 'react'
4
4
  import { useAppStore } from '@/stores/use-app-store'
5
5
  import { BottomSheet } from '@/components/shared/bottom-sheet'
6
6
  import { api } from '@/lib/api-client'
7
+ import { useWs } from '@/hooks/use-ws'
7
8
  import { toast } from 'sonner'
8
9
  import type { Connector, ConnectorPlatform } from '@/types'
9
10
  import { ConnectorPlatformBadge } from '@/components/shared/connector-platform-icon'
@@ -113,7 +114,7 @@ const PLATFORMS: {
113
114
  tokenHelp: 'Required when your OpenClaw gateway is auth-protected',
114
115
  configFields: [
115
116
  { key: 'wsUrl', label: 'WebSocket URL', placeholder: 'ws://localhost:18789', help: 'OpenClaw gateway WebSocket endpoint (root URL, not /ws)' },
116
- { key: 'sessionKey', label: 'Session Key Filter', placeholder: 'main', help: 'Optional. If set, only inbound events for this OpenClaw session are processed.' },
117
+ { key: 'sessionKey', label: 'Chat Key Filter', placeholder: 'main', help: 'Optional. If set, only inbound events for this OpenClaw session are processed.' },
117
118
  { key: 'nodeId', label: 'Client Label', placeholder: 'swarmclaw', help: 'Optional display label shown in OpenClaw presence metadata.' },
118
119
  { key: 'role', label: 'Gateway Role', placeholder: 'operator', help: 'Optional role claim for connect handshake. Default is operator.' },
119
120
  { key: 'scopes', label: 'Scopes (CSV)', placeholder: 'operator.read,operator.write', help: 'Optional comma-separated scopes for OpenClaw connect.' },
@@ -121,6 +122,28 @@ const PLATFORMS: {
121
122
  { key: 'tickIntervalMs', label: 'Tick Interval Override (ms)', placeholder: '30000', help: 'Optional watchdog interval override when policy tick is unavailable.' },
122
123
  ],
123
124
  },
125
+ {
126
+ id: 'bluebubbles',
127
+ label: 'BlueBubbles',
128
+ color: '#2E89FF',
129
+ setupSteps: [
130
+ 'Run BlueBubbles server on your macOS host and enable the REST API',
131
+ 'Copy the BlueBubbles server password',
132
+ 'After saving the connector, point BlueBubbles webhook to /api/connectors/<connector-id>/webhook',
133
+ 'Optionally set dmPolicy=pairing to require explicit sender approval for new DMs',
134
+ ],
135
+ tokenLabel: 'BlueBubbles Password',
136
+ tokenHelp: 'Server password used for /api/v1/ping and /api/v1/message/text',
137
+ configFields: [
138
+ { key: 'serverUrl', label: 'Server URL', placeholder: 'http://127.0.0.1:1234', help: 'BlueBubbles server URL (no trailing /api path needed)' },
139
+ { key: 'chatIds', label: 'Allowed Chat IDs', placeholder: 'iMessage;-;+15551234567', help: 'Optional comma-separated chat IDs/guid fragments. Leave empty for all chats.' },
140
+ { key: 'dmPolicy', label: 'DM Policy', placeholder: 'open | allowlist | pairing | disabled', help: 'Access policy for direct-message senders. Default: open.' },
141
+ { key: 'allowFrom', label: 'Allowed Sender IDs', placeholder: '+15551234567,test@example.com', help: 'Optional comma-separated sender IDs for allowlist/pairing mode.' },
142
+ { key: 'outboundTarget', label: 'Default Outbound Target', placeholder: 'iMessage;-;+15551234567', help: 'Used when proactive sends omit "to".' },
143
+ { key: 'webhookSecret', label: 'Webhook Secret', placeholder: 'optional-shared-secret', help: 'Optional secret required by /api/connectors/{id}/webhook (header: x-connector-secret or ?secret=...)' },
144
+ { key: 'timeoutMs', label: 'Request Timeout (ms)', placeholder: '10000', help: 'Optional BlueBubbles API timeout in milliseconds.' },
145
+ ],
146
+ },
124
147
  {
125
148
  id: 'matrix',
126
149
  label: 'Matrix',
@@ -145,13 +168,14 @@ const PLATFORMS: {
145
168
  setupSteps: [
146
169
  'Create a Google Cloud project and enable the Google Chat API',
147
170
  'Create a service account and download the JSON key file',
148
- 'In Google Chat Admin, configure the bot with your app URL',
171
+ 'In Google Chat Admin, configure event delivery to /api/connectors/<connector-id>/webhook',
149
172
  'Paste the full service account JSON as the bot token',
150
173
  ],
151
174
  tokenLabel: 'Service Account JSON',
152
175
  tokenHelp: 'Paste the full service account JSON key file contents',
153
176
  configFields: [
154
177
  { key: 'spaceIds', label: 'Space IDs', placeholder: 'spaces/AAAA123', help: 'Comma-separated Google Chat space IDs' },
178
+ { key: 'webhookSecret', label: 'Webhook Secret', placeholder: 'optional-shared-secret', help: 'Optional secret required by /api/connectors/{id}/webhook (header: x-connector-secret or ?secret=...)' },
155
179
  ],
156
180
  },
157
181
  {
@@ -162,13 +186,14 @@ const PLATFORMS: {
162
186
  'Register a bot in the Azure Bot Framework portal',
163
187
  'Note the Microsoft App ID and generate an App Secret',
164
188
  'Set up a public HTTPS endpoint for webhook delivery',
165
- 'Configure the messaging endpoint in Azure to your notify URL',
189
+ 'After saving the connector, point Azure to /api/connectors/<connector-id>/webhook',
166
190
  ],
167
191
  tokenLabel: 'App Secret',
168
192
  tokenHelp: 'Microsoft App Secret from Azure Bot registration',
169
193
  configFields: [
170
194
  { key: 'appId', label: 'Microsoft App ID', placeholder: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx', help: 'Azure Bot Framework App ID' },
171
- { key: 'notifyUrl', label: 'Notify URL', placeholder: 'https://your-server.com/api/teams/webhook', help: 'Public HTTPS endpoint for receiving messages' },
195
+ { key: 'notifyUrl', label: 'Notify URL', placeholder: 'https://your-server.com/api/connectors/<id>/webhook', help: 'Public HTTPS endpoint for receiving messages (informational)' },
196
+ { key: 'webhookSecret', label: 'Webhook Secret', placeholder: 'optional-shared-secret', help: 'Optional secret required by /api/connectors/{id}/webhook (header: x-connector-secret or ?secret=...)' },
172
197
  ],
173
198
  },
174
199
  {
@@ -255,35 +280,29 @@ export function ConnectorSheet() {
255
280
 
256
281
  // Poll for QR code when WhatsApp connector is running or connecting
257
282
  const isWaRunning = editing?.platform === 'whatsapp' && (editing?.status === 'running' || waConnecting)
258
- useEffect(() => {
259
- if (!editing || !isWaRunning) {
260
- return
261
- }
262
- let cancelled = false
263
- const poll = async () => {
264
- try {
265
- const data = await api<any>('GET', `/connectors/${editing.id}`)
266
- if (!cancelled) {
267
- setQrDataUrl(data.qrDataUrl || null)
268
- setWaAuthenticated(data.authenticated ?? false)
269
- setWaHasCreds(data.hasCredentials ?? false)
270
- // Sync store with the individual endpoint's runtime status
271
- if (data.status === 'running' && editing.status !== 'running') {
272
- // Store is stale — update it directly
273
- const store = useAppStore.getState()
274
- const updated = { ...store.connectors }
275
- if (updated[editing.id]) {
276
- updated[editing.id] = { ...updated[editing.id], status: 'running' as const }
277
- useAppStore.setState({ connectors: updated })
278
- }
279
- }
283
+ const pollWaStatus = useCallback(async () => {
284
+ if (!editing) return
285
+ try {
286
+ const data = await api<any>('GET', `/connectors/${editing.id}`)
287
+ setQrDataUrl(data.qrDataUrl || null)
288
+ setWaAuthenticated(data.authenticated ?? false)
289
+ setWaHasCreds(data.hasCredentials ?? false)
290
+ if (data.status === 'running' && editing.status !== 'running') {
291
+ const store = useAppStore.getState()
292
+ const updated = { ...store.connectors }
293
+ if (updated[editing.id]) {
294
+ updated[editing.id] = { ...updated[editing.id], status: 'running' as const }
295
+ useAppStore.setState({ connectors: updated })
280
296
  }
281
- } catch { /* ignore */ }
282
- }
283
- poll()
284
- const interval = setInterval(poll, 2000)
285
- return () => { cancelled = true; clearInterval(interval) }
286
- }, [editing?.id, isWaRunning])
297
+ }
298
+ } catch { /* ignore */ }
299
+ }, [editing?.id, editing?.status])
300
+
301
+ useEffect(() => {
302
+ if (editing && isWaRunning) pollWaStatus()
303
+ }, [editing?.id, isWaRunning, pollWaStatus])
304
+
305
+ useWs('connectors', pollWaStatus, isWaRunning ? 2000 : undefined)
287
306
 
288
307
  const handleSave = async () => {
289
308
  if (!agentId) return
@@ -371,7 +390,7 @@ export function ConnectorSheet() {
371
390
  <div>
372
391
  <div className={`text-[14px] font-600 ${platform === p.id ? 'text-text' : 'text-text-2'}`}>{p.label}</div>
373
392
  <div className="text-[11px] text-text-3 mt-0.5">
374
- {p.id === 'whatsapp' ? 'QR code pairing' : p.id === 'openclaw' ? 'WebSocket gateway' : p.id === 'signal' ? 'signal-cli binary' : p.id === 'matrix' ? 'Access token' : p.id === 'googlechat' ? 'Service account' : p.id === 'teams' ? 'Bot Framework' : 'Bot token'}
393
+ {p.id === 'whatsapp' ? 'QR code pairing' : p.id === 'openclaw' ? 'WebSocket gateway' : p.id === 'bluebubbles' ? 'iMessage bridge' : p.id === 'signal' ? 'signal-cli binary' : p.id === 'matrix' ? 'Access token' : p.id === 'googlechat' ? 'Service account' : p.id === 'teams' ? 'Bot Framework' : 'Bot token'}
375
394
  </div>
376
395
  </div>
377
396
  </button>
@@ -557,7 +576,7 @@ export function ConnectorSheet() {
557
576
 
558
577
  {/* Platform-specific config */}
559
578
  {platformConfig.configFields.map((field) => {
560
- const isTagField = field.key === 'allowedJids' || field.key === 'channelIds' || field.key === 'chatIds'
579
+ const isTagField = field.key === 'allowedJids' || field.key === 'channelIds' || field.key === 'chatIds' || field.key === 'allowFrom'
561
580
  if (isTagField) {
562
581
  const tags = (config[field.key] || '').split(',').map((s) => s.trim()).filter(Boolean)
563
582
  return (
@@ -737,7 +756,7 @@ export function ConnectorSheet() {
737
756
  </div>
738
757
  <p className="text-[11px] text-text-3">
739
758
  {waHasCreds
740
- ? 'Reconnecting with saved session, this should only take a moment'
759
+ ? 'Reconnecting with saved credentials, this should only take a moment'
741
760
  : 'Connecting to WhatsApp, QR code will appear shortly'}
742
761
  </p>
743
762
  {waHasCreds && (