@swarmclawai/swarmclaw 0.4.0 → 0.5.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 (209) hide show
  1. package/README.md +21 -4
  2. package/bin/server-cmd.js +28 -19
  3. package/next.config.ts +13 -0
  4. package/package.json +3 -1
  5. package/src/app/api/agents/[id]/route.ts +39 -22
  6. package/src/app/api/agents/[id]/thread/route.ts +2 -2
  7. package/src/app/api/agents/route.ts +3 -2
  8. package/src/app/api/agents/trash/route.ts +44 -0
  9. package/src/app/api/clawhub/install/route.ts +2 -2
  10. package/src/app/api/connectors/[id]/route.ts +17 -7
  11. package/src/app/api/connectors/[id]/webhook/route.ts +103 -0
  12. package/src/app/api/connectors/route.ts +6 -3
  13. package/src/app/api/credentials/[id]/route.ts +2 -1
  14. package/src/app/api/credentials/route.ts +2 -2
  15. package/src/app/api/documents/route.ts +2 -2
  16. package/src/app/api/files/serve/route.ts +8 -0
  17. package/src/app/api/knowledge/[id]/route.ts +5 -4
  18. package/src/app/api/knowledge/upload/route.ts +2 -2
  19. package/src/app/api/mcp-servers/[id]/route.ts +11 -14
  20. package/src/app/api/mcp-servers/[id]/test/route.ts +2 -1
  21. package/src/app/api/mcp-servers/[id]/tools/route.ts +2 -1
  22. package/src/app/api/mcp-servers/route.ts +2 -2
  23. package/src/app/api/memory/[id]/route.ts +9 -8
  24. package/src/app/api/memory/route.ts +2 -2
  25. package/src/app/api/memory-images/[filename]/route.ts +2 -1
  26. package/src/app/api/openclaw/agent-files/route.ts +57 -0
  27. package/src/app/api/openclaw/approvals/route.ts +46 -0
  28. package/src/app/api/openclaw/config-sync/route.ts +33 -0
  29. package/src/app/api/openclaw/cron/route.ts +52 -0
  30. package/src/app/api/openclaw/directory/route.ts +27 -0
  31. package/src/app/api/openclaw/discover/route.ts +62 -0
  32. package/src/app/api/openclaw/dotenv-keys/route.ts +18 -0
  33. package/src/app/api/openclaw/exec-config/route.ts +41 -0
  34. package/src/app/api/openclaw/gateway/route.ts +72 -0
  35. package/src/app/api/openclaw/history/route.ts +109 -0
  36. package/src/app/api/openclaw/media/route.ts +53 -0
  37. package/src/app/api/openclaw/models/route.ts +12 -0
  38. package/src/app/api/openclaw/permissions/route.ts +39 -0
  39. package/src/app/api/openclaw/sandbox-env/route.ts +69 -0
  40. package/src/app/api/openclaw/skills/install/route.ts +32 -0
  41. package/src/app/api/openclaw/skills/remove/route.ts +24 -0
  42. package/src/app/api/openclaw/skills/route.ts +82 -0
  43. package/src/app/api/openclaw/sync/route.ts +31 -0
  44. package/src/app/api/orchestrator/run/route.ts +2 -2
  45. package/src/app/api/projects/[id]/route.ts +55 -0
  46. package/src/app/api/projects/route.ts +27 -0
  47. package/src/app/api/providers/[id]/models/route.ts +2 -1
  48. package/src/app/api/providers/[id]/route.ts +13 -15
  49. package/src/app/api/providers/route.ts +2 -2
  50. package/src/app/api/schedules/[id]/route.ts +16 -18
  51. package/src/app/api/schedules/[id]/run/route.ts +4 -3
  52. package/src/app/api/schedules/route.ts +2 -2
  53. package/src/app/api/secrets/[id]/route.ts +16 -17
  54. package/src/app/api/secrets/route.ts +2 -2
  55. package/src/app/api/sessions/[id]/clear/route.ts +2 -1
  56. package/src/app/api/sessions/[id]/deploy/route.ts +2 -1
  57. package/src/app/api/sessions/[id]/devserver/route.ts +2 -1
  58. package/src/app/api/sessions/[id]/edit-resend/route.ts +22 -0
  59. package/src/app/api/sessions/[id]/fork/route.ts +44 -0
  60. package/src/app/api/sessions/[id]/messages/route.ts +20 -2
  61. package/src/app/api/sessions/[id]/retry/route.ts +2 -1
  62. package/src/app/api/sessions/[id]/route.ts +14 -4
  63. package/src/app/api/sessions/route.ts +8 -4
  64. package/src/app/api/skills/[id]/route.ts +23 -21
  65. package/src/app/api/skills/import/route.ts +2 -2
  66. package/src/app/api/skills/route.ts +2 -2
  67. package/src/app/api/tasks/[id]/approve/route.ts +2 -1
  68. package/src/app/api/tasks/[id]/route.ts +6 -5
  69. package/src/app/api/tasks/route.ts +2 -2
  70. package/src/app/api/tts/stream/route.ts +48 -0
  71. package/src/app/api/upload/route.ts +2 -2
  72. package/src/app/api/uploads/[filename]/route.ts +4 -1
  73. package/src/app/api/webhooks/[id]/route.ts +29 -31
  74. package/src/app/api/webhooks/route.ts +2 -2
  75. package/src/app/globals.css +14 -0
  76. package/src/app/layout.tsx +5 -20
  77. package/src/app/page.tsx +3 -24
  78. package/src/cli/index.js +60 -0
  79. package/src/cli/index.ts +1 -1
  80. package/src/cli/spec.js +42 -0
  81. package/src/components/agents/agent-avatar.tsx +45 -0
  82. package/src/components/agents/agent-card.tsx +19 -5
  83. package/src/components/agents/agent-chat-list.tsx +31 -24
  84. package/src/components/agents/agent-files-editor.tsx +185 -0
  85. package/src/components/agents/agent-list.tsx +84 -3
  86. package/src/components/agents/agent-sheet.tsx +147 -14
  87. package/src/components/agents/cron-job-form.tsx +137 -0
  88. package/src/components/agents/exec-config-panel.tsx +147 -0
  89. package/src/components/agents/inspector-panel.tsx +310 -0
  90. package/src/components/agents/openclaw-skills-panel.tsx +230 -0
  91. package/src/components/agents/permission-preset-selector.tsx +79 -0
  92. package/src/components/agents/personality-builder.tsx +111 -0
  93. package/src/components/agents/sandbox-env-panel.tsx +72 -0
  94. package/src/components/agents/skill-install-dialog.tsx +102 -0
  95. package/src/components/agents/trash-list.tsx +109 -0
  96. package/src/components/chat/chat-area.tsx +41 -6
  97. package/src/components/chat/chat-header.tsx +305 -29
  98. package/src/components/chat/chat-preview-panel.tsx +113 -0
  99. package/src/components/chat/exec-approval-card.tsx +89 -0
  100. package/src/components/chat/message-bubble.tsx +218 -36
  101. package/src/components/chat/message-list.tsx +135 -31
  102. package/src/components/chat/streaming-bubble.tsx +59 -10
  103. package/src/components/chat/suggestions-bar.tsx +74 -0
  104. package/src/components/chat/thinking-indicator.tsx +20 -6
  105. package/src/components/chat/tool-call-bubble.tsx +98 -19
  106. package/src/components/chat/tool-request-banner.tsx +20 -2
  107. package/src/components/chat/trace-block.tsx +103 -0
  108. package/src/components/chat/voice-overlay.tsx +80 -0
  109. package/src/components/connectors/connector-list.tsx +6 -2
  110. package/src/components/connectors/connector-sheet.tsx +31 -7
  111. package/src/components/layout/app-layout.tsx +47 -25
  112. package/src/components/projects/project-list.tsx +123 -0
  113. package/src/components/projects/project-sheet.tsx +135 -0
  114. package/src/components/schedules/schedule-list.tsx +3 -1
  115. package/src/components/sessions/new-session-sheet.tsx +6 -6
  116. package/src/components/sessions/session-card.tsx +1 -1
  117. package/src/components/sessions/session-list.tsx +7 -7
  118. package/src/components/settings/gateway-connection-panel.tsx +278 -0
  119. package/src/components/shared/avatar.tsx +13 -2
  120. package/src/components/shared/connector-platform-icon.tsx +4 -0
  121. package/src/components/shared/settings/section-heartbeat.tsx +1 -1
  122. package/src/components/shared/settings/section-orchestrator.tsx +1 -2
  123. package/src/components/shared/settings/section-web-search.tsx +56 -0
  124. package/src/components/shared/settings/settings-page.tsx +74 -0
  125. package/src/components/skills/skill-list.tsx +2 -1
  126. package/src/components/tasks/task-board.tsx +1 -1
  127. package/src/components/tasks/task-list.tsx +5 -2
  128. package/src/components/tasks/task-sheet.tsx +12 -12
  129. package/src/hooks/use-continuous-speech.ts +181 -0
  130. package/src/hooks/use-openclaw-gateway.ts +63 -0
  131. package/src/hooks/use-view-router.ts +52 -0
  132. package/src/hooks/use-voice-conversation.ts +80 -0
  133. package/src/lib/id.ts +6 -0
  134. package/src/lib/notification-sounds.ts +58 -0
  135. package/src/lib/personality-parser.ts +97 -0
  136. package/src/lib/projects.ts +13 -0
  137. package/src/lib/provider-sets.ts +5 -0
  138. package/src/lib/providers/anthropic.ts +14 -1
  139. package/src/lib/providers/index.ts +6 -0
  140. package/src/lib/providers/ollama.ts +9 -1
  141. package/src/lib/providers/openai.ts +9 -1
  142. package/src/lib/providers/openclaw.ts +28 -2
  143. package/src/lib/runtime-loop.ts +2 -2
  144. package/src/lib/server/api-routes.test.ts +5 -6
  145. package/src/lib/server/build-llm.ts +17 -4
  146. package/src/lib/server/chat-execution.ts +82 -6
  147. package/src/lib/server/collection-helpers.ts +54 -0
  148. package/src/lib/server/connectors/bluebubbles.test.ts +217 -0
  149. package/src/lib/server/connectors/bluebubbles.ts +360 -0
  150. package/src/lib/server/connectors/connector-routing.test.ts +1 -1
  151. package/src/lib/server/connectors/googlechat.ts +51 -8
  152. package/src/lib/server/connectors/manager.ts +424 -13
  153. package/src/lib/server/connectors/media.ts +2 -2
  154. package/src/lib/server/connectors/openclaw.ts +65 -0
  155. package/src/lib/server/connectors/pairing.test.ts +99 -0
  156. package/src/lib/server/connectors/pairing.ts +256 -0
  157. package/src/lib/server/connectors/signal.ts +1 -0
  158. package/src/lib/server/connectors/teams.ts +5 -5
  159. package/src/lib/server/connectors/types.ts +10 -0
  160. package/src/lib/server/daemon-state.ts +11 -0
  161. package/src/lib/server/execution-log.ts +3 -3
  162. package/src/lib/server/heartbeat-service.ts +1 -1
  163. package/src/lib/server/knowledge-db.test.ts +2 -33
  164. package/src/lib/server/main-agent-loop.ts +8 -9
  165. package/src/lib/server/main-session.ts +21 -0
  166. package/src/lib/server/memory-db.ts +6 -6
  167. package/src/lib/server/openclaw-approvals.ts +105 -0
  168. package/src/lib/server/openclaw-config-sync.ts +107 -0
  169. package/src/lib/server/openclaw-exec-config.ts +52 -0
  170. package/src/lib/server/openclaw-gateway.ts +291 -0
  171. package/src/lib/server/openclaw-history-merge.ts +36 -0
  172. package/src/lib/server/openclaw-models.ts +56 -0
  173. package/src/lib/server/openclaw-permission-presets.ts +64 -0
  174. package/src/lib/server/openclaw-sync.ts +497 -0
  175. package/src/lib/server/orchestrator-lg.ts +30 -9
  176. package/src/lib/server/orchestrator.ts +4 -4
  177. package/src/lib/server/process-manager.ts +2 -2
  178. package/src/lib/server/queue.ts +24 -11
  179. package/src/lib/server/scheduler.ts +2 -2
  180. package/src/lib/server/session-mailbox.ts +2 -2
  181. package/src/lib/server/session-run-manager.ts +2 -2
  182. package/src/lib/server/session-tools/connector.ts +53 -6
  183. package/src/lib/server/session-tools/crud.ts +3 -3
  184. package/src/lib/server/session-tools/delegate.ts +22 -6
  185. package/src/lib/server/session-tools/file.ts +192 -19
  186. package/src/lib/server/session-tools/index.ts +4 -2
  187. package/src/lib/server/session-tools/memory.ts +2 -2
  188. package/src/lib/server/session-tools/openclaw-nodes.ts +112 -0
  189. package/src/lib/server/session-tools/sandbox.ts +33 -0
  190. package/src/lib/server/session-tools/search-providers.ts +277 -0
  191. package/src/lib/server/session-tools/session-info.ts +2 -2
  192. package/src/lib/server/session-tools/session-tools-wiring.test.ts +2 -2
  193. package/src/lib/server/session-tools/shell.ts +1 -1
  194. package/src/lib/server/session-tools/web.ts +53 -72
  195. package/src/lib/server/storage.ts +74 -11
  196. package/src/lib/server/stream-agent-chat.ts +53 -4
  197. package/src/lib/server/suggestions.ts +20 -0
  198. package/src/lib/server/task-result.test.ts +44 -0
  199. package/src/lib/server/task-result.ts +14 -0
  200. package/src/lib/server/ws-hub.ts +14 -0
  201. package/src/lib/tool-definitions.ts +5 -3
  202. package/src/lib/tts-stream.ts +130 -0
  203. package/src/lib/view-routes.ts +28 -0
  204. package/src/proxy.ts +3 -0
  205. package/src/stores/use-app-store.ts +80 -1
  206. package/src/stores/use-approval-store.ts +78 -0
  207. package/src/stores/use-chat-store.ts +162 -6
  208. package/src/types/index.ts +154 -3
  209. package/tsconfig.json +13 -4
