@swarmclawai/swarmclaw 0.6.0 → 0.6.2
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 +15 -2
- package/bin/server-cmd.js +1 -0
- package/package.json +2 -1
- package/src/app/api/canvas/[sessionId]/route.ts +31 -0
- package/src/app/api/chatrooms/[id]/chat/route.ts +10 -136
- package/src/app/api/connectors/[id]/route.ts +1 -0
- package/src/app/api/connectors/route.ts +2 -1
- package/src/app/api/files/open/route.ts +43 -0
- package/src/app/api/search/route.ts +9 -7
- package/src/app/api/sessions/[id]/messages/route.ts +70 -2
- package/src/app/api/sessions/[id]/route.ts +4 -0
- package/src/app/api/tasks/metrics/route.ts +101 -0
- package/src/app/api/tasks/route.ts +17 -2
- package/src/app/api/tts/route.ts +3 -2
- package/src/app/api/tts/stream/route.ts +3 -2
- package/src/app/api/uploads/[filename]/route.ts +19 -34
- package/src/app/api/uploads/route.ts +94 -0
- package/src/app/globals.css +5 -0
- package/src/cli/index.js +16 -1
- package/src/cli/spec.js +26 -0
- package/src/components/agents/agent-card.tsx +3 -3
- package/src/components/agents/agent-chat-list.tsx +29 -6
- package/src/components/agents/agent-sheet.tsx +66 -4
- package/src/components/agents/inspector-panel.tsx +81 -6
- package/src/components/agents/openclaw-skills-panel.tsx +32 -3
- package/src/components/agents/personality-builder.tsx +42 -14
- package/src/components/agents/soul-library-picker.tsx +89 -0
- package/src/components/canvas/canvas-panel.tsx +96 -0
- package/src/components/chat/activity-moment.tsx +8 -4
- package/src/components/chat/chat-area.tsx +46 -22
- package/src/components/chat/chat-header.tsx +455 -286
- package/src/components/chat/chat-preview-panel.tsx +1 -2
- package/src/components/chat/delegation-banner.tsx +371 -0
- package/src/components/chat/file-path-chip.tsx +23 -2
- package/src/components/chat/heartbeat-history-panel.tsx +269 -0
- package/src/components/chat/message-bubble.tsx +315 -25
- package/src/components/chat/message-list.tsx +180 -7
- package/src/components/chat/streaming-bubble.tsx +68 -1
- package/src/components/chat/tool-call-bubble.tsx +45 -3
- package/src/components/chat/transfer-agent-picker.tsx +1 -1
- package/src/components/chatrooms/chatroom-list.tsx +8 -1
- package/src/components/chatrooms/chatroom-message.tsx +8 -3
- package/src/components/chatrooms/chatroom-view.tsx +3 -3
- package/src/components/connectors/connector-list.tsx +168 -90
- package/src/components/connectors/connector-sheet.tsx +68 -16
- package/src/components/home/home-view.tsx +1 -1
- package/src/components/input/chat-input.tsx +28 -2
- package/src/components/layout/app-layout.tsx +19 -2
- package/src/components/projects/project-detail.tsx +1 -1
- package/src/components/schedules/schedule-sheet.tsx +260 -127
- package/src/components/settings/gateway-disconnect-overlay.tsx +80 -0
- package/src/components/shared/agent-switch-dialog.tsx +1 -1
- package/src/components/shared/chatroom-picker-list.tsx +61 -0
- package/src/components/shared/connector-platform-icon.tsx +51 -4
- package/src/components/shared/icon-button.tsx +16 -2
- package/src/components/shared/keyboard-shortcuts-dialog.tsx +1 -1
- package/src/components/shared/search-dialog.tsx +17 -10
- package/src/components/shared/settings/section-embedding.tsx +48 -13
- package/src/components/shared/settings/section-orchestrator.tsx +46 -15
- package/src/components/shared/settings/section-storage.tsx +206 -0
- package/src/components/shared/settings/section-user-preferences.tsx +18 -0
- package/src/components/shared/settings/section-voice.tsx +42 -21
- package/src/components/shared/settings/section-web-search.tsx +30 -6
- package/src/components/shared/settings/settings-page.tsx +3 -1
- package/src/components/shared/settings/storage-browser.tsx +259 -0
- package/src/components/tasks/task-card.tsx +14 -1
- package/src/components/tasks/task-sheet.tsx +328 -3
- package/src/components/usage/metrics-dashboard.tsx +90 -6
- package/src/hooks/use-continuous-speech.ts +10 -4
- package/src/hooks/use-voice-conversation.ts +53 -10
- package/src/hooks/use-ws.ts +4 -2
- package/src/lib/providers/anthropic.ts +13 -7
- package/src/lib/providers/index.ts +1 -0
- package/src/lib/providers/openai.ts +13 -7
- package/src/lib/server/chat-execution.ts +51 -11
- package/src/lib/server/chatroom-helpers.ts +146 -0
- package/src/lib/server/connectors/manager.ts +218 -7
- package/src/lib/server/heartbeat-service.ts +8 -1
- package/src/lib/server/main-agent-loop.ts +1 -1
- package/src/lib/server/memory-consolidation.ts +15 -2
- package/src/lib/server/memory-db.ts +134 -6
- package/src/lib/server/mime.ts +51 -0
- package/src/lib/server/openclaw-gateway.ts +2 -2
- package/src/lib/server/orchestrator-lg.ts +2 -0
- package/src/lib/server/orchestrator.ts +5 -2
- package/src/lib/server/playwright-proxy.mjs +2 -3
- package/src/lib/server/prompt-runtime-context.ts +53 -0
- package/src/lib/server/queue.ts +52 -7
- package/src/lib/server/session-tools/canvas.ts +67 -0
- package/src/lib/server/session-tools/connector.ts +83 -9
- package/src/lib/server/session-tools/crud.ts +21 -0
- package/src/lib/server/session-tools/delegate.ts +68 -4
- package/src/lib/server/session-tools/git.ts +71 -0
- package/src/lib/server/session-tools/http.ts +57 -0
- package/src/lib/server/session-tools/index.ts +8 -0
- package/src/lib/server/session-tools/memory.ts +1 -0
- package/src/lib/server/session-tools/search-providers.ts +16 -8
- package/src/lib/server/session-tools/subagent.ts +106 -0
- package/src/lib/server/session-tools/web.ts +115 -4
- package/src/lib/server/stream-agent-chat.ts +32 -10
- package/src/lib/server/task-mention.ts +41 -0
- package/src/lib/sessions.ts +10 -0
- package/src/lib/soul-library.ts +103 -0
- package/src/lib/task-dedupe.ts +26 -0
- package/src/lib/tool-definitions.ts +2 -0
- package/src/lib/tts.ts +2 -2
- package/src/stores/use-app-store.ts +5 -1
- package/src/stores/use-chat-store.ts +65 -2
- package/src/types/index.ts +32 -2
|
@@ -2,27 +2,40 @@
|
|
|
2
2
|
|
|
3
3
|
import { useCallback, useEffect, useState } from 'react'
|
|
4
4
|
import { useAppStore } from '@/stores/use-app-store'
|
|
5
|
+
import { useChatroomStore } from '@/stores/use-chatroom-store'
|
|
5
6
|
import { useWs } from '@/hooks/use-ws'
|
|
6
7
|
import { api } from '@/lib/api-client'
|
|
7
8
|
import type { Connector } from '@/types'
|
|
8
|
-
import { ConnectorPlatformBadge, getConnectorPlatformLabel } from '@/components/shared/connector-platform-icon'
|
|
9
|
+
import { ConnectorPlatformIcon, ConnectorPlatformBadge, CONNECTOR_PLATFORM_META, getConnectorPlatformLabel } from '@/components/shared/connector-platform-icon'
|
|
10
|
+
import { AgentAvatar } from '@/components/agents/agent-avatar'
|
|
9
11
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
+
function relativeTime(ts: number): string {
|
|
13
|
+
const diff = Date.now() - ts
|
|
14
|
+
if (diff < 60_000) return 'just now'
|
|
15
|
+
if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m ago`
|
|
16
|
+
if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)}h ago`
|
|
17
|
+
const d = new Date(ts)
|
|
18
|
+
return d.toLocaleDateString([], { month: 'short', day: 'numeric' })
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function ConnectorList({ inSidebar }: { inSidebar?: boolean }) {
|
|
12
22
|
const connectors = useAppStore((s) => s.connectors)
|
|
13
23
|
const loadConnectors = useAppStore((s) => s.loadConnectors)
|
|
14
24
|
const setConnectorSheetOpen = useAppStore((s) => s.setConnectorSheetOpen)
|
|
15
25
|
const setEditingConnectorId = useAppStore((s) => s.setEditingConnectorId)
|
|
16
26
|
const agents = useAppStore((s) => s.agents)
|
|
17
27
|
const loadAgents = useAppStore((s) => s.loadAgents)
|
|
28
|
+
const chatrooms = useChatroomStore((s) => s.chatrooms)
|
|
29
|
+
const loadChatrooms = useChatroomStore((s) => s.loadChatrooms)
|
|
18
30
|
const [toggling, setToggling] = useState<string | null>(null)
|
|
19
31
|
const [reconnecting, setReconnecting] = useState<string | null>(null)
|
|
20
32
|
const [loaded, setLoaded] = useState(false)
|
|
21
33
|
const [error, setError] = useState<string | null>(null)
|
|
22
34
|
|
|
23
35
|
const refresh = useCallback(async () => {
|
|
24
|
-
await Promise.all([loadConnectors(), loadAgents()])
|
|
36
|
+
await Promise.all([loadConnectors(), loadAgents(), loadChatrooms()])
|
|
25
37
|
setLoaded(true)
|
|
38
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
26
39
|
}, [loadConnectors, loadAgents])
|
|
27
40
|
|
|
28
41
|
useEffect(() => { void refresh() }, [refresh])
|
|
@@ -55,7 +68,6 @@ export function ConnectorList({ inSidebar: _inSidebar }: { inSidebar?: boolean }
|
|
|
55
68
|
setReconnecting(c.id)
|
|
56
69
|
setError(null)
|
|
57
70
|
try {
|
|
58
|
-
// Stop then start to reconnect
|
|
59
71
|
try { await api('PUT', `/connectors/${c.id}`, { action: 'stop' }) } catch { /* may already be stopped */ }
|
|
60
72
|
await api('PUT', `/connectors/${c.id}`, { action: 'start' })
|
|
61
73
|
await refresh()
|
|
@@ -92,104 +104,170 @@ export function ConnectorList({ inSidebar: _inSidebar }: { inSidebar?: boolean }
|
|
|
92
104
|
)
|
|
93
105
|
}
|
|
94
106
|
|
|
107
|
+
// Sidebar: compact list layout
|
|
108
|
+
if (inSidebar) {
|
|
109
|
+
return (
|
|
110
|
+
<div className="flex-1 overflow-y-auto pb-20">
|
|
111
|
+
{error && (
|
|
112
|
+
<div className="mx-4 mt-2 mb-1 px-3 py-2 rounded-[8px] bg-red-500/10 border border-red-500/20 text-red-400 text-[11px] leading-snug">
|
|
113
|
+
{error}
|
|
114
|
+
</div>
|
|
115
|
+
)}
|
|
116
|
+
{list.map((c) => {
|
|
117
|
+
const agent = c.agentId ? agents[c.agentId] : null
|
|
118
|
+
const chatroom = c.chatroomId ? chatrooms[c.chatroomId] : null
|
|
119
|
+
const isRunning = c.status === 'running'
|
|
120
|
+
const meta = CONNECTOR_PLATFORM_META[c.platform]
|
|
121
|
+
return (
|
|
122
|
+
<button
|
|
123
|
+
key={c.id}
|
|
124
|
+
onClick={() => { setEditingConnectorId(c.id); setConnectorSheetOpen(true) }}
|
|
125
|
+
className="w-full flex items-center gap-3 px-5 py-2.5 hover:bg-white/[0.02] transition-colors cursor-pointer bg-transparent border-none text-left"
|
|
126
|
+
>
|
|
127
|
+
<ConnectorPlatformIcon platform={c.platform} size={16} />
|
|
128
|
+
<div className="flex-1 min-w-0">
|
|
129
|
+
<span className="text-[13px] font-600 text-text truncate block">{c.name}</span>
|
|
130
|
+
<span className="text-[11px] text-text-3 truncate block">
|
|
131
|
+
{chatroom ? chatroom.name : agent?.name || meta?.label}
|
|
132
|
+
</span>
|
|
133
|
+
</div>
|
|
134
|
+
<span className={`shrink-0 w-2 h-2 rounded-full ${
|
|
135
|
+
isRunning ? 'bg-green-400' : c.status === 'error' ? 'bg-red-400' : 'bg-white/20'
|
|
136
|
+
}`} />
|
|
137
|
+
</button>
|
|
138
|
+
)
|
|
139
|
+
})}
|
|
140
|
+
</div>
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Main view: card grid
|
|
95
145
|
return (
|
|
96
|
-
<div className="flex-1 overflow-y-auto pb-20">
|
|
146
|
+
<div className="flex-1 overflow-y-auto pb-20 px-5 pt-2">
|
|
97
147
|
{error && (
|
|
98
|
-
<div className="
|
|
148
|
+
<div className="mb-3 px-3 py-2 rounded-[8px] bg-red-500/10 border border-red-500/20 text-red-400 text-[11px] leading-snug">
|
|
99
149
|
{error}
|
|
100
150
|
</div>
|
|
101
151
|
)}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
>
|
|
118
|
-
{/* Clickable area — opens editor */}
|
|
152
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
|
|
153
|
+
{list.map((c) => {
|
|
154
|
+
const platformLabel = getConnectorPlatformLabel(c.platform)
|
|
155
|
+
const agent = c.agentId ? agents[c.agentId] : null
|
|
156
|
+
const chatroom = c.chatroomId ? chatrooms[c.chatroomId] : null
|
|
157
|
+
const isRunning = c.status === 'running'
|
|
158
|
+
const isToggling = toggling === c.id
|
|
159
|
+
const hasCredentials = c.platform === 'whatsapp'
|
|
160
|
+
|| c.platform === 'openclaw'
|
|
161
|
+
|| c.platform === 'signal'
|
|
162
|
+
|| (c.platform === 'bluebubbles' && (!!c.credentialId || !!c.config?.password))
|
|
163
|
+
|| !!c.credentialId
|
|
164
|
+
const lastMsg = c.presence?.lastMessageAt
|
|
165
|
+
|
|
166
|
+
return (
|
|
119
167
|
<button
|
|
168
|
+
key={c.id}
|
|
120
169
|
onClick={() => { setEditingConnectorId(c.id); setConnectorSheetOpen(true) }}
|
|
121
|
-
className="flex
|
|
170
|
+
className="group relative flex flex-col rounded-[14px] border border-white/[0.06] bg-surface p-4 cursor-pointer transition-all hover:border-white/[0.12] hover:bg-white/[0.02] text-left w-full"
|
|
171
|
+
style={{ fontFamily: 'inherit' }}
|
|
122
172
|
>
|
|
123
|
-
|
|
173
|
+
{/* Header: platform badge + status */}
|
|
174
|
+
<div className="flex items-center gap-3 mb-3">
|
|
175
|
+
<ConnectorPlatformBadge platform={c.platform} size={40} iconSize={20} roundedClassName="rounded-[10px]" />
|
|
176
|
+
<div className="flex-1 min-w-0">
|
|
177
|
+
<div className="flex items-center gap-2">
|
|
178
|
+
<span className="text-[14px] font-600 text-text truncate">{c.name}</span>
|
|
179
|
+
<span className={`shrink-0 w-2 h-2 rounded-full ${
|
|
180
|
+
isRunning ? 'bg-green-400' : c.status === 'error' ? 'bg-red-400' : 'bg-white/20'
|
|
181
|
+
}`} />
|
|
182
|
+
</div>
|
|
183
|
+
<span className="text-[11px] text-text-3 block">
|
|
184
|
+
{isRunning ? 'Connected' : c.status === 'error' ? 'Error' : 'Stopped'}
|
|
185
|
+
{c.qrDataUrl && ' · QR ready'}
|
|
186
|
+
</span>
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
124
189
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
className=
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
190
|
+
{/* Route target: agent or chatroom */}
|
|
191
|
+
<div className="flex items-center gap-2.5 mb-2.5 px-0.5">
|
|
192
|
+
{chatroom ? (
|
|
193
|
+
<>
|
|
194
|
+
<div className="w-6 h-6 rounded-full bg-white/[0.06] flex items-center justify-center shrink-0">
|
|
195
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-text-3">
|
|
196
|
+
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
|
|
197
|
+
</svg>
|
|
198
|
+
</div>
|
|
199
|
+
<div className="flex-1 min-w-0">
|
|
200
|
+
<span className="text-[12px] font-600 text-text-2 block truncate">{chatroom.name}</span>
|
|
201
|
+
<span className="text-[10px] text-text-3/60 block">
|
|
202
|
+
{chatroom.agentIds.length} agent{chatroom.agentIds.length !== 1 ? 's' : ''}
|
|
203
|
+
{chatroom.chatMode === 'parallel' ? ' · parallel' : ' · sequential'}
|
|
204
|
+
</span>
|
|
205
|
+
</div>
|
|
206
|
+
</>
|
|
207
|
+
) : agent ? (
|
|
208
|
+
<>
|
|
209
|
+
<AgentAvatar seed={agent.avatarSeed || null} name={agent.name} size={24} />
|
|
210
|
+
<div className="flex-1 min-w-0">
|
|
211
|
+
<span className="text-[12px] font-600 text-text-2 block truncate">{agent.name}</span>
|
|
212
|
+
<span className="text-[10px] text-text-3/60 block">{agent.provider}/{agent.model}</span>
|
|
213
|
+
</div>
|
|
214
|
+
</>
|
|
215
|
+
) : (
|
|
216
|
+
<span className="text-[11px] text-text-3/50">{platformLabel}</span>
|
|
217
|
+
)}
|
|
218
|
+
</div>
|
|
219
|
+
|
|
220
|
+
{/* Footer: last message time + error */}
|
|
221
|
+
<div className="flex items-center gap-2 mt-auto pt-2 border-t border-white/[0.04]">
|
|
222
|
+
{c.lastError ? (
|
|
223
|
+
<span className="text-[10px] text-red-400 truncate flex-1">
|
|
224
|
+
{c.lastError.slice(0, 50)}{c.lastError.length > 50 ? '...' : ''}
|
|
225
|
+
</span>
|
|
226
|
+
) : lastMsg ? (
|
|
227
|
+
<span className="text-[10px] text-text-3/60 flex-1">Last message {relativeTime(lastMsg)}</span>
|
|
228
|
+
) : (
|
|
229
|
+
<span className="text-[10px] text-text-3/40 flex-1">No messages yet</span>
|
|
230
|
+
)}
|
|
231
|
+
|
|
232
|
+
{/* Action buttons */}
|
|
233
|
+
<div className="flex gap-1 shrink-0" onClick={(e) => e.stopPropagation()}>
|
|
234
|
+
{c.status === 'error' && hasCredentials && (
|
|
235
|
+
<button
|
|
236
|
+
onClick={(e) => handleReconnect(e, c)}
|
|
237
|
+
disabled={reconnecting === c.id}
|
|
238
|
+
title="Reconnect"
|
|
239
|
+
className="px-2 py-1 rounded-[6px] text-[10px] font-600 transition-all cursor-pointer border-none opacity-0 group-hover:opacity-100 bg-amber-500/10 text-amber-400 hover:bg-amber-500/20 disabled:opacity-50"
|
|
240
|
+
>
|
|
241
|
+
{reconnecting === c.id ? '...' : 'Reconnect'}
|
|
242
|
+
</button>
|
|
243
|
+
)}
|
|
244
|
+
{hasCredentials && (
|
|
245
|
+
<button
|
|
246
|
+
onClick={(e) => handleToggle(e, c)}
|
|
247
|
+
disabled={isToggling}
|
|
248
|
+
title={isRunning ? 'Stop' : 'Start'}
|
|
249
|
+
className={`w-7 h-7 rounded-[6px] flex items-center justify-center transition-all cursor-pointer border-none ${
|
|
250
|
+
isToggling ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'
|
|
251
|
+
} ${isRunning
|
|
252
|
+
? 'bg-red-500/10 text-red-400 hover:bg-red-500/20'
|
|
253
|
+
: 'bg-green-500/10 text-green-400 hover:bg-green-500/20'
|
|
254
|
+
} disabled:opacity-50`}
|
|
255
|
+
>
|
|
256
|
+
{isToggling ? (
|
|
257
|
+
<span className="w-3 h-3 rounded-full border-2 border-current border-t-transparent animate-spin" />
|
|
258
|
+
) : isRunning ? (
|
|
259
|
+
<svg width="10" height="10" viewBox="0 0 24 24" fill="currentColor"><rect x="4" y="4" width="16" height="16" rx="2" /></svg>
|
|
260
|
+
) : (
|
|
261
|
+
<svg width="10" height="10" viewBox="0 0 24 24" fill="currentColor"><polygon points="6,3 21,12 6,21" /></svg>
|
|
262
|
+
)}
|
|
263
|
+
</button>
|
|
138
264
|
)}
|
|
139
|
-
</div>
|
|
140
|
-
<div className="text-[11px] text-text-3 truncate">
|
|
141
|
-
{c.lastError
|
|
142
|
-
? <span className="text-red-400">{c.lastError.slice(0, 60)}{c.lastError.length > 60 ? '...' : ''}</span>
|
|
143
|
-
: <>{platformLabel} {agent ? `\u2192 ${agent.name}` : ''}</>
|
|
144
|
-
}
|
|
145
265
|
</div>
|
|
146
266
|
</div>
|
|
147
267
|
</button>
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
<button
|
|
152
|
-
onClick={(e) => handleReconnect(e, c)}
|
|
153
|
-
disabled={reconnecting === c.id}
|
|
154
|
-
title="Reconnect"
|
|
155
|
-
aria-label="Reconnect connector"
|
|
156
|
-
className={`shrink-0 px-2.5 py-1.5 rounded-[8px] text-[11px] font-600 transition-all cursor-pointer border-none
|
|
157
|
-
${reconnecting === c.id ? 'opacity-50' : 'opacity-0 group-hover:opacity-100 focus:opacity-100'}
|
|
158
|
-
bg-amber-500/10 text-amber-400 hover:bg-amber-500/20`}
|
|
159
|
-
>
|
|
160
|
-
{reconnecting === c.id ? '...' : 'Reconnect'}
|
|
161
|
-
</button>
|
|
162
|
-
)}
|
|
163
|
-
|
|
164
|
-
{/* Toggle button — visible on hover, only if connector has credentials */}
|
|
165
|
-
{hasCredentials && <button
|
|
166
|
-
onClick={(e) => handleToggle(e, c)}
|
|
167
|
-
disabled={isToggling}
|
|
168
|
-
title={isRunning ? 'Stop connector' : 'Start connector'}
|
|
169
|
-
aria-label={isRunning ? 'Stop connector' : 'Start connector'}
|
|
170
|
-
className={`shrink-0 w-8 h-8 rounded-[8px] flex items-center justify-center transition-all cursor-pointer border-none ${
|
|
171
|
-
isToggling ? 'opacity-100' : 'opacity-0 group-hover:opacity-100 focus:opacity-100'
|
|
172
|
-
} ${
|
|
173
|
-
isRunning
|
|
174
|
-
? 'bg-red-500/10 text-red-400 hover:bg-red-500/20'
|
|
175
|
-
: 'bg-green-500/10 text-green-400 hover:bg-green-500/20'
|
|
176
|
-
} disabled:opacity-50`}
|
|
177
|
-
>
|
|
178
|
-
{isToggling ? (
|
|
179
|
-
<span className="w-3 h-3 rounded-full border-2 border-current border-t-transparent animate-spin" />
|
|
180
|
-
) : isRunning ? (
|
|
181
|
-
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor">
|
|
182
|
-
<rect x="4" y="4" width="16" height="16" rx="2" />
|
|
183
|
-
</svg>
|
|
184
|
-
) : (
|
|
185
|
-
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor">
|
|
186
|
-
<polygon points="6,3 21,12 6,21" />
|
|
187
|
-
</svg>
|
|
188
|
-
)}
|
|
189
|
-
</button>}
|
|
190
|
-
</div>
|
|
191
|
-
)
|
|
192
|
-
})}
|
|
268
|
+
)
|
|
269
|
+
})}
|
|
270
|
+
</div>
|
|
193
271
|
</div>
|
|
194
272
|
)
|
|
195
273
|
}
|
|
@@ -9,8 +9,10 @@ import { toast } from 'sonner'
|
|
|
9
9
|
import type { Connector, ConnectorPlatform } from '@/types'
|
|
10
10
|
import { ConnectorPlatformBadge } from '@/components/shared/connector-platform-icon'
|
|
11
11
|
import { AgentPickerList } from '@/components/shared/agent-picker-list'
|
|
12
|
+
import { ChatroomPickerList } from '@/components/shared/chatroom-picker-list'
|
|
12
13
|
import { SheetFooter } from '@/components/shared/sheet-footer'
|
|
13
14
|
import { SectionLabel } from '@/components/shared/section-label'
|
|
15
|
+
import { useChatroomStore } from '@/stores/use-chatroom-store'
|
|
14
16
|
|
|
15
17
|
/** Auto-detect URLs in text and make them clickable links that open in a new tab */
|
|
16
18
|
function linkify(text: string) {
|
|
@@ -232,9 +234,14 @@ export function ConnectorSheet() {
|
|
|
232
234
|
const loadAgents = useAppStore((s) => s.loadAgents)
|
|
233
235
|
const loadCredentials = useAppStore((s) => s.loadCredentials)
|
|
234
236
|
|
|
237
|
+
const chatrooms = useChatroomStore((s) => s.chatrooms)
|
|
238
|
+
const loadChatrooms = useChatroomStore((s) => s.loadChatrooms)
|
|
239
|
+
|
|
235
240
|
const [name, setName] = useState('')
|
|
236
241
|
const [platform, setPlatform] = useState<ConnectorPlatform>('discord')
|
|
237
242
|
const [agentId, setAgentId] = useState('')
|
|
243
|
+
const [routeMode, setRouteMode] = useState<'agent' | 'chatroom'>('agent')
|
|
244
|
+
const [chatroomId, setChatroomId] = useState('')
|
|
238
245
|
const [credentialId, setCredentialId] = useState('')
|
|
239
246
|
const [config, setConfig] = useState<Record<string, string>>({})
|
|
240
247
|
const [saving, setSaving] = useState(false)
|
|
@@ -255,8 +262,10 @@ export function ConnectorSheet() {
|
|
|
255
262
|
if (open) {
|
|
256
263
|
loadAgents()
|
|
257
264
|
loadCredentials()
|
|
265
|
+
loadChatrooms()
|
|
258
266
|
setShowSetup(false)
|
|
259
267
|
}
|
|
268
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
260
269
|
}, [open])
|
|
261
270
|
|
|
262
271
|
// Sync form fields when editing connector changes (by ID, not reference)
|
|
@@ -265,13 +274,17 @@ export function ConnectorSheet() {
|
|
|
265
274
|
if (editing) {
|
|
266
275
|
setName(editing.name)
|
|
267
276
|
setPlatform(editing.platform)
|
|
268
|
-
setAgentId(editing.agentId)
|
|
277
|
+
setAgentId(editing.agentId || '')
|
|
278
|
+
setRouteMode(editing.chatroomId ? 'chatroom' : 'agent')
|
|
279
|
+
setChatroomId(editing.chatroomId || '')
|
|
269
280
|
setCredentialId(editing.credentialId || '')
|
|
270
281
|
setConfig(editing.config || {})
|
|
271
282
|
} else {
|
|
272
283
|
setName('')
|
|
273
284
|
setPlatform('discord')
|
|
274
285
|
setAgentId('')
|
|
286
|
+
setRouteMode('agent')
|
|
287
|
+
setChatroomId('')
|
|
275
288
|
setCredentialId('')
|
|
276
289
|
setConfig({})
|
|
277
290
|
}
|
|
@@ -308,13 +321,17 @@ export function ConnectorSheet() {
|
|
|
308
321
|
useWs('connectors', pollWaStatus, isWaRunning ? 2000 : undefined)
|
|
309
322
|
|
|
310
323
|
const handleSave = async () => {
|
|
311
|
-
|
|
324
|
+
const hasTarget = routeMode === 'agent' ? !!agentId : !!chatroomId
|
|
325
|
+
if (!hasTarget) return
|
|
312
326
|
setSaving(true)
|
|
327
|
+
const routePayload = routeMode === 'agent'
|
|
328
|
+
? { agentId, chatroomId: null }
|
|
329
|
+
: { agentId: null, chatroomId }
|
|
313
330
|
try {
|
|
314
331
|
if (editing) {
|
|
315
|
-
await api('PUT', `/connectors/${editing.id}`, { name,
|
|
332
|
+
await api('PUT', `/connectors/${editing.id}`, { name, ...routePayload, credentialId: credentialId || null, config })
|
|
316
333
|
} else {
|
|
317
|
-
await api('POST', '/connectors', { name: name || `${platformConfig?.label} Bot`, platform,
|
|
334
|
+
await api('POST', '/connectors', { name: name || `${platformConfig?.label} Bot`, platform, ...routePayload, credentialId: credentialId || null, config })
|
|
318
335
|
}
|
|
319
336
|
await loadConnectors()
|
|
320
337
|
setOpen(false)
|
|
@@ -459,16 +476,51 @@ export function ConnectorSheet() {
|
|
|
459
476
|
/>
|
|
460
477
|
</div>
|
|
461
478
|
|
|
462
|
-
{/*
|
|
479
|
+
{/* Route mode toggle + target selector */}
|
|
463
480
|
<div className="mb-6">
|
|
464
|
-
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">Route
|
|
465
|
-
<
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
481
|
+
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">Route Messages To</label>
|
|
482
|
+
<div className="flex gap-1 mb-3 p-1 rounded-[10px] bg-white/[0.04] border border-white/[0.06]">
|
|
483
|
+
<button
|
|
484
|
+
type="button"
|
|
485
|
+
onClick={() => setRouteMode('agent')}
|
|
486
|
+
className={`flex-1 py-2 px-3 rounded-[8px] text-[13px] font-600 transition-all cursor-pointer border-none ${
|
|
487
|
+
routeMode === 'agent' ? 'bg-accent-soft text-accent-bright' : 'bg-transparent text-text-3 hover:text-text-2'
|
|
488
|
+
}`}
|
|
489
|
+
style={{ fontFamily: 'inherit' }}
|
|
490
|
+
>
|
|
491
|
+
Single Agent
|
|
492
|
+
</button>
|
|
493
|
+
<button
|
|
494
|
+
type="button"
|
|
495
|
+
onClick={() => setRouteMode('chatroom')}
|
|
496
|
+
className={`flex-1 py-2 px-3 rounded-[8px] text-[13px] font-600 transition-all cursor-pointer border-none ${
|
|
497
|
+
routeMode === 'chatroom' ? 'bg-accent-soft text-accent-bright' : 'bg-transparent text-text-3 hover:text-text-2'
|
|
498
|
+
}`}
|
|
499
|
+
style={{ fontFamily: 'inherit' }}
|
|
500
|
+
>
|
|
501
|
+
Chat Room
|
|
502
|
+
</button>
|
|
503
|
+
</div>
|
|
504
|
+
{routeMode === 'agent' ? (
|
|
505
|
+
<>
|
|
506
|
+
<p className="text-[12px] text-text-3/60 mb-2">Incoming messages will be handled by this agent</p>
|
|
507
|
+
<AgentPickerList
|
|
508
|
+
agents={agentList}
|
|
509
|
+
selected={agentId}
|
|
510
|
+
onSelect={(id) => setAgentId(id)}
|
|
511
|
+
showOrchBadge={true}
|
|
512
|
+
/>
|
|
513
|
+
</>
|
|
514
|
+
) : (
|
|
515
|
+
<>
|
|
516
|
+
<p className="text-[12px] text-text-3/60 mb-2">Incoming messages will be routed to a chat room with multiple agents</p>
|
|
517
|
+
<ChatroomPickerList
|
|
518
|
+
chatrooms={Object.values(chatrooms)}
|
|
519
|
+
selected={chatroomId}
|
|
520
|
+
onSelect={(id) => setChatroomId(id)}
|
|
521
|
+
/>
|
|
522
|
+
</>
|
|
523
|
+
)}
|
|
472
524
|
</div>
|
|
473
525
|
|
|
474
526
|
{/* Bot token credential */}
|
|
@@ -747,8 +799,8 @@ export function ConnectorSheet() {
|
|
|
747
799
|
{editing && editing.platform === 'whatsapp' && (editing.status === 'running' || waConnecting) && !qrDataUrl && !waAuthenticated && (
|
|
748
800
|
<div className="mb-6 p-5 rounded-[14px] border border-white/[0.06] bg-white/[0.01] text-center">
|
|
749
801
|
<div className="flex items-center justify-center gap-2 mb-1">
|
|
750
|
-
<span className="w-3 h-3 rounded-full border-2 border-
|
|
751
|
-
<span className="text-[13px] font-600 text-
|
|
802
|
+
<span className="w-3 h-3 rounded-full border-2 border-blue-500 border-t-transparent animate-spin" />
|
|
803
|
+
<span className="text-[13px] font-600 text-blue-500">
|
|
752
804
|
{waHasCreds ? 'Reconnecting...' : 'Waiting for QR code...'}
|
|
753
805
|
</span>
|
|
754
806
|
</div>
|
|
@@ -798,7 +850,7 @@ export function ConnectorSheet() {
|
|
|
798
850
|
onCancel={() => { setOpen(false); setEditingId(null) }}
|
|
799
851
|
onSave={handleSave}
|
|
800
852
|
saveLabel={saving ? 'Saving...' : editing ? 'Save' : 'Create Connector'}
|
|
801
|
-
saveDisabled={saving || !agentId}
|
|
853
|
+
saveDisabled={saving || (routeMode === 'agent' ? !agentId : !chatroomId)}
|
|
802
854
|
left={editing && (
|
|
803
855
|
<button onClick={handleDelete} className="py-3.5 px-6 rounded-[14px] border border-red-500/20 bg-transparent text-red-400 text-[15px] font-600 cursor-pointer hover:bg-red-500/10 transition-all" style={{ fontFamily: 'inherit' }}>
|
|
804
856
|
Delete
|
|
@@ -354,7 +354,7 @@ export function HomeView() {
|
|
|
354
354
|
>
|
|
355
355
|
<div className="relative">
|
|
356
356
|
<AgentAvatar seed={agent.avatarSeed} name={agent.name} size={36} />
|
|
357
|
-
<div className={`absolute -bottom-0.5 -right-0.5 w-2.5 h-2.5 rounded-full border-2 border-
|
|
357
|
+
<div className={`absolute -bottom-0.5 -right-0.5 w-2.5 h-2.5 rounded-full border-2 border-surface ${
|
|
358
358
|
isTyping ? 'bg-accent-bright animate-pulse'
|
|
359
359
|
: isOnline ? 'bg-emerald-400 shadow-[0_0_6px_rgba(52,211,153,0.4)]'
|
|
360
360
|
: 'bg-text-3/30'
|
|
@@ -7,6 +7,7 @@ import { uploadImage } from '@/lib/upload'
|
|
|
7
7
|
import { useAutoResize } from '@/hooks/use-auto-resize'
|
|
8
8
|
import { useSpeechRecognition } from '@/hooks/use-speech-recognition'
|
|
9
9
|
import { FilePreview } from '@/components/shared/file-preview'
|
|
10
|
+
import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip'
|
|
10
11
|
|
|
11
12
|
interface Props {
|
|
12
13
|
streaming: boolean
|
|
@@ -91,8 +92,8 @@ export function ChatInput({ streaming, onSend, onStop }: Props) {
|
|
|
91
92
|
try {
|
|
92
93
|
const result = await uploadImage(file)
|
|
93
94
|
addPendingFile({ file, path: result.path, url: result.url })
|
|
94
|
-
} catch {
|
|
95
|
-
|
|
95
|
+
} catch (err: unknown) {
|
|
96
|
+
console.error('File upload failed:', err instanceof Error ? err.message : String(err))
|
|
96
97
|
}
|
|
97
98
|
}, [addPendingFile])
|
|
98
99
|
|
|
@@ -226,6 +227,31 @@ export function ChatInput({ streaming, onSend, onStop }: Props) {
|
|
|
226
227
|
</button>
|
|
227
228
|
)}
|
|
228
229
|
|
|
230
|
+
<Tooltip>
|
|
231
|
+
<TooltipTrigger asChild>
|
|
232
|
+
<button
|
|
233
|
+
type="button"
|
|
234
|
+
onClick={() => { useChatStore.getState().clearContext() }}
|
|
235
|
+
disabled={streaming}
|
|
236
|
+
className="flex items-center gap-1.5 px-3 py-2 rounded-[10px] border-none bg-transparent
|
|
237
|
+
text-text-3 text-[13px] cursor-pointer hover:text-amber-400 hover:bg-amber-400/10 transition-all duration-200 disabled:opacity-30 disabled:pointer-events-none"
|
|
238
|
+
style={{ fontFamily: 'inherit' }}
|
|
239
|
+
>
|
|
240
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round">
|
|
241
|
+
<line x1="2" y1="12" x2="22" y2="12" />
|
|
242
|
+
<polyline points="8 8 4 12 8 16" />
|
|
243
|
+
<polyline points="16 8 20 12 16 16" />
|
|
244
|
+
</svg>
|
|
245
|
+
<span className="hidden sm:inline">New context</span>
|
|
246
|
+
</button>
|
|
247
|
+
</TooltipTrigger>
|
|
248
|
+
<TooltipContent side="top" sideOffset={8}
|
|
249
|
+
className="bg-raised border border-white/[0.08] text-text shadow-[0_8px_32px_rgba(0,0,0,0.5)] rounded-[10px] px-3.5 py-2.5 max-w-[220px]">
|
|
250
|
+
<div className="font-display text-[12px] font-600 mb-0.5">New context window</div>
|
|
251
|
+
<div className="text-[11px] text-text-3 leading-[1.4]">Adds a marker — messages above it won't be sent to the AI. Nothing is deleted.</div>
|
|
252
|
+
</TooltipContent>
|
|
253
|
+
</Tooltip>
|
|
254
|
+
|
|
229
255
|
<div className="flex-1" />
|
|
230
256
|
|
|
231
257
|
<span className="text-[11px] text-text-3/60 tabular-nums mr-2 font-mono">
|
|
@@ -55,6 +55,7 @@ import { MobileHeader } from './mobile-header'
|
|
|
55
55
|
import { DaemonIndicator } from './daemon-indicator'
|
|
56
56
|
import { NotificationCenter } from '@/components/shared/notification-center'
|
|
57
57
|
import { ChatArea } from '@/components/chat/chat-area'
|
|
58
|
+
import { CanvasPanel } from '@/components/canvas/canvas-panel'
|
|
58
59
|
import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip'
|
|
59
60
|
import { api } from '@/lib/api-client'
|
|
60
61
|
import type { AppView } from '@/types'
|
|
@@ -91,6 +92,7 @@ export function AppLayout() {
|
|
|
91
92
|
const appSettings = useAppStore((s) => s.appSettings)
|
|
92
93
|
const [agentViewMode, setAgentViewMode] = useState<'chat' | 'config'>('chat')
|
|
93
94
|
const [profileSheetOpen, setProfileSheetOpen] = useState(false)
|
|
95
|
+
const [canvasDismissedFor, setCanvasDismissedFor] = useState<string | null>(null)
|
|
94
96
|
|
|
95
97
|
const handleShortcutKey = useCallback((e: KeyboardEvent) => {
|
|
96
98
|
const mod = e.metaKey || e.ctrlKey
|
|
@@ -192,6 +194,10 @@ export function AppLayout() {
|
|
|
192
194
|
: Object.values(agents)[0]?.id || null
|
|
193
195
|
const isMainChat = activeView === 'agents' && currentAgentId === defaultAgentId
|
|
194
196
|
|
|
197
|
+
const currentSession = currentSessionId ? sessions[currentSessionId] : null
|
|
198
|
+
const hasCanvas = !!(currentSession?.canvasContent && canvasDismissedFor !== currentSessionId)
|
|
199
|
+
const canvasAgentName = currentSession?.agentId && agents[currentSession.agentId] ? agents[currentSession.agentId].name : undefined
|
|
200
|
+
|
|
195
201
|
const goToMainChat = async () => {
|
|
196
202
|
if (defaultAgentId) {
|
|
197
203
|
await setCurrentAgent(defaultAgentId)
|
|
@@ -680,12 +686,23 @@ export function AppLayout() {
|
|
|
680
686
|
|
|
681
687
|
{/* Main content */}
|
|
682
688
|
<ErrorBoundary>
|
|
683
|
-
<div className="flex-1 flex flex-col h-full min-w-0 bg-bg">
|
|
689
|
+
<div className="flex-1 flex flex-col h-full min-h-0 min-w-0 bg-bg">
|
|
684
690
|
{!isDesktop && <MobileHeader />}
|
|
685
691
|
{activeView === 'home' ? (
|
|
686
692
|
<HomeView />
|
|
687
693
|
) : activeView === 'agents' && hasSelectedSession ? (
|
|
688
|
-
<
|
|
694
|
+
<div className="flex-1 flex h-full min-h-0 min-w-0">
|
|
695
|
+
<div className="flex-1 min-h-0 min-w-0 overflow-hidden">
|
|
696
|
+
<ChatArea />
|
|
697
|
+
</div>
|
|
698
|
+
{hasCanvas && currentSessionId && (
|
|
699
|
+
<CanvasPanel
|
|
700
|
+
sessionId={currentSessionId}
|
|
701
|
+
agentName={canvasAgentName}
|
|
702
|
+
onClose={() => setCanvasDismissedFor(currentSessionId)}
|
|
703
|
+
/>
|
|
704
|
+
)}
|
|
705
|
+
</div>
|
|
689
706
|
) : activeView === 'agents' ? (
|
|
690
707
|
<div className="flex-1 flex flex-col">
|
|
691
708
|
{!isDesktop ? (
|
|
@@ -44,7 +44,7 @@ function AssignAgentPicker({ projectId, onClose }: { projectId: string; onClose:
|
|
|
44
44
|
return (
|
|
45
45
|
<>
|
|
46
46
|
<div className="fixed inset-0 z-40" onClick={onClose} />
|
|
47
|
-
<div className="absolute left-0 top-full mt-2 z-50 w-[260px] rounded-[12px] bg-
|
|
47
|
+
<div className="absolute left-0 top-full mt-2 z-50 w-[260px] rounded-[12px] bg-surface/95 backdrop-blur-xl border border-white/[0.1] shadow-[0_12px_40px_rgba(0,0,0,0.5)] overflow-hidden">
|
|
48
48
|
<div className="p-2.5 border-b border-white/[0.06]">
|
|
49
49
|
<input
|
|
50
50
|
value={query}
|