@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.
- package/README.md +33 -13
- package/bin/server-cmd.js +14 -7
- package/bin/swarmclaw.js +3 -1
- package/bin/update-cmd.js +120 -0
- package/next.config.ts +10 -0
- package/package.json +4 -1
- package/src/app/api/agents/[id]/route.ts +20 -18
- package/src/app/api/agents/[id]/thread/route.ts +4 -3
- package/src/app/api/agents/route.ts +8 -3
- package/src/app/api/auth/route.ts +3 -1
- package/src/app/api/claude-skills/route.ts +3 -1
- package/src/app/api/clawhub/install/route.ts +2 -2
- package/src/app/api/connectors/[id]/route.ts +14 -3
- package/src/app/api/connectors/[id]/webhook/route.ts +99 -0
- package/src/app/api/connectors/route.ts +12 -4
- package/src/app/api/credentials/[id]/route.ts +2 -1
- package/src/app/api/credentials/route.ts +5 -3
- package/src/app/api/daemon/route.ts +6 -1
- package/src/app/api/documents/route.ts +2 -2
- package/src/app/api/files/serve/route.ts +8 -0
- package/src/app/api/ip/route.ts +3 -1
- package/src/app/api/knowledge/[id]/route.ts +5 -4
- package/src/app/api/knowledge/upload/route.ts +2 -2
- package/src/app/api/mcp-servers/[id]/route.ts +11 -14
- package/src/app/api/mcp-servers/[id]/test/route.ts +2 -1
- package/src/app/api/mcp-servers/[id]/tools/route.ts +2 -1
- package/src/app/api/mcp-servers/route.ts +5 -3
- package/src/app/api/memory/[id]/route.ts +9 -8
- package/src/app/api/memory/route.ts +2 -2
- package/src/app/api/memory-images/[filename]/route.ts +2 -1
- package/src/app/api/openclaw/directory/route.ts +26 -0
- package/src/app/api/openclaw/discover/route.ts +61 -0
- package/src/app/api/openclaw/sync/route.ts +30 -0
- package/src/app/api/orchestrator/graph/route.ts +25 -0
- package/src/app/api/orchestrator/run/route.ts +2 -2
- package/src/app/api/plugins/marketplace/route.ts +3 -1
- package/src/app/api/plugins/route.ts +3 -1
- package/src/app/api/projects/[id]/route.ts +55 -0
- package/src/app/api/projects/route.ts +27 -0
- package/src/app/api/providers/[id]/models/route.ts +2 -1
- package/src/app/api/providers/[id]/route.ts +13 -12
- package/src/app/api/providers/configs/route.ts +3 -1
- package/src/app/api/providers/route.ts +7 -3
- package/src/app/api/schedules/[id]/route.ts +16 -15
- package/src/app/api/schedules/[id]/run/route.ts +4 -3
- package/src/app/api/schedules/route.ts +8 -3
- package/src/app/api/secrets/[id]/route.ts +16 -17
- package/src/app/api/secrets/route.ts +5 -3
- package/src/app/api/sessions/[id]/chat/route.ts +5 -2
- package/src/app/api/sessions/[id]/clear/route.ts +2 -1
- package/src/app/api/sessions/[id]/deploy/route.ts +2 -1
- package/src/app/api/sessions/[id]/devserver/route.ts +2 -1
- package/src/app/api/sessions/[id]/messages/route.ts +2 -1
- package/src/app/api/sessions/[id]/retry/route.ts +2 -1
- package/src/app/api/sessions/[id]/route.ts +2 -1
- package/src/app/api/sessions/route.ts +11 -4
- package/src/app/api/settings/route.ts +3 -1
- package/src/app/api/setup/doctor/route.ts +1 -0
- package/src/app/api/setup/openclaw-device/route.ts +3 -1
- package/src/app/api/skills/[id]/route.ts +23 -21
- package/src/app/api/skills/import/route.ts +2 -2
- package/src/app/api/skills/route.ts +5 -3
- package/src/app/api/tasks/[id]/approve/route.ts +74 -0
- package/src/app/api/tasks/[id]/route.ts +9 -5
- package/src/app/api/tasks/route.ts +5 -2
- package/src/app/api/tts/stream/route.ts +48 -0
- package/src/app/api/upload/route.ts +2 -2
- package/src/app/api/uploads/[filename]/route.ts +4 -1
- package/src/app/api/usage/route.ts +3 -1
- package/src/app/api/version/route.ts +3 -1
- package/src/app/api/webhooks/[id]/route.ts +31 -32
- package/src/app/api/webhooks/route.ts +5 -3
- package/src/app/icon.svg +58 -0
- package/src/app/page.tsx +11 -26
- package/src/cli/index.js +28 -9
- package/src/cli/index.ts +45 -2
- package/src/cli/spec.js +2 -8
- package/src/components/agents/agent-card.tsx +1 -1
- package/src/components/agents/agent-list.tsx +3 -1
- package/src/components/agents/agent-sheet.tsx +166 -81
- package/src/components/chat/chat-area.tsx +71 -34
- package/src/components/chat/chat-header.tsx +141 -29
- package/src/components/chat/chat-tool-toggles.tsx +12 -53
- package/src/components/chat/message-bubble.tsx +110 -42
- package/src/components/chat/tool-call-bubble.tsx +50 -6
- package/src/components/chat/tool-request-banner.tsx +1 -9
- package/src/components/chat/voice-overlay.tsx +80 -0
- package/src/components/connectors/connector-list.tsx +9 -10
- package/src/components/connectors/connector-sheet.tsx +55 -36
- package/src/components/input/chat-input.tsx +72 -56
- package/src/components/knowledge/knowledge-list.tsx +27 -31
- package/src/components/layout/app-layout.tsx +133 -90
- package/src/components/layout/daemon-indicator.tsx +3 -5
- package/src/components/logs/log-list.tsx +5 -9
- package/src/components/mcp-servers/mcp-server-list.tsx +24 -2
- package/src/components/memory/memory-detail.tsx +1 -1
- package/src/components/plugins/plugin-list.tsx +227 -27
- package/src/components/projects/project-list.tsx +122 -0
- package/src/components/projects/project-sheet.tsx +135 -0
- package/src/components/providers/provider-list.tsx +46 -13
- package/src/components/providers/provider-sheet.tsx +0 -45
- package/src/components/runs/run-list.tsx +6 -15
- package/src/components/schedules/schedule-card.tsx +54 -4
- package/src/components/schedules/schedule-list.tsx +9 -4
- package/src/components/schedules/schedule-sheet.tsx +0 -47
- package/src/components/secrets/secrets-list.tsx +20 -2
- package/src/components/sessions/new-session-sheet.tsx +14 -15
- package/src/components/sessions/session-card.tsx +1 -1
- package/src/components/sessions/session-list.tsx +7 -7
- package/src/components/shared/connector-platform-icon.tsx +26 -20
- package/src/components/shared/model-combobox.tsx +148 -0
- package/src/components/shared/settings/section-heartbeat.tsx +8 -40
- package/src/components/shared/settings/section-orchestrator.tsx +9 -11
- package/src/components/shared/settings/section-web-search.tsx +56 -0
- package/src/components/shared/settings/settings-page.tsx +73 -0
- package/src/components/skills/skill-list.tsx +262 -35
- package/src/components/skills/skill-sheet.tsx +0 -45
- package/src/components/tasks/task-board.tsx +3 -6
- package/src/components/tasks/task-card.tsx +43 -1
- package/src/components/tasks/task-list.tsx +8 -7
- package/src/components/tasks/task-sheet.tsx +0 -44
- package/src/components/usage/usage-list.tsx +12 -4
- package/src/hooks/use-continuous-speech.ts +144 -0
- package/src/hooks/use-view-router.ts +52 -0
- package/src/hooks/use-voice-conversation.ts +80 -0
- package/src/hooks/use-ws.ts +66 -0
- package/src/instrumentation.ts +2 -0
- package/src/lib/chat.ts +14 -2
- package/src/lib/id.ts +6 -0
- package/src/lib/projects.ts +13 -0
- package/src/lib/provider-sets.ts +5 -0
- package/src/lib/providers/anthropic.ts +15 -2
- package/src/lib/providers/index.ts +8 -0
- package/src/lib/providers/ollama.ts +10 -2
- package/src/lib/providers/openai.ts +42 -13
- package/src/lib/providers/openclaw.ts +11 -0
- package/src/lib/server/api-routes.test.ts +5 -6
- package/src/lib/server/build-llm.ts +17 -4
- package/src/lib/server/chat-execution.ts +57 -8
- package/src/lib/server/collection-helpers.ts +54 -0
- package/src/lib/server/connectors/bluebubbles.test.ts +208 -0
- package/src/lib/server/connectors/bluebubbles.ts +357 -0
- package/src/lib/server/connectors/connector-routing.test.ts +1 -1
- package/src/lib/server/connectors/googlechat.ts +46 -7
- package/src/lib/server/connectors/manager.ts +401 -6
- package/src/lib/server/connectors/media.ts +2 -2
- package/src/lib/server/connectors/openclaw.ts +64 -0
- package/src/lib/server/connectors/pairing.test.ts +99 -0
- package/src/lib/server/connectors/pairing.ts +256 -0
- package/src/lib/server/connectors/signal.ts +1 -0
- package/src/lib/server/connectors/teams.ts +5 -5
- package/src/lib/server/connectors/types.ts +10 -0
- package/src/lib/server/context-manager.ts +1 -1
- package/src/lib/server/daemon-state.ts +3 -0
- package/src/lib/server/data-dir.ts +1 -0
- package/src/lib/server/execution-log.ts +3 -3
- package/src/lib/server/heartbeat-service.ts +67 -3
- package/src/lib/server/knowledge-db.test.ts +2 -33
- package/src/lib/server/langgraph-checkpoint.ts +274 -0
- package/src/lib/server/main-agent-loop.ts +67 -8
- package/src/lib/server/memory-db.ts +6 -6
- package/src/lib/server/openclaw-approvals.ts +105 -0
- package/src/lib/server/openclaw-sync.ts +496 -0
- package/src/lib/server/orchestrator-lg.ts +422 -20
- package/src/lib/server/orchestrator.ts +29 -9
- package/src/lib/server/process-manager.ts +2 -2
- package/src/lib/server/queue.ts +39 -13
- package/src/lib/server/scheduler.ts +2 -2
- package/src/lib/server/session-mailbox.ts +2 -2
- package/src/lib/server/session-run-manager.ts +8 -3
- package/src/lib/server/session-tools/connector.ts +51 -4
- package/src/lib/server/session-tools/crud.ts +3 -3
- package/src/lib/server/session-tools/delegate.ts +5 -5
- package/src/lib/server/session-tools/file.ts +176 -3
- package/src/lib/server/session-tools/index.ts +4 -0
- package/src/lib/server/session-tools/memory.ts +2 -2
- package/src/lib/server/session-tools/openclaw-nodes.ts +112 -0
- package/src/lib/server/session-tools/sandbox.ts +197 -0
- package/src/lib/server/session-tools/search-providers.ts +270 -0
- package/src/lib/server/session-tools/session-info.ts +2 -2
- package/src/lib/server/session-tools/web.ts +47 -66
- package/src/lib/server/storage-mcp.test.ts +25 -2
- package/src/lib/server/storage.ts +36 -7
- package/src/lib/server/stream-agent-chat.ts +106 -22
- package/src/lib/server/task-result.test.ts +44 -0
- package/src/lib/server/task-result.ts +14 -0
- package/src/lib/server/task-validation.test.ts +23 -0
- package/src/lib/server/task-validation.ts +5 -3
- package/src/lib/server/ws-hub.ts +85 -0
- package/src/lib/tool-definitions.ts +44 -0
- package/src/lib/tts-stream.ts +130 -0
- package/src/lib/upload.ts +7 -1
- package/src/lib/view-routes.ts +28 -0
- package/src/lib/ws-client.ts +124 -0
- package/src/proxy.ts +3 -0
- package/src/stores/use-app-store.ts +28 -1
- package/src/stores/use-chat-store.ts +42 -14
- package/src/types/index.ts +34 -2
- package/src/app/api/agents/generate/route.ts +0 -42
- package/src/app/api/generate/info/route.ts +0 -12
- package/src/app/api/generate/route.ts +0 -106
- package/src/app/favicon.ico +0 -0
- 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
|
|
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: '
|
|
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
|
|
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
|
|
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
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
|
|
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
|
|
113
|
-
const hasCredentials = c.platform === 'whatsapp'
|
|
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: '
|
|
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
|
|
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
|
-
'
|
|
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/
|
|
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
|
-
|
|
259
|
-
if (!editing
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
-
}
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
|
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 && (
|