@swarmclawai/swarmclaw 0.7.3 → 0.7.4
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 +47 -40
- package/bin/package-manager.js +157 -0
- package/bin/package-manager.test.js +90 -0
- package/bin/server-cmd.js +38 -7
- package/bin/swarmclaw.js +54 -4
- package/bin/update-cmd.js +48 -10
- package/bin/update-cmd.test.js +55 -0
- package/package.json +8 -3
- package/scripts/postinstall.mjs +26 -0
- package/src/app/api/agents/[id]/route.ts +17 -0
- package/src/app/api/agents/[id]/thread/route.ts +3 -1
- package/src/app/api/agents/route.ts +23 -1
- package/src/app/api/auth/route.ts +1 -1
- package/src/app/api/chatrooms/[id]/chat/route.ts +16 -5
- package/src/app/api/chatrooms/[id]/pins/route.ts +2 -1
- package/src/app/api/chatrooms/[id]/reactions/route.ts +2 -1
- package/src/app/api/chatrooms/[id]/route.ts +6 -0
- package/src/app/api/chats/[id]/route.ts +12 -0
- package/src/app/api/chats/heartbeat/route.ts +2 -1
- package/src/app/api/chats/route.ts +7 -1
- package/src/app/api/external-agents/[id]/heartbeat/route.ts +33 -0
- package/src/app/api/external-agents/[id]/route.ts +31 -0
- package/src/app/api/external-agents/register/route.ts +3 -0
- package/src/app/api/external-agents/route.ts +66 -0
- package/src/app/api/gateways/[id]/health/route.ts +28 -0
- package/src/app/api/gateways/[id]/route.ts +79 -0
- package/src/app/api/gateways/route.ts +57 -0
- package/src/app/api/openclaw/gateway/route.ts +10 -7
- package/src/app/api/openclaw/skills/route.ts +1 -1
- package/src/app/api/providers/[id]/discover-models/route.ts +27 -0
- package/src/app/api/schedules/[id]/route.ts +38 -9
- package/src/app/api/schedules/route.ts +51 -28
- package/src/app/api/settings/route.ts +6 -10
- package/src/app/api/setup/doctor/route.ts +6 -4
- package/src/app/api/tasks/[id]/route.ts +2 -1
- package/src/app/api/tasks/bulk/route.ts +2 -2
- package/src/app/page.tsx +126 -15
- package/src/cli/binary.test.js +142 -0
- package/src/cli/index.js +34 -11
- package/src/cli/index.test.js +195 -0
- package/src/cli/index.ts +20 -4
- package/src/cli/server-cmd.test.js +59 -0
- package/src/cli/spec.js +20 -2
- package/src/components/agents/agent-sheet.tsx +249 -7
- package/src/components/agents/inspector-panel.tsx +3 -2
- package/src/components/agents/sandbox-env-panel.tsx +4 -1
- package/src/components/auth/setup-wizard.tsx +970 -275
- package/src/components/chat/chat-area.tsx +41 -14
- package/src/components/chat/chat-card.tsx +2 -1
- package/src/components/chat/chat-header.tsx +8 -13
- package/src/components/chat/chat-list.tsx +58 -20
- package/src/components/chat/message-list.tsx +142 -18
- package/src/components/chatrooms/chatroom-input.tsx +96 -33
- package/src/components/chatrooms/chatroom-list.tsx +141 -72
- package/src/components/chatrooms/chatroom-message.tsx +7 -6
- package/src/components/chatrooms/chatroom-sheet.tsx +13 -1
- package/src/components/chatrooms/chatroom-tool-request-banner.tsx +5 -2
- package/src/components/chatrooms/chatroom-view.tsx +157 -86
- package/src/components/chatrooms/reaction-picker.tsx +38 -33
- package/src/components/gateways/gateway-sheet.tsx +567 -0
- package/src/components/input/chat-input.tsx +135 -86
- package/src/components/layout/app-layout.tsx +2 -0
- package/src/components/memory/memory-browser.tsx +71 -6
- package/src/components/memory/memory-card.tsx +18 -0
- package/src/components/memory/memory-detail.tsx +58 -31
- package/src/components/memory/memory-sheet.tsx +32 -4
- package/src/components/projects/project-detail.tsx +7 -2
- package/src/components/providers/provider-list.tsx +158 -2
- package/src/components/providers/provider-sheet.tsx +81 -70
- package/src/components/shared/bottom-sheet.tsx +31 -15
- package/src/components/shared/confirm-dialog.tsx +45 -30
- package/src/components/shared/model-combobox.tsx +90 -8
- package/src/components/shared/settings/section-heartbeat.tsx +11 -6
- package/src/components/shared/settings/section-orchestrator.tsx +3 -0
- package/src/components/shared/settings/settings-page.tsx +5 -3
- package/src/components/tasks/approvals-panel.tsx +7 -1
- package/src/components/ui/dialog.tsx +2 -2
- package/src/components/wallets/wallet-approval-dialog.tsx +59 -54
- package/src/lib/heartbeat-defaults.ts +48 -0
- package/src/lib/memory-presentation.ts +59 -0
- package/src/lib/provider-model-discovery-client.ts +29 -0
- package/src/lib/providers/index.ts +12 -5
- package/src/lib/runtime-loop.ts +105 -3
- package/src/lib/safe-storage.ts +6 -1
- package/src/lib/server/agent-runtime-config.test.ts +141 -0
- package/src/lib/server/agent-runtime-config.ts +277 -0
- package/src/lib/server/approvals-auto-approve.test.ts +59 -0
- package/src/lib/server/build-llm.test.ts +13 -5
- package/src/lib/server/chat-execution-tool-events.test.ts +87 -2
- package/src/lib/server/chat-execution.ts +159 -71
- package/src/lib/server/chatroom-helpers.test.ts +7 -0
- package/src/lib/server/chatroom-helpers.ts +99 -6
- package/src/lib/server/chatroom-session-persistence.test.ts +87 -0
- package/src/lib/server/connectors/manager.ts +89 -61
- package/src/lib/server/connectors/slack.ts +1 -1
- package/src/lib/server/daemon-state.ts +3 -2
- package/src/lib/server/eval/agent-regression.test.ts +47 -0
- package/src/lib/server/eval/agent-regression.ts +1742 -0
- package/src/lib/server/eval/runner.ts +11 -1
- package/src/lib/server/eval/store.ts +2 -1
- package/src/lib/server/heartbeat-service.ts +10 -4
- package/src/lib/server/main-agent-loop.ts +13 -6
- package/src/lib/server/openclaw-exec-config.ts +4 -2
- package/src/lib/server/openclaw-gateway.ts +123 -36
- package/src/lib/server/orchestrator-lg.ts +1 -2
- package/src/lib/server/orchestrator.ts +3 -2
- package/src/lib/server/plugins.test.ts +9 -1
- package/src/lib/server/plugins.ts +12 -2
- package/src/lib/server/provider-model-discovery.ts +481 -0
- package/src/lib/server/queue.ts +1 -1
- package/src/lib/server/runtime-settings.test.ts +119 -0
- package/src/lib/server/runtime-settings.ts +12 -92
- package/src/lib/server/schedule-normalization.ts +187 -0
- package/src/lib/server/session-tools/autonomy-tools.test.ts +23 -0
- package/src/lib/server/session-tools/crud.ts +27 -3
- package/src/lib/server/session-tools/discovery-approvals.test.ts +170 -0
- package/src/lib/server/session-tools/discovery.ts +18 -8
- package/src/lib/server/session-tools/file-normalize.test.ts +5 -0
- package/src/lib/server/session-tools/file.ts +8 -2
- package/src/lib/server/session-tools/http.ts +9 -3
- package/src/lib/server/session-tools/index.ts +31 -1
- package/src/lib/server/session-tools/manage-schedules.test.ts +137 -0
- package/src/lib/server/session-tools/monitor.ts +14 -7
- package/src/lib/server/session-tools/openclaw-nodes.test.ts +111 -0
- package/src/lib/server/session-tools/openclaw-nodes.ts +86 -20
- package/src/lib/server/session-tools/platform.ts +1 -1
- package/src/lib/server/session-tools/plugin-creator.ts +9 -2
- package/src/lib/server/session-tools/sandbox.ts +51 -92
- package/src/lib/server/session-tools/session-info.ts +22 -1
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +23 -0
- package/src/lib/server/session-tools/shell.ts +2 -2
- package/src/lib/server/session-tools/subagent.ts +3 -1
- package/src/lib/server/session-tools/web.ts +73 -30
- package/src/lib/server/storage.ts +29 -3
- package/src/lib/server/stream-agent-chat.test.ts +61 -0
- package/src/lib/server/stream-agent-chat.ts +139 -4
- package/src/lib/server/structured-extract.ts +1 -1
- package/src/lib/server/task-mention.ts +0 -1
- package/src/lib/server/tool-aliases.ts +37 -6
- package/src/lib/server/tool-capability-policy.ts +1 -1
- package/src/lib/setup-defaults.ts +352 -11
- package/src/lib/tool-definitions.ts +3 -4
- package/src/lib/validation/schemas.ts +55 -1
- package/src/stores/use-app-store.ts +43 -1
- package/src/stores/use-chatroom-store.ts +153 -26
- package/src/types/index.ts +189 -6
- package/src/app/api/chats/[id]/main-loop/route.ts +0 -13
|
@@ -29,6 +29,16 @@ export function ChatroomInput({ agents, onSend, disabled }: Props) {
|
|
|
29
29
|
const removePendingFile = useChatroomStore((s) => s.removePendingFile)
|
|
30
30
|
const replyingTo = useChatroomStore((s) => s.replyingTo)
|
|
31
31
|
const setReplyingTo = useChatroomStore((s) => s.setReplyingTo)
|
|
32
|
+
const streaming = useChatroomStore((s) => s.streaming)
|
|
33
|
+
const queuedMessages = useChatroomStore((s) => s.queuedMessages)
|
|
34
|
+
const removeQueuedMessage = useChatroomStore((s) => s.removeQueuedMessage)
|
|
35
|
+
|
|
36
|
+
const resizeTextarea = useCallback(() => {
|
|
37
|
+
const node = inputRef.current
|
|
38
|
+
if (!node) return
|
|
39
|
+
node.style.height = 'auto'
|
|
40
|
+
node.style.height = `${Math.min(node.scrollHeight, 160)}px`
|
|
41
|
+
}, [])
|
|
32
42
|
|
|
33
43
|
// Draft persistence: restore on chatroom change
|
|
34
44
|
const draftTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
@@ -38,6 +48,10 @@ export function ChatroomInput({ agents, onSend, disabled }: Props) {
|
|
|
38
48
|
setText(draft || '')
|
|
39
49
|
}, [chatroomId])
|
|
40
50
|
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
resizeTextarea()
|
|
53
|
+
}, [resizeTextarea, text, chatroomId])
|
|
54
|
+
|
|
41
55
|
// Debounced save to localStorage
|
|
42
56
|
useEffect(() => {
|
|
43
57
|
if (!chatroomId) return
|
|
@@ -83,6 +97,7 @@ export function ChatroomInput({ agents, onSend, disabled }: Props) {
|
|
|
83
97
|
|
|
84
98
|
const handleChange = useCallback((value: string) => {
|
|
85
99
|
setText(value)
|
|
100
|
+
resizeTextarea()
|
|
86
101
|
const cursorPos = inputRef.current?.selectionStart || value.length
|
|
87
102
|
const beforeCursor = value.slice(0, cursorPos)
|
|
88
103
|
const mentionMatch = beforeCursor.match(/@(\S*)$/)
|
|
@@ -95,7 +110,7 @@ export function ChatroomInput({ agents, onSend, disabled }: Props) {
|
|
|
95
110
|
setMentionFilter('')
|
|
96
111
|
setSelectedIndex(0)
|
|
97
112
|
}
|
|
98
|
-
}, [])
|
|
113
|
+
}, [resizeTextarea])
|
|
99
114
|
|
|
100
115
|
const insertMention = useCallback((name: string) => {
|
|
101
116
|
const cursorPos = inputRef.current?.selectionStart || text.length
|
|
@@ -138,13 +153,23 @@ export function ChatroomInput({ agents, onSend, disabled }: Props) {
|
|
|
138
153
|
return parts.length > 0 ? parts : null
|
|
139
154
|
}, [text])
|
|
140
155
|
|
|
141
|
-
const mentionDropdownVisible = showMentions
|
|
156
|
+
const mentionDropdownVisible = showMentions
|
|
142
157
|
const mentionItems = mentionDropdownVisible
|
|
143
158
|
? ['all', ...filteredAgents.map((a) => a.name)]
|
|
144
159
|
: []
|
|
160
|
+
const visibleQueuedMessages = queuedMessages.filter((item) => item.chatroomId === chatroomId)
|
|
161
|
+
|
|
162
|
+
const handleSendCurrent = useCallback(() => {
|
|
163
|
+
if ((!text.trim() && !pendingFiles.length) || disabled) return
|
|
164
|
+
onSend(text)
|
|
165
|
+
setText('')
|
|
166
|
+
resizeTextarea()
|
|
167
|
+
if (chatroomId) safeStorageRemove(`sc_draft_cr_${chatroomId}`)
|
|
168
|
+
setShowMentions(false)
|
|
169
|
+
}, [chatroomId, disabled, onSend, pendingFiles.length, resizeTextarea, text])
|
|
145
170
|
|
|
146
171
|
const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
|
|
147
|
-
if (mentionDropdownVisible) {
|
|
172
|
+
if (mentionDropdownVisible && mentionItems.length > 0) {
|
|
148
173
|
if (e.key === 'ArrowDown') {
|
|
149
174
|
e.preventDefault()
|
|
150
175
|
setSelectedIndex((i) => (i + 1) % mentionItems.length)
|
|
@@ -165,12 +190,7 @@ export function ChatroomInput({ agents, onSend, disabled }: Props) {
|
|
|
165
190
|
|
|
166
191
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
167
192
|
e.preventDefault()
|
|
168
|
-
|
|
169
|
-
onSend(text)
|
|
170
|
-
setText('')
|
|
171
|
-
if (chatroomId) safeStorageRemove(`sc_draft_cr_${chatroomId}`)
|
|
172
|
-
setShowMentions(false)
|
|
173
|
-
}
|
|
193
|
+
handleSendCurrent()
|
|
174
194
|
}
|
|
175
195
|
if (e.key === 'Escape') {
|
|
176
196
|
if (replyingTo) {
|
|
@@ -195,21 +215,68 @@ export function ChatroomInput({ agents, onSend, disabled }: Props) {
|
|
|
195
215
|
<span className="text-[13px] text-text">all</span>
|
|
196
216
|
<span className="text-[11px] text-text-3 ml-auto">Mention all agents</span>
|
|
197
217
|
</button>
|
|
198
|
-
{filteredAgents.
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
218
|
+
{filteredAgents.length > 0 ? (
|
|
219
|
+
filteredAgents.map((agent, i) => (
|
|
220
|
+
<button
|
|
221
|
+
key={agent.id}
|
|
222
|
+
onClick={() => insertMention(agent.name)}
|
|
223
|
+
className={`w-full flex items-center gap-2 px-3 py-2 text-left transition-all cursor-pointer ${
|
|
224
|
+
selectedIndex === i + 1 ? 'bg-white/[0.08]' : 'hover:bg-white/[0.06]'
|
|
225
|
+
}`}
|
|
226
|
+
>
|
|
227
|
+
<AgentAvatar seed={agent.avatarSeed} avatarUrl={agent.avatarUrl} name={agent.name} size={20} />
|
|
228
|
+
<span className="text-[13px] text-text">{agent.name}</span>
|
|
229
|
+
</button>
|
|
230
|
+
))
|
|
231
|
+
) : (
|
|
232
|
+
<div className="px-3 py-3 text-[12px] text-text-3">
|
|
233
|
+
No agents match <span className="text-text">@{mentionFilter}</span>.
|
|
234
|
+
</div>
|
|
235
|
+
)}
|
|
236
|
+
</div>
|
|
237
|
+
)}
|
|
238
|
+
|
|
239
|
+
{visibleQueuedMessages.length > 0 && (
|
|
240
|
+
<div className="mb-2 flex flex-wrap items-center gap-1.5">
|
|
241
|
+
<span className="label-mono text-amber-400/70">Queued</span>
|
|
242
|
+
{visibleQueuedMessages.map((item) => (
|
|
243
|
+
<span key={item.id} className="inline-flex items-center gap-1.5 rounded-[8px] border border-amber-500/15 bg-amber-500/10 px-2.5 py-1 text-[11px] text-amber-300">
|
|
244
|
+
<span className="truncate max-w-[180px]">
|
|
245
|
+
{item.text.trim() || `Attachment${item.pendingFiles.length > 1 ? 's' : ''}`}
|
|
246
|
+
</span>
|
|
247
|
+
{item.pendingFiles.length > 0 && (
|
|
248
|
+
<span className="rounded-full bg-amber-500/10 px-1.5 py-0.5 text-[10px]">
|
|
249
|
+
+{item.pendingFiles.length} file{item.pendingFiles.length === 1 ? '' : 's'}
|
|
250
|
+
</span>
|
|
251
|
+
)}
|
|
252
|
+
<button
|
|
253
|
+
type="button"
|
|
254
|
+
onClick={() => removeQueuedMessage(item.id)}
|
|
255
|
+
className="border-none bg-transparent p-0 text-amber-300/70 hover:text-amber-200 cursor-pointer"
|
|
256
|
+
>
|
|
257
|
+
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
|
|
258
|
+
<line x1="18" y1="6" x2="6" y2="18" />
|
|
259
|
+
<line x1="6" y1="6" x2="18" y2="18" />
|
|
260
|
+
</svg>
|
|
261
|
+
</button>
|
|
262
|
+
</span>
|
|
209
263
|
))}
|
|
210
264
|
</div>
|
|
211
265
|
)}
|
|
212
266
|
|
|
267
|
+
{visibleQueuedMessages.length === 0 && !disabled && (
|
|
268
|
+
<div className="mb-2 flex items-center justify-between gap-2 rounded-[10px] border border-white/[0.06] bg-white/[0.03] px-3 py-2">
|
|
269
|
+
<span className="text-[11px] text-text-3">
|
|
270
|
+
{streaming
|
|
271
|
+
? 'Current round is still running. Press send to queue the next message.'
|
|
272
|
+
: agents.length > 0
|
|
273
|
+
? 'Use @AgentName or @all to direct the next reply.'
|
|
274
|
+
: 'Start the next round here.'}
|
|
275
|
+
</span>
|
|
276
|
+
<span className="text-[10px] text-text-3/50">Enter sends · Shift+Enter newline</span>
|
|
277
|
+
</div>
|
|
278
|
+
)}
|
|
279
|
+
|
|
213
280
|
{/* Reply preview banner */}
|
|
214
281
|
{replyingTo && (
|
|
215
282
|
<div className="flex items-center gap-2 mb-2 px-2 py-1.5 rounded-[8px] bg-white/[0.04] border border-white/[0.06]">
|
|
@@ -268,12 +335,12 @@ export function ChatroomInput({ agents, onSend, disabled }: Props) {
|
|
|
268
335
|
</svg>
|
|
269
336
|
</button>
|
|
270
337
|
|
|
271
|
-
<div className="flex-1 relative rounded-[
|
|
338
|
+
<div className="flex-1 relative rounded-[10px] bg-white/[0.06] border border-white/[0.08] focus-within:border-accent-bright/40">
|
|
272
339
|
{/* Highlight mirror — renders @mentions with accent background behind the transparent textarea */}
|
|
273
340
|
<div
|
|
274
341
|
aria-hidden
|
|
275
342
|
className="absolute inset-0 px-3 py-2 text-[13px] leading-[1.5] break-words whitespace-pre-wrap pointer-events-none overflow-hidden"
|
|
276
|
-
style={{ minHeight: '
|
|
343
|
+
style={{ minHeight: '44px', color: 'transparent' }}
|
|
277
344
|
>
|
|
278
345
|
{highlightedSegments}
|
|
279
346
|
</div>
|
|
@@ -286,21 +353,17 @@ export function ChatroomInput({ agents, onSend, disabled }: Props) {
|
|
|
286
353
|
placeholder="Type a message... Use @ to mention agents"
|
|
287
354
|
disabled={disabled}
|
|
288
355
|
rows={1}
|
|
289
|
-
className="w-full resize-none
|
|
290
|
-
style={{ minHeight: '
|
|
356
|
+
className="relative w-full resize-none rounded-[10px] border-none bg-transparent px-3 py-2.5 text-[13px] text-text placeholder:text-text-3 focus:outline-none max-h-[160px] disabled:opacity-50"
|
|
357
|
+
style={{ minHeight: '44px' }}
|
|
291
358
|
/>
|
|
292
359
|
</div>
|
|
293
360
|
<button
|
|
294
|
-
onClick={
|
|
295
|
-
if ((text.trim() || pendingFiles.length) && !disabled) {
|
|
296
|
-
onSend(text)
|
|
297
|
-
setText('')
|
|
298
|
-
if (chatroomId) safeStorageRemove(`sc_draft_cr_${chatroomId}`)
|
|
299
|
-
setShowMentions(false)
|
|
300
|
-
}
|
|
301
|
-
}}
|
|
361
|
+
onClick={handleSendCurrent}
|
|
302
362
|
disabled={(!text.trim() && !pendingFiles.length) || disabled}
|
|
303
|
-
className=
|
|
363
|
+
className={`shrink-0 w-10 h-10 rounded-[10px] flex items-center justify-center transition-all disabled:opacity-30 cursor-pointer ${
|
|
364
|
+
streaming ? 'bg-amber-500/20 hover:bg-amber-500/30' : 'bg-accent-bright hover:bg-accent-bright/90'
|
|
365
|
+
}`}
|
|
366
|
+
title="Send or queue message"
|
|
304
367
|
>
|
|
305
368
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
306
369
|
<line x1="22" y1="2" x2="11" y2="13" />
|
|
@@ -7,6 +7,14 @@ import { useWs } from '@/hooks/use-ws'
|
|
|
7
7
|
import type { Chatroom } from '@/types'
|
|
8
8
|
import { EmptyState } from '@/components/shared/empty-state'
|
|
9
9
|
|
|
10
|
+
function formatRoomTime(ts: number): string {
|
|
11
|
+
const diff = Date.now() - ts
|
|
12
|
+
if (diff < 60_000) return 'Now'
|
|
13
|
+
if (diff < 3_600_000) return `${Math.max(1, Math.floor(diff / 60_000))}m`
|
|
14
|
+
if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)}h`
|
|
15
|
+
return new Date(ts).toLocaleDateString(undefined, { month: 'short', day: 'numeric' })
|
|
16
|
+
}
|
|
17
|
+
|
|
10
18
|
export function ChatroomList() {
|
|
11
19
|
const chatrooms = useChatroomStore((s) => s.chatrooms)
|
|
12
20
|
const currentChatroomId = useChatroomStore((s) => s.currentChatroomId)
|
|
@@ -15,6 +23,9 @@ export function ChatroomList() {
|
|
|
15
23
|
const setChatroomSheetOpen = useChatroomStore((s) => s.setChatroomSheetOpen)
|
|
16
24
|
const setEditingChatroomId = useChatroomStore((s) => s.setEditingChatroomId)
|
|
17
25
|
const agents = useAppStore((s) => s.agents)
|
|
26
|
+
const lastReadTimestamps = useAppStore((s) => s.lastReadTimestamps)
|
|
27
|
+
const [filter, setFilter] = useState<'all' | 'active' | 'recent' | 'unread'>('all')
|
|
28
|
+
const [search, setSearch] = useState('')
|
|
18
29
|
|
|
19
30
|
const refresh = useCallback(() => {
|
|
20
31
|
loadChatrooms()
|
|
@@ -24,32 +35,56 @@ export function ChatroomList() {
|
|
|
24
35
|
useEffect(() => { refresh() }, [refresh])
|
|
25
36
|
useWs('chatrooms', refresh, 15_000)
|
|
26
37
|
|
|
27
|
-
// Auto-select the latest chatroom when none is selected
|
|
28
38
|
useEffect(() => {
|
|
29
39
|
if (currentChatroomId) return
|
|
30
40
|
const latest = Object.values(chatrooms).sort((a, b) => b.updatedAt - a.updatedAt)[0]
|
|
31
41
|
if (latest) setCurrentChatroom(latest.id)
|
|
32
42
|
}, [chatrooms, currentChatroomId, setCurrentChatroom])
|
|
33
43
|
|
|
34
|
-
const
|
|
44
|
+
const enriched = useMemo(() => (
|
|
45
|
+
Object.values(chatrooms)
|
|
46
|
+
.map((chatroom: Chatroom) => {
|
|
47
|
+
const memberNames = chatroom.agentIds
|
|
48
|
+
.map((id) => agents[id]?.name)
|
|
49
|
+
.filter(Boolean)
|
|
50
|
+
const lastMsg = chatroom.messages[chatroom.messages.length - 1]
|
|
51
|
+
const lastReadAt = lastReadTimestamps[chatroom.id] || 0
|
|
52
|
+
const unreadCount = chatroom.messages.filter(
|
|
53
|
+
(msg) => msg.senderId !== 'user' && msg.senderId !== 'system' && (msg.time || 0) > lastReadAt,
|
|
54
|
+
).length
|
|
35
55
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
56
|
+
return {
|
|
57
|
+
chatroom,
|
|
58
|
+
memberNames,
|
|
59
|
+
lastMsg,
|
|
60
|
+
unreadCount,
|
|
61
|
+
searchText: [
|
|
62
|
+
chatroom.name,
|
|
63
|
+
chatroom.description,
|
|
64
|
+
memberNames.join(' '),
|
|
65
|
+
lastMsg?.senderName,
|
|
66
|
+
lastMsg?.text,
|
|
67
|
+
].filter(Boolean).join(' ').toLowerCase(),
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
.sort((a, b) => b.chatroom.updatedAt - a.chatroom.updatedAt)
|
|
71
|
+
), [agents, chatrooms, lastReadTimestamps])
|
|
40
72
|
|
|
41
73
|
const filtered = useMemo(() => {
|
|
42
|
-
|
|
74
|
+
const query = search.trim().toLowerCase()
|
|
43
75
|
const now = Date.now()
|
|
44
|
-
return
|
|
45
|
-
if (
|
|
46
|
-
return now -
|
|
76
|
+
return enriched.filter((item) => {
|
|
77
|
+
if (query && !item.searchText.includes(query)) return false
|
|
78
|
+
if (filter === 'active') return now - item.chatroom.updatedAt < 3_600_000
|
|
79
|
+
if (filter === 'recent') return now - item.chatroom.updatedAt < 86_400_000
|
|
80
|
+
if (filter === 'unread') return item.unreadCount > 0
|
|
81
|
+
return true
|
|
47
82
|
})
|
|
48
|
-
}, [
|
|
83
|
+
}, [enriched, filter, search])
|
|
49
84
|
|
|
50
85
|
return (
|
|
51
86
|
<div className="flex-1 overflow-y-auto">
|
|
52
|
-
{
|
|
87
|
+
{enriched.length === 0 ? (
|
|
53
88
|
<EmptyState
|
|
54
89
|
icon={
|
|
55
90
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" className="text-accent-bright">
|
|
@@ -61,75 +96,109 @@ export function ChatroomList() {
|
|
|
61
96
|
action={{ label: '+ New Chatroom', onClick: () => { setEditingChatroomId(null); setChatroomSheetOpen(true) } }}
|
|
62
97
|
/>
|
|
63
98
|
) : (
|
|
64
|
-
<div className="p-3 space-y-
|
|
65
|
-
|
|
66
|
-
<
|
|
67
|
-
|
|
99
|
+
<div className="p-3 space-y-3">
|
|
100
|
+
<div className="space-y-2">
|
|
101
|
+
<input
|
|
102
|
+
type="text"
|
|
103
|
+
value={search}
|
|
104
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
105
|
+
placeholder="Search rooms, members, or recent messages..."
|
|
106
|
+
className="w-full rounded-[12px] border border-white/[0.06] bg-surface px-3 py-2.5 text-[13px] text-text placeholder:text-text-3/70 focus:outline-none focus:border-accent-bright/35"
|
|
107
|
+
/>
|
|
108
|
+
<div className="flex flex-wrap items-center gap-1">
|
|
109
|
+
{(['all', 'active', 'recent', 'unread'] as const).map((value) => (
|
|
68
110
|
<button
|
|
69
|
-
key={
|
|
111
|
+
key={value}
|
|
70
112
|
type="button"
|
|
71
|
-
onClick={() => setFilter(
|
|
72
|
-
data-active={filter ===
|
|
73
|
-
className="px-3 py-1.5
|
|
113
|
+
onClick={() => setFilter(value)}
|
|
114
|
+
data-active={filter === value || undefined}
|
|
115
|
+
className="rounded-[8px] border-none px-3 py-1.5 text-[11px] font-600 capitalize cursor-pointer transition-all focus-visible:ring-1 focus-visible:ring-accent-bright/50
|
|
74
116
|
data-[active]:bg-accent-soft data-[active]:text-accent-bright
|
|
75
117
|
bg-transparent text-text-3 hover:text-text-2 hover:bg-white/[0.04]"
|
|
76
118
|
>
|
|
77
|
-
{
|
|
119
|
+
{value}
|
|
78
120
|
</button>
|
|
79
121
|
))}
|
|
122
|
+
<span className="ml-auto text-[11px] text-text-3/55">
|
|
123
|
+
{filtered.length} room{filtered.length === 1 ? '' : 's'}
|
|
124
|
+
</span>
|
|
80
125
|
</div>
|
|
81
|
-
|
|
82
|
-
{filtered.map((chatroom, idx) => {
|
|
83
|
-
const isActive = chatroom.id === currentChatroomId
|
|
84
|
-
const memberNames = chatroom.agentIds
|
|
85
|
-
.map((id) => agents[id]?.name)
|
|
86
|
-
.filter(Boolean)
|
|
87
|
-
.slice(0, 3)
|
|
88
|
-
const lastMsg = chatroom.messages[chatroom.messages.length - 1]
|
|
126
|
+
</div>
|
|
89
127
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
128
|
+
{filtered.length === 0 ? (
|
|
129
|
+
<div className="rounded-[14px] border border-white/[0.06] bg-white/[0.02] px-4 py-8 text-center">
|
|
130
|
+
<div className="text-[13px] font-600 text-text-2">No rooms match this view</div>
|
|
131
|
+
<div className="mt-1 text-[12px] text-text-3/65">
|
|
132
|
+
Clear the search or switch filters to see more chatrooms.
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
) : (
|
|
136
|
+
<div className="space-y-1">
|
|
137
|
+
{filtered.map(({ chatroom, memberNames, lastMsg, unreadCount }, idx) => {
|
|
138
|
+
const isActive = chatroom.id === currentChatroomId
|
|
139
|
+
return (
|
|
140
|
+
<button
|
|
141
|
+
key={chatroom.id}
|
|
142
|
+
onClick={() => setCurrentChatroom(chatroom.id)}
|
|
143
|
+
className={`relative w-full overflow-hidden rounded-[14px] border px-4 py-3.5 text-left transition-all cursor-pointer ${
|
|
144
|
+
isActive
|
|
145
|
+
? 'border-accent-bright/20 bg-accent-soft/55'
|
|
146
|
+
: 'border-transparent hover:bg-white/[0.04] hover:border-white/[0.05]'
|
|
147
|
+
}`}
|
|
148
|
+
style={{
|
|
149
|
+
animation: 'fade-up 0.4s var(--ease-spring) both',
|
|
150
|
+
animationDelay: `${idx * 0.03}s`,
|
|
151
|
+
}}
|
|
152
|
+
>
|
|
153
|
+
{isActive && (
|
|
154
|
+
<div className="absolute inset-y-3 left-0 w-1 rounded-r-full bg-accent-bright" />
|
|
155
|
+
)}
|
|
156
|
+
<div className="flex items-start gap-3">
|
|
157
|
+
<div className="mt-0.5 flex h-8 w-8 items-center justify-center rounded-full bg-accent-soft shrink-0">
|
|
158
|
+
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-accent-bright">
|
|
159
|
+
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
|
|
160
|
+
</svg>
|
|
161
|
+
</div>
|
|
162
|
+
<div className="min-w-0 flex-1">
|
|
163
|
+
<div className="flex items-center gap-2">
|
|
164
|
+
<span className={`truncate text-[13px] font-700 ${isActive ? 'text-accent-bright' : 'text-text'}`}>
|
|
165
|
+
{chatroom.name}
|
|
166
|
+
</span>
|
|
167
|
+
{unreadCount > 0 && (
|
|
168
|
+
<span className="inline-flex min-w-[18px] items-center justify-center rounded-full bg-accent-bright px-1.5 py-0.5 text-[10px] font-700 text-white">
|
|
169
|
+
{unreadCount > 99 ? '99+' : unreadCount}
|
|
170
|
+
</span>
|
|
171
|
+
)}
|
|
172
|
+
<span className="ml-auto shrink-0 text-[10px] font-mono text-text-3/55">
|
|
173
|
+
{formatRoomTime(lastMsg?.time || chatroom.updatedAt)}
|
|
174
|
+
</span>
|
|
175
|
+
</div>
|
|
176
|
+
<div className="mt-0.5 flex flex-wrap items-center gap-2 text-[11px] text-text-3">
|
|
177
|
+
<span>{chatroom.agentIds.length} agent{chatroom.agentIds.length === 1 ? '' : 's'}</span>
|
|
178
|
+
{chatroom.chatMode === 'parallel' && (
|
|
179
|
+
<span className="rounded-[6px] bg-sky-500/10 px-1.5 py-0.5 text-sky-300">Parallel</span>
|
|
180
|
+
)}
|
|
181
|
+
{chatroom.autoAddress && (
|
|
182
|
+
<span className="rounded-[6px] bg-emerald-500/10 px-1.5 py-0.5 text-emerald-300">Auto-address</span>
|
|
183
|
+
)}
|
|
184
|
+
</div>
|
|
185
|
+
{memberNames.length > 0 && (
|
|
186
|
+
<p className="mt-1 truncate text-[11px] text-text-3/80">
|
|
187
|
+
{memberNames.slice(0, 3).join(', ')}{memberNames.length > 3 ? ` +${memberNames.length - 3}` : ''}
|
|
188
|
+
</p>
|
|
189
|
+
)}
|
|
190
|
+
{lastMsg && (
|
|
191
|
+
<p className="mt-1 truncate text-[11px] text-text-3/65">
|
|
192
|
+
{lastMsg.senderName}: {lastMsg.text.slice(0, 72)}
|
|
193
|
+
</p>
|
|
194
|
+
)}
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
</button>
|
|
198
|
+
)
|
|
199
|
+
})}
|
|
200
|
+
</div>
|
|
201
|
+
)}
|
|
133
202
|
</div>
|
|
134
203
|
)}
|
|
135
204
|
</div>
|
|
@@ -393,12 +393,12 @@ export function ChatroomMessageBubble({ message, agents, onToggleReaction, onRep
|
|
|
393
393
|
</div>
|
|
394
394
|
|
|
395
395
|
{/* Action buttons (reply + pin + transfer + moderate + reaction) */}
|
|
396
|
-
<div className="relative shrink-0 mt-0.5 flex items-start gap-
|
|
396
|
+
<div className="relative shrink-0 mt-0.5 flex items-start gap-1" style={{ zIndex: showPicker || showTransferPicker || showModMenu ? 50 : undefined }}>
|
|
397
397
|
{/* Reply button */}
|
|
398
398
|
{onReply && (
|
|
399
399
|
<button
|
|
400
400
|
onClick={() => onReply(message)}
|
|
401
|
-
className="
|
|
401
|
+
className="w-7 h-7 rounded-[8px] border border-white/[0.06] bg-white/[0.02] flex items-center justify-center hover:bg-white/[0.08] transition-all cursor-pointer"
|
|
402
402
|
title="Reply"
|
|
403
403
|
>
|
|
404
404
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-text-3">
|
|
@@ -411,7 +411,7 @@ export function ChatroomMessageBubble({ message, agents, onToggleReaction, onRep
|
|
|
411
411
|
{onTogglePin && (
|
|
412
412
|
<button
|
|
413
413
|
onClick={() => onTogglePin(message.id)}
|
|
414
|
-
className="
|
|
414
|
+
className="w-7 h-7 rounded-[8px] border border-white/[0.06] bg-white/[0.02] flex items-center justify-center hover:bg-white/[0.08] transition-all cursor-pointer"
|
|
415
415
|
title={pinnedMessageIds?.includes(message.id) ? 'Unpin message' : 'Pin message'}
|
|
416
416
|
>
|
|
417
417
|
<svg width="12" height="12" viewBox="0 0 24 24" fill={pinnedMessageIds?.includes(message.id) ? 'currentColor' : 'none'} stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={pinnedMessageIds?.includes(message.id) ? 'text-amber-400' : 'text-text-3'}>
|
|
@@ -424,7 +424,7 @@ export function ChatroomMessageBubble({ message, agents, onToggleReaction, onRep
|
|
|
424
424
|
{onTransfer && !isUser && (
|
|
425
425
|
<button
|
|
426
426
|
onClick={() => setShowTransferPicker(!showTransferPicker)}
|
|
427
|
-
className="
|
|
427
|
+
className="w-7 h-7 rounded-[8px] border border-white/[0.06] bg-white/[0.02] flex items-center justify-center hover:bg-white/[0.08] transition-all cursor-pointer"
|
|
428
428
|
title="Transfer to agent"
|
|
429
429
|
>
|
|
430
430
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-text-3">
|
|
@@ -449,7 +449,7 @@ export function ChatroomMessageBubble({ message, agents, onToggleReaction, onRep
|
|
|
449
449
|
{!isUser && (onDeleteMessage || onMuteAgent || onSetRole) && (
|
|
450
450
|
<button
|
|
451
451
|
onClick={() => setShowModMenu(!showModMenu)}
|
|
452
|
-
className="
|
|
452
|
+
className="w-7 h-7 rounded-[8px] border border-white/[0.06] bg-white/[0.02] flex items-center justify-center hover:bg-white/[0.08] transition-all cursor-pointer"
|
|
453
453
|
title="Moderate"
|
|
454
454
|
>
|
|
455
455
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-text-3">
|
|
@@ -550,7 +550,8 @@ export function ChatroomMessageBubble({ message, agents, onToggleReaction, onRep
|
|
|
550
550
|
{/* Reaction button */}
|
|
551
551
|
<button
|
|
552
552
|
onClick={() => setShowPicker(!showPicker)}
|
|
553
|
-
className="
|
|
553
|
+
className="w-7 h-7 rounded-[8px] border border-white/[0.06] bg-white/[0.02] flex items-center justify-center hover:bg-white/[0.08] transition-all cursor-pointer"
|
|
554
|
+
title="Add reaction"
|
|
554
555
|
>
|
|
555
556
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-text-3">
|
|
556
557
|
<circle cx="12" cy="12" r="10" />
|
|
@@ -190,6 +190,10 @@ export function ChatroomSheet() {
|
|
|
190
190
|
|
|
191
191
|
const handleSave = async () => {
|
|
192
192
|
if (!name.trim() || saving) return
|
|
193
|
+
if (selectedAgentIds.length === 0) {
|
|
194
|
+
toast.error('Select at least one chatroom member.')
|
|
195
|
+
return
|
|
196
|
+
}
|
|
193
197
|
setSaving(true)
|
|
194
198
|
try {
|
|
195
199
|
const payload = {
|
|
@@ -363,6 +367,9 @@ export function ChatroomSheet() {
|
|
|
363
367
|
<label className="block text-[12px] font-600 text-text-2 mb-1.5">
|
|
364
368
|
Members ({selectedAgentIds.length} selected)
|
|
365
369
|
</label>
|
|
370
|
+
<p className="mb-2 text-[11px] text-text-3">
|
|
371
|
+
Choose the agents who should be available in this room. Every chatroom needs at least one member.
|
|
372
|
+
</p>
|
|
366
373
|
<div className="max-h-[240px] overflow-y-auto rounded-[8px] border border-white/[0.08] bg-white/[0.03]">
|
|
367
374
|
{agentList.length === 0 ? (
|
|
368
375
|
<p className="p-3 text-[12px] text-text-3">No agents available</p>
|
|
@@ -387,6 +394,11 @@ export function ChatroomSheet() {
|
|
|
387
394
|
})
|
|
388
395
|
)}
|
|
389
396
|
</div>
|
|
397
|
+
{selectedAgentIds.length === 0 && (
|
|
398
|
+
<p className="mt-2 text-[11px] text-amber-300">
|
|
399
|
+
Select at least one member before creating the room.
|
|
400
|
+
</p>
|
|
401
|
+
)}
|
|
390
402
|
</div>
|
|
391
403
|
|
|
392
404
|
{/* Routing Rules */}
|
|
@@ -484,7 +496,7 @@ export function ChatroomSheet() {
|
|
|
484
496
|
<div className="flex items-center gap-3 mt-6">
|
|
485
497
|
<button
|
|
486
498
|
onClick={handleSave}
|
|
487
|
-
disabled={!name.trim() || saving}
|
|
499
|
+
disabled={!name.trim() || saving || selectedAgentIds.length === 0}
|
|
488
500
|
className="flex-1 py-2.5 rounded-[8px] text-[13px] font-600 bg-accent-bright text-white hover:bg-accent-bright/90 transition-all disabled:opacity-50 cursor-pointer"
|
|
489
501
|
>
|
|
490
502
|
{saving ? 'Saving...' : editing ? 'Save Changes' : 'Create Chatroom'}
|
|
@@ -103,6 +103,9 @@ export function ChatroomToolRequestBanner({ agentId, agentName, text, toolOutput
|
|
|
103
103
|
<span className="text-accent-bright">{agentName}</span> requesting <span className="text-amber-400">{label}</span>
|
|
104
104
|
</p>
|
|
105
105
|
{reason && <p className="text-[11px] text-text-3/60 mt-0.5 truncate">{reason}</p>}
|
|
106
|
+
<p className="text-[10px] text-text-3/45 mt-1">
|
|
107
|
+
Approving updates this agent's tool access and posts a follow-up continue message in the room.
|
|
108
|
+
</p>
|
|
106
109
|
</div>
|
|
107
110
|
{isGranted ? (
|
|
108
111
|
<span className="text-[11px] text-emerald-400 font-600 shrink-0">Granted</span>
|
|
@@ -115,14 +118,14 @@ export function ChatroomToolRequestBanner({ agentId, agentName, text, toolOutput
|
|
|
115
118
|
className="px-3 py-1.5 rounded-[8px] bg-amber-500/20 hover:bg-amber-500/30 text-amber-300 text-[11px] font-600 border-none cursor-pointer transition-colors"
|
|
116
119
|
style={{ fontFamily: 'inherit' }}
|
|
117
120
|
>
|
|
118
|
-
Grant
|
|
121
|
+
Grant & Continue
|
|
119
122
|
</button>
|
|
120
123
|
<button
|
|
121
124
|
onClick={() => handleDeny(toolId)}
|
|
122
125
|
className="px-3 py-1.5 rounded-[8px] bg-red-500/15 hover:bg-red-500/25 text-red-400 text-[11px] font-600 border-none cursor-pointer transition-colors"
|
|
123
126
|
style={{ fontFamily: 'inherit' }}
|
|
124
127
|
>
|
|
125
|
-
Deny
|
|
128
|
+
Deny & Reply
|
|
126
129
|
</button>
|
|
127
130
|
</div>
|
|
128
131
|
)}
|