@@ -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 { useChatStore } from '@/stores/use-chat-store'
5
6
  import { useAppStore } from '@/stores/use-app-store'
6
7
 
7
8
  const TOOL_COLORS: Record<string, string> = {
@@ -14,6 +15,8 @@ const TOOL_COLORS: Record<string, string> = {
14
15
  delete_file: '#EF4444',
15
16
  edit_file: '#10B981',
16
17
  send_file: '#10B981',
18
+ create_document: '#10B981',
19
+ create_spreadsheet: '#10B981',
17
20
  web_search: '#3B82F6',
18
21
  web_fetch: '#3B82F6',
19
22
  delegate_to_agent: '#6366F1',
@@ -60,6 +63,8 @@ export const TOOL_LABELS: Record<string, string> = {
60
63
  delete_file: 'Delete File',
61
64
  edit_file: 'Edit File',
62
65
  send_file: 'Send File',
66
+ create_document: 'Create Document',
67
+ create_spreadsheet: 'Create Spreadsheet',
63
68
  web_search: 'Web Search',
64
69
  web_fetch: 'Web Fetch',
65
70
  claude_code: 'Claude Code',
@@ -79,7 +84,7 @@ export const TOOL_LABELS: Record<string, string> = {
79
84
  manage_documents: 'Documents',
80
85
  manage_webhooks: 'Webhooks',
81
86
  manage_connectors: 'Connectors',
82
- manage_sessions: 'Sessions',
87
+ manage_sessions: 'Chats',
83
88
  memory: 'Memory',
84
89
  browser: 'Browser',
85
90
  }
@@ -94,6 +99,8 @@ export const TOOL_DESCRIPTIONS: Record<string, string> = {
94
99
  delete_file: 'Delete files or directories (when explicitly enabled)',
95
100
  edit_file: 'Edit existing files with find-and-replace',
96
101
  send_file: 'Send files to the user (images, PDFs, videos, documents, etc.)',
102
+ create_document: 'Render markdown content into PDF, HTML, or image',
103
+ create_spreadsheet: 'Create Excel or CSV files from structured data',
97
104
  web_search: 'Search the web for information',
98
105
  web_fetch: 'Fetch and read web page content',
99
106
  claude_code: 'Enable delegation to Claude Code CLI',
@@ -103,7 +110,7 @@ export const TOOL_DESCRIPTIONS: Record<string, string> = {
103
110
  delegate_to_claude_code: 'Delegate complex coding tasks to Claude Code',
104
111
  delegate_to_codex_cli: 'Delegate complex coding tasks to Codex CLI',
105
112
  delegate_to_opencode_cli: 'Delegate complex coding tasks to OpenCode CLI',
106
- whoami_tool: 'Reveal the current session and agent identity context',
113
+ whoami_tool: 'Reveal the current agent and chat context',
107
114
  connector_message_tool: 'Send proactive outbound messages via running connectors',
108
115
  search_history_tool: 'Search chat history for relevant prior context',
109
116
  manage_tasks: 'Create, update, and manage tasks on the board',
@@ -113,7 +120,7 @@ export const TOOL_DESCRIPTIONS: Record<string, string> = {
113
120
  manage_documents: 'Upload and search indexed documents',
114
121
  manage_webhooks: 'Register and manage inbound webhooks',
115
122
  manage_connectors: 'Manage chat platform connectors (Slack, Discord, etc.)',
116
- manage_sessions: 'Create and manage chat sessions',
123
+ manage_sessions: 'Create and manage agent chats',
117
124
  memory: 'Store and recall information across conversations',
118
125
  browser: 'Browse the web, take screenshots, and interact with pages',
119
126
  }
@@ -245,6 +252,61 @@ function extractMedia(output: string): { images: string[]; videos: string[]; pdf
245
252
  return { images, videos, pdfs, files, cleanText }
246
253
  }
247
254
 
255
+ import type { AppSettings } from '@/types'
256
+
257
+ /** Settings keys that can be quick-fixed from error output */
258
+ const TIMEOUT_SETTINGS: Array<{ pattern: RegExp; settingKey: keyof AppSettings; label: string; increment: number }> = [
259
+ { pattern: /Claude Code CLI timed out/i, settingKey: 'claudeCodeTimeoutSec', label: 'Claude Code Timeout', increment: 600 },
260
+ { pattern: /Codex CLI timed out|OpenCode CLI timed out/i, settingKey: 'cliProcessTimeoutSec', label: 'CLI Process Timeout', increment: 600 },
261
+ { pattern: /command timed out|shell.*timed out/i, settingKey: 'shellCommandTimeoutSec', label: 'Shell Timeout', increment: 60 },
262
+ ]
263
+
264
+ /** Inline quick-fix button for timeout errors */
265
+ function TimeoutQuickFix({ event }: { event: ToolEvent }) {
266
+ const [applied, setApplied] = useState(false)
267
+ if (event.status !== 'error' || !event.output) return null
268
+
269
+ const match = TIMEOUT_SETTINGS.find((s) => s.pattern.test(event.output || ''))
270
+ if (!match) return null
271
+
272
+ const handleIncrease = async (e: React.MouseEvent) => {
273
+ e.stopPropagation()
274
+ const store = useAppStore.getState()
275
+ const current = (store.appSettings[match.settingKey] as number) || 0
276
+ const newValue = current + match.increment
277
+ await store.updateSettings({ [match.settingKey]: newValue })
278
+ setApplied(true)
279
+ }
280
+
281
+ if (applied) {
282
+ const store = useAppStore.getState()
283
+ const val = store.appSettings[match.settingKey] as number
284
+ return (
285
+ <div className="flex items-center gap-2 mt-2 text-[12px] text-emerald-400 font-500">
286
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"><polyline points="20 6 9 17 4 12" /></svg>
287
+ {match.label} increased to {val}s
288
+ </div>
289
+ )
290
+ }
291
+
292
+ return (
293
+ <button
294
+ type="button"
295
+ onClick={handleIncrease}
296
+ className="mt-2 flex items-center gap-1.5 px-3 py-1.5 rounded-[8px] text-[12px] font-600
297
+ bg-amber-500/10 border border-amber-500/20 text-amber-400 hover:bg-amber-500/20
298
+ cursor-pointer transition-all"
299
+ style={{ fontFamily: 'inherit' }}
300
+ >
301
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
302
+ <circle cx="12" cy="12" r="10" />
303
+ <polyline points="12 6 12 12 16 14" />
304
+ </svg>
305
+ Increase {match.label}
306
+ </button>
307
+ )
308
+ }
309
+
248
310
  export function ToolCallBubble({ event }: { event: ToolEvent }) {
249
311
  const [expanded, setExpanded] = useState(false)
250
312
  const [imgExpanded, setImgExpanded] = useState(false)
@@ -332,7 +394,7 @@ export function ToolCallBubble({ event }: { event: ToolEvent }) {
332
394
  role="link"
333
395
  tabIndex={0}
334
396
  onClick={handleAgentClick}
335
- onKeyDown={(e) => e.key === 'Enter' && handleAgentClick(e as any)}
397
+ onKeyDown={(e) => e.key === 'Enter' && handleAgentClick(e as unknown as React.MouseEvent)}
336
398
  className="text-accent-bright hover:underline cursor-pointer font-600"
337
399
  >
338
400
  {delegationInfo.agentName}
@@ -377,6 +439,7 @@ export function ToolCallBubble({ event }: { event: ToolEvent }) {
377
439
  {formattedCleanOutput}
378
440
  </pre>
379
441
  )}
442
+ <TimeoutQuickFix event={event} />
380
443
  </>
381
444
  )}
382
445
  </div>
@@ -391,23 +454,39 @@ export function ToolCallBubble({ event }: { event: ToolEvent }) {
391
454
  <img
392
455
  src={src}
393
456
  alt={`Screenshot ${i + 1}`}
457
+ loading="lazy"
394
458
  className={`rounded-[10px] border border-white/10 cursor-pointer transition-all duration-200 hover:border-white/25 ${imgExpanded ? 'max-w-full' : 'max-w-[400px]'}`}
395
459
  onClick={(e) => { e.stopPropagation(); setImgExpanded(!imgExpanded) }}
396
460
  onError={(e) => { (e.target as HTMLImageElement).style.display = 'none' }}
397
461
  />
398
- <a
399
- href={src}
400
- download
401
- onClick={(e) => e.stopPropagation()}
402
- className="absolute top-2 right-2 opacity-0 group-hover/img:opacity-100 transition-opacity bg-black/60 backdrop-blur-sm rounded-[8px] p-1.5 hover:bg-black/80"
403
- title="Download"
404
- >
405
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2" strokeLinecap="round">
406
- <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
407
- <polyline points="7 10 12 15 17 10" />
408
- <line x1="12" y1="15" x2="12" y2="3" />
409
- </svg>
410
- </a>
462
+ <div className="absolute top-2 right-2 flex gap-1 opacity-0 group-hover/img:opacity-100 transition-opacity">
463
+ <button
464
+ onClick={(e) => {
465
+ e.stopPropagation()
466
+ useChatStore.getState().setPreviewContent({ type: 'image', url: src, title: `${label} Screenshot` })
467
+ }}
468
+ className="bg-black/60 backdrop-blur-sm rounded-[8px] p-1.5 hover:bg-black/80 border-none cursor-pointer"
469
+ title="Open in side panel"
470
+ >
471
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2" strokeLinecap="round">
472
+ <rect x="3" y="3" width="18" height="18" rx="2" />
473
+ <line x1="12" y1="3" x2="12" y2="21" />
474
+ </svg>
475
+ </button>
476
+ <a
477
+ href={src}
478
+ download
479
+ onClick={(e) => e.stopPropagation()}
480
+ className="bg-black/60 backdrop-blur-sm rounded-[8px] p-1.5 hover:bg-black/80"
481
+ title="Download"
482
+ >
483
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2" strokeLinecap="round">
484
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
485
+ <polyline points="7 10 12 15 17 10" />
486
+ <line x1="12" y1="15" x2="12" y2="3" />
487
+ </svg>
488
+ </a>
489
+ </div>
411
490
  </div>
412
491
  ))}
413
492
  </div>
@@ -417,7 +496,7 @@ export function ToolCallBubble({ event }: { event: ToolEvent }) {
417
496
  {media.videos.length > 0 && (
418
497
  <div className="mt-2 flex flex-col gap-2">
419
498
  {media.videos.map((src, i) => (
420
- <video key={i} src={src} controls playsInline className="max-w-full rounded-[10px] border border-white/10" />
499
+ <video key={i} src={src} controls playsInline preload="none" className="max-w-full rounded-[10px] border border-white/10" />
421
500
  ))}
422
501
  </div>
423
502
  )}
@@ -427,7 +506,7 @@ export function ToolCallBubble({ event }: { event: ToolEvent }) {
427
506
  <div className="mt-2 flex flex-col gap-2">
428
507
  {media.pdfs.map((file, i) => (
429
508
  <div key={i} className="rounded-[10px] border border-white/10 overflow-hidden">
430
- <iframe src={file.url} className="w-full h-[400px] bg-white" title={file.name} />
509
+ <iframe src={file.url} loading="lazy" className="w-full h-[400px] bg-white" title={file.name} />
431
510
  <a
432
511
  href={file.url}
433
512
  download
@@ -1,7 +1,8 @@
1
1
  'use client'
2
2
 
3
- import { useState } from 'react'
3
+ import { useState, useRef } from 'react'
4
4
  import { useAppStore } from '@/stores/use-app-store'
5
+ import { useChatStore } from '@/stores/use-chat-store'
5
6
  import { api } from '@/lib/api-client'
6
7
  import { TOOL_LABELS } from '@/lib/tool-definitions'
7
8
 
@@ -15,6 +16,7 @@ export function ToolRequestBanner({ text, toolOutputs = [] }: Props) {
15
16
  const currentSessionId = useAppStore((s) => s.currentSessionId)
16
17
  const sessions = useAppStore((s) => s.sessions)
17
18
  const [granted, setGranted] = useState<Set<string>>(new Set())
19
+ const continueSentRef = useRef(false)
18
20
 
19
21
  const toolRequests: { toolId: string; reason: string }[] = []
20
22
  const seen = new Set<string>()
@@ -53,7 +55,23 @@ export function ToolRequestBanner({ text, toolOutputs = [] }: Props) {
53
55
  const updated = [...currentTools, toolId]
54
56
  await api('PUT', `/sessions/${sid}`, { tools: updated })
55
57
  await loadSessions()
56
- setGranted((prev) => new Set(prev).add(toolId))
58
+ const newGranted = new Set(granted).add(toolId)
59
+ setGranted(newGranted)
60
+
61
+ // Auto-continue: once all requested tools are granted, send a follow-up message
62
+ const allGranted = toolRequests.every(
63
+ (r) => newGranted.has(r.toolId) || updated.includes(r.toolId),
64
+ )
65
+ if (allGranted && !continueSentRef.current) {
66
+ continueSentRef.current = true
67
+ // Small delay to let the session update propagate
68
+ setTimeout(() => {
69
+ const { streaming, sendMessage } = useChatStore.getState()
70
+ if (!streaming) {
71
+ sendMessage('Continue')
72
+ }
73
+ }, 300)
74
+ }
57
75
  }
58
76
 
59
77
  return (
@@ -0,0 +1,103 @@
1
+ 'use client'
2
+
3
+ import { useState } from 'react'
4
+ import type { ChatTraceBlock } from '@/types'
5
+
6
+ interface Props {
7
+ trace: ChatTraceBlock
8
+ }
9
+
10
+ export function TraceBlock({ trace }: Props) {
11
+ const [collapsed, setCollapsed] = useState(trace.collapsed !== false)
12
+
13
+ const bgColor = trace.type === 'thinking'
14
+ ? 'bg-purple-500/[0.04] border-purple-500/10'
15
+ : trace.type === 'tool-call'
16
+ ? 'bg-sky-500/[0.04] border-sky-500/10'
17
+ : 'bg-emerald-500/[0.04] border-emerald-500/10'
18
+
19
+ const labelColor = trace.type === 'thinking'
20
+ ? 'text-purple-400/70'
21
+ : trace.type === 'tool-call'
22
+ ? 'text-sky-400/70'
23
+ : 'text-emerald-400/70'
24
+
25
+ const icon = trace.type === 'thinking'
26
+ ? '...'
27
+ : trace.type === 'tool-call'
28
+ ? '>'
29
+ : '<'
30
+
31
+ return (
32
+ <div className={`my-1 rounded-[8px] border ${bgColor} overflow-hidden`}>
33
+ <button
34
+ onClick={() => setCollapsed(!collapsed)}
35
+ className={`w-full flex items-center gap-2 px-3 py-1.5 text-left cursor-pointer border-none bg-transparent transition-colors hover:bg-white/[0.02] ${labelColor}`}
36
+ style={{ fontFamily: 'inherit' }}
37
+ >
38
+ <span className="font-mono text-[10px] w-4 shrink-0">{collapsed ? '+' : '-'}</span>
39
+ <span className="font-mono text-[10px] shrink-0">{icon}</span>
40
+ <span className="text-[11px] font-600 truncate">
41
+ {trace.label || trace.type.replace('-', ' ')}
42
+ </span>
43
+ </button>
44
+ {!collapsed && (
45
+ <div className="px-3 pb-2">
46
+ <pre className={`text-[11px] leading-relaxed whitespace-pre-wrap break-words m-0 ${
47
+ trace.type === 'thinking'
48
+ ? 'text-text-3/60 italic'
49
+ : 'text-text-3/70 font-mono'
50
+ }`}>
51
+ {trace.content.length > 2000
52
+ ? trace.content.slice(0, 2000) + '\n... (truncated)'
53
+ : trace.content}
54
+ </pre>
55
+ </div>
56
+ )}
57
+ </div>
58
+ )
59
+ }
60
+
61
+ /** Parse message text with [[prefix]] markers into text and trace blocks */
62
+ export function parseTraceBlocks(text: string): Array<{ type: 'text'; content: string } | ChatTraceBlock> {
63
+ const blocks: Array<{ type: 'text'; content: string } | ChatTraceBlock> = []
64
+ const regex = /\[\[(thinking|tool|tool-result|trace|meta)\]\]([\s\S]*?)(?=\[\[(thinking|tool|tool-result|trace|meta)\]\]|$)/g
65
+
66
+ let lastEnd = 0
67
+ let match: RegExpExecArray | null
68
+
69
+ while ((match = regex.exec(text)) !== null) {
70
+ // Add any text before this match
71
+ if (match.index > lastEnd) {
72
+ const before = text.slice(lastEnd, match.index).trim()
73
+ if (before) blocks.push({ type: 'text', content: before })
74
+ }
75
+
76
+ const prefix = match[1]
77
+ const content = match[2].trim()
78
+ if (content) {
79
+ if (prefix === 'thinking' || prefix === 'trace') {
80
+ blocks.push({ type: 'thinking', content, collapsed: true })
81
+ } else if (prefix === 'tool') {
82
+ const firstLine = content.split('\n')[0] || ''
83
+ blocks.push({ type: 'tool-call', content, label: firstLine.slice(0, 60), collapsed: true })
84
+ } else if (prefix === 'tool-result') {
85
+ blocks.push({ type: 'tool-result', content, collapsed: true })
86
+ }
87
+ // meta is ignored
88
+ }
89
+
90
+ lastEnd = match.index + match[0].length
91
+ }
92
+
93
+ // Add remaining text
94
+ if (lastEnd === 0) {
95
+ // No trace markers found
96
+ if (text.trim()) blocks.push({ type: 'text', content: text })
97
+ } else if (lastEnd < text.length) {
98
+ const remaining = text.slice(lastEnd).trim()
99
+ if (remaining) blocks.push({ type: 'text', content: remaining })
100
+ }
101
+
102
+ return blocks
103
+ }
@@ -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
+ }
@@ -104,8 +104,12 @@ export function ConnectorList({ inSidebar: _inSidebar }: { inSidebar?: boolean }
104
104
  const agent = agents[c.agentId]
105
105
  const isRunning = c.status === 'running'
106
106
  const isToggling = toggling === c.id
107
- // Can only toggle if connector has credentials (or is WhatsApp which uses QR)
108
- 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
109
113
  return (
110
114
  <div
111
115
  key={c.id}
@@ -114,7 +114,7 @@ const PLATFORMS: {
114
114
  tokenHelp: 'Required when your OpenClaw gateway is auth-protected',
115
115
  configFields: [
116
116
  { key: 'wsUrl', label: 'WebSocket URL', placeholder: 'ws://localhost:18789', help: 'OpenClaw gateway WebSocket endpoint (root URL, not /ws)' },
117
- { 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.' },
118
118
  { key: 'nodeId', label: 'Client Label', placeholder: 'swarmclaw', help: 'Optional display label shown in OpenClaw presence metadata.' },
119
119
  { key: 'role', label: 'Gateway Role', placeholder: 'operator', help: 'Optional role claim for connect handshake. Default is operator.' },
120
120
  { key: 'scopes', label: 'Scopes (CSV)', placeholder: 'operator.read,operator.write', help: 'Optional comma-separated scopes for OpenClaw connect.' },
@@ -122,6 +122,28 @@ const PLATFORMS: {
122
122
  { key: 'tickIntervalMs', label: 'Tick Interval Override (ms)', placeholder: '30000', help: 'Optional watchdog interval override when policy tick is unavailable.' },
123
123
  ],
124
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
+ },
125
147
  {
126
148
  id: 'matrix',
127
149
  label: 'Matrix',
@@ -146,13 +168,14 @@ const PLATFORMS: {
146
168
  setupSteps: [
147
169
  'Create a Google Cloud project and enable the Google Chat API',
148
170
  'Create a service account and download the JSON key file',
149
- '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',
150
172
  'Paste the full service account JSON as the bot token',
151
173
  ],
152
174
  tokenLabel: 'Service Account JSON',
153
175
  tokenHelp: 'Paste the full service account JSON key file contents',
154
176
  configFields: [
155
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=...)' },
156
179
  ],
157
180
  },
158
181
  {
@@ -163,13 +186,14 @@ const PLATFORMS: {
163
186
  'Register a bot in the Azure Bot Framework portal',
164
187
  'Note the Microsoft App ID and generate an App Secret',
165
188
  'Set up a public HTTPS endpoint for webhook delivery',
166
- 'Configure the messaging endpoint in Azure to your notify URL',
189
+ 'After saving the connector, point Azure to /api/connectors/<connector-id>/webhook',
167
190
  ],
168
191
  tokenLabel: 'App Secret',
169
192
  tokenHelp: 'Microsoft App Secret from Azure Bot registration',
170
193
  configFields: [
171
194
  { key: 'appId', label: 'Microsoft App ID', placeholder: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx', help: 'Azure Bot Framework App ID' },
172
- { 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=...)' },
173
197
  ],
174
198
  },
175
199
  {
@@ -366,7 +390,7 @@ export function ConnectorSheet() {
366
390
  <div>
367
391
  <div className={`text-[14px] font-600 ${platform === p.id ? 'text-text' : 'text-text-2'}`}>{p.label}</div>
368
392
  <div className="text-[11px] text-text-3 mt-0.5">
369
- {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'}
370
394
  </div>
371
395
  </div>
372
396
  </button>
@@ -552,7 +576,7 @@ export function ConnectorSheet() {
552
576
 
553
577
  {/* Platform-specific config */}
554
578
  {platformConfig.configFields.map((field) => {
555
- 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'
556
580
  if (isTagField) {
557
581
  const tags = (config[field.key] || '').split(',').map((s) => s.trim()).filter(Boolean)
558
582
  return (
@@ -732,7 +756,7 @@ export function ConnectorSheet() {
732
756
  </div>
733
757
  <p className="text-[11px] text-text-3">
734
758
  {waHasCreds
735
- ? 'Reconnecting with saved session, this should only take a moment'
759
+ ? 'Reconnecting with saved credentials, this should only take a moment'
736
760
  : 'Connecting to WhatsApp, QR code will appear shortly'}
737
761
  </p>
738
762
  {waHasCreds && (