@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
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useEffect, useRef, useCallback, useState, useMemo } from 'react'
|
|
4
4
|
import { useChatroomStore } from '@/stores/use-chatroom-store'
|
|
5
|
+
import type { StreamingAgent } from '@/stores/use-chatroom-store'
|
|
5
6
|
import { useAppStore } from '@/stores/use-app-store'
|
|
6
7
|
import { useWs } from '@/hooks/use-ws'
|
|
7
8
|
import { ChatroomMessageBubble } from './chatroom-message'
|
|
@@ -10,6 +11,7 @@ import { ChatroomTypingBar } from './chatroom-typing-bar'
|
|
|
10
11
|
import { AgentAvatar } from '@/components/agents/agent-avatar'
|
|
11
12
|
import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip'
|
|
12
13
|
import { HeartbeatMoment, ActivityMoment, isNotableTool } from '@/components/chat/activity-moment'
|
|
14
|
+
import { BottomSheet } from '@/components/shared/bottom-sheet'
|
|
13
15
|
import type { Chatroom, ChatroomMessage, ChatroomMember, Agent } from '@/types'
|
|
14
16
|
|
|
15
17
|
function navigateToAgent(agentId: string) {
|
|
@@ -50,16 +52,21 @@ function useAgentHeartbeat(agentId: string, onPulse: (id: string) => void) {
|
|
|
50
52
|
useWs(topic, () => onPulseRef.current(agentId))
|
|
51
53
|
}
|
|
52
54
|
|
|
53
|
-
function
|
|
54
|
-
useAgentHeartbeat(
|
|
55
|
-
useAgentHeartbeat(agentIds[1] || '', onPulse)
|
|
56
|
-
useAgentHeartbeat(agentIds[2] || '', onPulse)
|
|
57
|
-
useAgentHeartbeat(agentIds[3] || '', onPulse)
|
|
58
|
-
useAgentHeartbeat(agentIds[4] || '', onPulse)
|
|
59
|
-
useAgentHeartbeat(agentIds[5] || '', onPulse)
|
|
55
|
+
function AgentHeartbeatListener({ agentId, onPulse }: { agentId: string; onPulse: (id: string) => void }) {
|
|
56
|
+
useAgentHeartbeat(agentId, onPulse)
|
|
60
57
|
return null
|
|
61
58
|
}
|
|
62
59
|
|
|
60
|
+
function AgentHeartbeatListeners({ agentIds, onPulse }: { agentIds: string[]; onPulse: (id: string) => void }) {
|
|
61
|
+
return (
|
|
62
|
+
<>
|
|
63
|
+
{agentIds.map((agentId) => (
|
|
64
|
+
<AgentHeartbeatListener key={agentId} agentId={agentId} onPulse={onPulse} />
|
|
65
|
+
))}
|
|
66
|
+
</>
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
63
70
|
const GROUP_THRESHOLD_MS = 2 * 60 * 1000
|
|
64
71
|
|
|
65
72
|
function dayLabel(ts: number): string {
|
|
@@ -76,7 +83,6 @@ function dayLabel(ts: number): string {
|
|
|
76
83
|
export function ChatroomView() {
|
|
77
84
|
const currentChatroomId = useChatroomStore((s) => s.currentChatroomId)
|
|
78
85
|
const chatrooms = useChatroomStore((s) => s.chatrooms)
|
|
79
|
-
const streaming = useChatroomStore((s) => s.streaming)
|
|
80
86
|
const streamingAgents = useChatroomStore((s) => s.streamingAgents)
|
|
81
87
|
const sendMessage = useChatroomStore((s) => s.sendMessage)
|
|
82
88
|
const toggleReaction = useChatroomStore((s) => s.toggleReaction)
|
|
@@ -96,6 +102,7 @@ export function ChatroomView() {
|
|
|
96
102
|
const [pinsExpanded, setPinsExpanded] = useState(false)
|
|
97
103
|
const [isNearBottom, setIsNearBottom] = useState(true)
|
|
98
104
|
const [agentMoments, setAgentMoments] = useState<Record<string, MomentType>>({})
|
|
105
|
+
const [detailsOpen, setDetailsOpen] = useState(false)
|
|
99
106
|
|
|
100
107
|
const handleHeartbeatPulse = useCallback((agentId: string) => {
|
|
101
108
|
setAgentMoments((prev) => ({ ...prev, [agentId]: { kind: 'heartbeat' } }))
|
|
@@ -150,13 +157,14 @@ export function ChatroomView() {
|
|
|
150
157
|
), [agents, chatroom])
|
|
151
158
|
|
|
152
159
|
const streamingAgentIds = useMemo(() => new Set(streamingAgents.keys()), [streamingAgents])
|
|
153
|
-
const
|
|
160
|
+
const chatroomId = chatroom?.id || null
|
|
161
|
+
const pinnedIds = useMemo(() => chatroom?.pinnedMessageIds ?? [], [chatroom?.pinnedMessageIds])
|
|
154
162
|
const pinnedMessages = useMemo(() => (
|
|
155
163
|
chatroom
|
|
156
164
|
? (pinnedIds.map((pid) => chatroom.messages.find((m) => m.id === pid)).filter(Boolean) as ChatroomMessage[])
|
|
157
165
|
: []
|
|
158
166
|
), [chatroom, pinnedIds])
|
|
159
|
-
const memberAgentIds = chatroom?.agentIds
|
|
167
|
+
const memberAgentIds = chatroom?.agentIds || []
|
|
160
168
|
const mutedCount = chatroom ? chatroom.agentIds.filter((agentId) => isAgentMuted(chatroom, agentId)).length : 0
|
|
161
169
|
const adminCount = chatroom ? chatroom.agentIds.filter((agentId) => getMemberRole(chatroom, agentId) === 'admin').length : 0
|
|
162
170
|
const lastReadAt = chatroom ? (lastReadTimestamps[chatroom.id] || 0) : 0
|
|
@@ -183,22 +191,26 @@ export function ChatroomView() {
|
|
|
183
191
|
}, [chatroom, markChatRead])
|
|
184
192
|
|
|
185
193
|
useEffect(() => {
|
|
186
|
-
if (!
|
|
187
|
-
markChatRead(
|
|
188
|
-
}, [
|
|
194
|
+
if (!chatroomId) return
|
|
195
|
+
markChatRead(chatroomId)
|
|
196
|
+
}, [chatroomId, markChatRead])
|
|
197
|
+
|
|
198
|
+
useEffect(() => {
|
|
199
|
+
setDetailsOpen(false)
|
|
200
|
+
}, [chatroomId])
|
|
189
201
|
|
|
190
202
|
useEffect(() => {
|
|
191
203
|
const node = scrollRef.current
|
|
192
|
-
if (!node || !
|
|
204
|
+
if (!node || !chatroomId) return
|
|
193
205
|
const handleScroll = () => {
|
|
194
206
|
const nearBottom = node.scrollHeight - node.scrollTop - node.clientHeight < 120
|
|
195
207
|
setIsNearBottom(nearBottom)
|
|
196
|
-
if (nearBottom) markChatRead(
|
|
208
|
+
if (nearBottom) markChatRead(chatroomId)
|
|
197
209
|
}
|
|
198
210
|
handleScroll()
|
|
199
211
|
node.addEventListener('scroll', handleScroll)
|
|
200
212
|
return () => node.removeEventListener('scroll', handleScroll)
|
|
201
|
-
}, [
|
|
213
|
+
}, [chatroomId, markChatRead])
|
|
202
214
|
|
|
203
215
|
useEffect(() => {
|
|
204
216
|
if (chatroom && isNearBottom) {
|
|
@@ -298,6 +310,14 @@ export function ChatroomView() {
|
|
|
298
310
|
)}
|
|
299
311
|
</div>
|
|
300
312
|
|
|
313
|
+
<button
|
|
314
|
+
type="button"
|
|
315
|
+
onClick={() => setDetailsOpen(true)}
|
|
316
|
+
className="xl:hidden shrink-0 rounded-[9px] border border-white/[0.08] bg-white/[0.03] px-2.5 py-1.5 text-[11px] font-600 text-text-2 hover:bg-white/[0.06] cursor-pointer transition-colors"
|
|
317
|
+
>
|
|
318
|
+
Details
|
|
319
|
+
</button>
|
|
320
|
+
|
|
301
321
|
<button
|
|
302
322
|
onClick={() => {
|
|
303
323
|
setEditingChatroomId(chatroom.id)
|
|
@@ -434,98 +454,149 @@ export function ChatroomView() {
|
|
|
434
454
|
<ChatroomInput
|
|
435
455
|
agents={memberAgents}
|
|
436
456
|
onSend={sendMessage}
|
|
437
|
-
disabled={streaming}
|
|
438
457
|
/>
|
|
439
458
|
</div>
|
|
440
459
|
|
|
441
460
|
<aside className="hidden xl:flex xl:w-[300px] xl:flex-col xl:border-l xl:border-white/[0.06] bg-surface/30">
|
|
442
|
-
<
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
<div className={`text-[18px] font-display font-700 tracking-[-0.02em] ${item.tone}`}>{item.value}</div>
|
|
453
|
-
<div className="text-[10px] text-text-3/50 uppercase tracking-[0.08em] mt-0.5">{item.label}</div>
|
|
454
|
-
</div>
|
|
455
|
-
))}
|
|
456
|
-
</div>
|
|
457
|
-
<div className="mt-3 space-y-1 text-[11px] text-text-3/65">
|
|
458
|
-
<div>Mode: {chatroom.chatMode === 'parallel' ? 'Parallel replies' : 'Sequential replies'}</div>
|
|
459
|
-
<div>Auto-address: {chatroom.autoAddress ? 'Enabled' : 'Off'}</div>
|
|
460
|
-
<div>Admins: {adminCount}</div>
|
|
461
|
-
</div>
|
|
462
|
-
</div>
|
|
461
|
+
<RoomDetailsPanel
|
|
462
|
+
chatroom={chatroom}
|
|
463
|
+
memberAgents={memberAgents}
|
|
464
|
+
streamingAgents={streamingAgents}
|
|
465
|
+
pinnedMessages={pinnedMessages}
|
|
466
|
+
mutedCount={mutedCount}
|
|
467
|
+
adminCount={adminCount}
|
|
468
|
+
onFocusMessage={focusMessage}
|
|
469
|
+
/>
|
|
470
|
+
</aside>
|
|
463
471
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
472
|
+
<BottomSheet open={detailsOpen} onClose={() => setDetailsOpen(false)}>
|
|
473
|
+
<RoomDetailsPanel
|
|
474
|
+
chatroom={chatroom}
|
|
475
|
+
memberAgents={memberAgents}
|
|
476
|
+
streamingAgents={streamingAgents}
|
|
477
|
+
pinnedMessages={pinnedMessages}
|
|
478
|
+
mutedCount={mutedCount}
|
|
479
|
+
adminCount={adminCount}
|
|
480
|
+
onFocusMessage={(messageId) => {
|
|
481
|
+
setDetailsOpen(false)
|
|
482
|
+
setTimeout(() => focusMessage(messageId), 50)
|
|
483
|
+
}}
|
|
484
|
+
compact
|
|
485
|
+
/>
|
|
486
|
+
</BottomSheet>
|
|
487
|
+
</div>
|
|
488
|
+
)
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function RoomDetailsPanel({
|
|
492
|
+
chatroom,
|
|
493
|
+
memberAgents,
|
|
494
|
+
streamingAgents,
|
|
495
|
+
pinnedMessages,
|
|
496
|
+
mutedCount,
|
|
497
|
+
adminCount,
|
|
498
|
+
onFocusMessage,
|
|
499
|
+
compact = false,
|
|
500
|
+
}: {
|
|
501
|
+
chatroom: Chatroom
|
|
502
|
+
memberAgents: Agent[]
|
|
503
|
+
streamingAgents: Map<string, StreamingAgent>
|
|
504
|
+
pinnedMessages: ChatroomMessage[]
|
|
505
|
+
mutedCount: number
|
|
506
|
+
adminCount: number
|
|
507
|
+
onFocusMessage: (messageId: string) => void
|
|
508
|
+
compact?: boolean
|
|
509
|
+
}) {
|
|
510
|
+
return (
|
|
511
|
+
<div className={`flex flex-col ${compact ? 'gap-5' : 'h-full'}`}>
|
|
512
|
+
<div className={compact ? '' : 'border-b border-white/[0.06] px-4 py-4'}>
|
|
513
|
+
<h3 className="text-[12px] font-700 uppercase tracking-[0.08em] text-text-3/60">Room Status</h3>
|
|
514
|
+
<div className="mt-3 grid grid-cols-2 gap-2">
|
|
515
|
+
{[
|
|
516
|
+
{ label: 'Members', value: String(memberAgents.length), tone: 'text-text' },
|
|
517
|
+
{ label: 'Active', value: String(streamingAgents.size), tone: 'text-sky-400' },
|
|
518
|
+
{ label: 'Pinned', value: String(pinnedMessages.length), tone: 'text-amber-400' },
|
|
519
|
+
{ label: 'Muted', value: String(mutedCount), tone: 'text-rose-400' },
|
|
520
|
+
].map((item) => (
|
|
521
|
+
<div key={item.label} className="rounded-[10px] border border-white/[0.06] bg-white/[0.02] px-3 py-2.5">
|
|
522
|
+
<div className={`text-[18px] font-display font-700 tracking-[-0.02em] ${item.tone}`}>{item.value}</div>
|
|
523
|
+
<div className="mt-0.5 text-[10px] uppercase tracking-[0.08em] text-text-3/50">{item.label}</div>
|
|
469
524
|
</div>
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
525
|
+
))}
|
|
526
|
+
</div>
|
|
527
|
+
<div className="mt-3 space-y-1 text-[11px] text-text-3/65">
|
|
528
|
+
<div>Mode: {chatroom.chatMode === 'parallel' ? 'Parallel replies' : 'Sequential replies'}</div>
|
|
529
|
+
<div>Auto-address: {chatroom.autoAddress ? 'Enabled' : 'Off'}</div>
|
|
530
|
+
<div>Admins: {adminCount}</div>
|
|
531
|
+
</div>
|
|
532
|
+
</div>
|
|
533
|
+
|
|
534
|
+
<div className={compact ? 'space-y-4' : 'flex-1 overflow-y-auto px-4 py-4 space-y-4'}>
|
|
535
|
+
<section>
|
|
536
|
+
<div className="mb-2 flex items-center justify-between">
|
|
537
|
+
<h4 className="text-[12px] font-700 uppercase tracking-[0.08em] text-text-3/60">Members</h4>
|
|
538
|
+
<span className="text-[11px] text-text-3/40">{memberAgents.length}</span>
|
|
539
|
+
</div>
|
|
540
|
+
<div className="space-y-2">
|
|
541
|
+
{memberAgents.map((agent) => {
|
|
542
|
+
const role = getMemberRole(chatroom, agent.id)
|
|
543
|
+
const muted = isAgentMuted(chatroom, agent.id)
|
|
544
|
+
return (
|
|
545
|
+
<button
|
|
546
|
+
key={agent.id}
|
|
547
|
+
onClick={() => navigateToAgent(agent.id)}
|
|
548
|
+
className="w-full rounded-[12px] border border-white/[0.06] bg-white/[0.02] px-3 py-2.5 text-left hover:bg-white/[0.05] transition-all cursor-pointer"
|
|
549
|
+
style={{ fontFamily: 'inherit' }}
|
|
550
|
+
>
|
|
551
|
+
<div className="flex items-center gap-3">
|
|
481
552
|
<AgentAvatar seed={agent.avatarSeed} avatarUrl={agent.avatarUrl} name={agent.name} size={26} status={streamingAgents.has(agent.id) ? 'busy' : 'online'} />
|
|
482
553
|
<div className="min-w-0 flex-1">
|
|
483
|
-
<div className="text-[12px] font-600 text-text
|
|
484
|
-
<div className="flex flex-wrap gap-1.5
|
|
485
|
-
<span className="
|
|
554
|
+
<div className="truncate text-[12px] font-600 text-text">{agent.name}</div>
|
|
555
|
+
<div className="mt-1 flex flex-wrap gap-1.5">
|
|
556
|
+
<span className="rounded-[5px] bg-white/[0.04] px-1.5 py-0.5 text-[10px] font-700 uppercase tracking-[0.08em] text-text-3/70">
|
|
486
557
|
{role}
|
|
487
558
|
</span>
|
|
488
559
|
{muted && (
|
|
489
|
-
<span className="
|
|
560
|
+
<span className="rounded-[5px] bg-rose-500/10 px-1.5 py-0.5 text-[10px] font-700 uppercase tracking-[0.08em] text-rose-400">
|
|
490
561
|
Muted
|
|
491
562
|
</span>
|
|
492
563
|
)}
|
|
493
564
|
{streamingAgents.has(agent.id) && (
|
|
494
|
-
<span className="
|
|
565
|
+
<span className="rounded-[5px] bg-sky-500/10 px-1.5 py-0.5 text-[10px] font-700 uppercase tracking-[0.08em] text-sky-400">
|
|
495
566
|
Active
|
|
496
567
|
</span>
|
|
497
568
|
)}
|
|
498
569
|
</div>
|
|
499
570
|
</div>
|
|
500
|
-
</
|
|
501
|
-
|
|
502
|
-
|
|
571
|
+
</div>
|
|
572
|
+
</button>
|
|
573
|
+
)
|
|
574
|
+
})}
|
|
575
|
+
</div>
|
|
576
|
+
</section>
|
|
577
|
+
|
|
578
|
+
{pinnedMessages.length > 0 && (
|
|
579
|
+
<section>
|
|
580
|
+
<div className="mb-2 flex items-center justify-between">
|
|
581
|
+
<h4 className="text-[12px] font-700 uppercase tracking-[0.08em] text-text-3/60">Pinned</h4>
|
|
582
|
+
<span className="text-[11px] text-text-3/40">{pinnedMessages.length}</span>
|
|
583
|
+
</div>
|
|
584
|
+
<div className="space-y-2">
|
|
585
|
+
{pinnedMessages.slice(0, compact ? pinnedMessages.length : 4).map((message) => (
|
|
586
|
+
<button
|
|
587
|
+
key={message.id}
|
|
588
|
+
onClick={() => onFocusMessage(message.id)}
|
|
589
|
+
className="w-full rounded-[12px] border border-white/[0.06] bg-white/[0.02] px-3 py-2.5 text-left hover:bg-white/[0.05] transition-all cursor-pointer"
|
|
590
|
+
style={{ fontFamily: 'inherit' }}
|
|
591
|
+
>
|
|
592
|
+
<div className="text-[11px] font-700 text-accent-bright">{message.senderName}</div>
|
|
593
|
+
<div className="mt-1 line-clamp-2 text-[12px] text-text-3">{message.text}</div>
|
|
594
|
+
</button>
|
|
595
|
+
))}
|
|
503
596
|
</div>
|
|
504
597
|
</section>
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
<section>
|
|
508
|
-
<div className="flex items-center justify-between mb-2">
|
|
509
|
-
<h4 className="text-[12px] font-700 uppercase tracking-[0.08em] text-text-3/60">Pinned</h4>
|
|
510
|
-
<span className="text-[11px] text-text-3/40">{pinnedMessages.length}</span>
|
|
511
|
-
</div>
|
|
512
|
-
<div className="space-y-2">
|
|
513
|
-
{pinnedMessages.slice(0, 4).map((message) => (
|
|
514
|
-
<button
|
|
515
|
-
key={message.id}
|
|
516
|
-
onClick={() => focusMessage(message.id)}
|
|
517
|
-
className="w-full rounded-[12px] border border-white/[0.06] bg-white/[0.02] px-3 py-2.5 text-left hover:bg-white/[0.05] transition-all cursor-pointer"
|
|
518
|
-
style={{ fontFamily: 'inherit' }}
|
|
519
|
-
>
|
|
520
|
-
<div className="text-[11px] font-700 text-accent-bright">{message.senderName}</div>
|
|
521
|
-
<div className="text-[12px] text-text-3 mt-1 line-clamp-2">{message.text}</div>
|
|
522
|
-
</button>
|
|
523
|
-
))}
|
|
524
|
-
</div>
|
|
525
|
-
</section>
|
|
526
|
-
)}
|
|
527
|
-
</div>
|
|
528
|
-
</aside>
|
|
598
|
+
)}
|
|
599
|
+
</div>
|
|
529
600
|
</div>
|
|
530
601
|
)
|
|
531
602
|
}
|
|
@@ -164,39 +164,33 @@ export function ReactionPicker({ onSelect, onClose }: Props) {
|
|
|
164
164
|
const filteredEmojis = useMemo(() => {
|
|
165
165
|
if (!search.trim()) return null
|
|
166
166
|
const q = search.toLowerCase()
|
|
167
|
-
|
|
168
|
-
const results: string[] = []
|
|
167
|
+
const directEmojiMatches: string[] = []
|
|
169
168
|
const seen = new Set<string>()
|
|
170
169
|
for (const cat of CATEGORIES) {
|
|
171
170
|
if (cat.id === 'frequent') continue
|
|
172
171
|
for (const emoji of cat.emojis) {
|
|
173
|
-
if (
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
172
|
+
if (seen.has(emoji)) continue
|
|
173
|
+
seen.add(emoji)
|
|
174
|
+
if (emoji.includes(search.trim())) directEmojiMatches.push(emoji)
|
|
177
175
|
}
|
|
178
176
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
//
|
|
177
|
+
if (directEmojiMatches.length > 0) return directEmojiMatches
|
|
178
|
+
|
|
179
|
+
// This lightweight picker only understands category labels, not emoji names.
|
|
182
180
|
const matchingCats = CATEGORIES.filter(
|
|
183
181
|
(c) => c.id !== 'frequent' && c.label.toLowerCase().includes(q)
|
|
184
182
|
)
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
for (const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
catResults.push(emoji)
|
|
193
|
-
}
|
|
183
|
+
const catResults: string[] = []
|
|
184
|
+
const catSeen = new Set<string>()
|
|
185
|
+
for (const cat of matchingCats) {
|
|
186
|
+
for (const emoji of cat.emojis) {
|
|
187
|
+
if (!catSeen.has(emoji)) {
|
|
188
|
+
catSeen.add(emoji)
|
|
189
|
+
catResults.push(emoji)
|
|
194
190
|
}
|
|
195
191
|
}
|
|
196
|
-
return catResults
|
|
197
192
|
}
|
|
198
|
-
|
|
199
|
-
return results
|
|
193
|
+
return catResults
|
|
200
194
|
}, [search])
|
|
201
195
|
|
|
202
196
|
return (
|
|
@@ -212,9 +206,14 @@ export function ReactionPicker({ onSelect, onClose }: Props) {
|
|
|
212
206
|
type="text"
|
|
213
207
|
value={search}
|
|
214
208
|
onChange={(e) => setSearch(e.target.value)}
|
|
215
|
-
placeholder="
|
|
209
|
+
placeholder="Filter by category or paste emoji..."
|
|
216
210
|
className="w-full px-2.5 py-1.5 rounded-[8px] bg-white/[0.06] border border-white/[0.08] text-[12px] text-text placeholder:text-text-3 focus:outline-none focus:border-accent-bright/40"
|
|
217
211
|
/>
|
|
212
|
+
{search.trim() && (
|
|
213
|
+
<p className="mt-1 px-0.5 text-[10px] text-text-3/55">
|
|
214
|
+
This picker filters category labels rather than emoji names.
|
|
215
|
+
</p>
|
|
216
|
+
)}
|
|
218
217
|
</div>
|
|
219
218
|
|
|
220
219
|
{/* Category tabs */}
|
|
@@ -238,17 +237,23 @@ export function ReactionPicker({ onSelect, onClose }: Props) {
|
|
|
238
237
|
{/* Emoji grid */}
|
|
239
238
|
<div className="px-2 pb-2 max-h-[220px] overflow-y-auto">
|
|
240
239
|
{search.trim() ? (
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
240
|
+
filteredEmojis && filteredEmojis.length > 0 ? (
|
|
241
|
+
<div className="grid grid-cols-8 gap-0.5">
|
|
242
|
+
{filteredEmojis.map((emoji, i) => (
|
|
243
|
+
<button
|
|
244
|
+
key={`${emoji}-${i}`}
|
|
245
|
+
onClick={() => onSelect(emoji)}
|
|
246
|
+
className="w-[34px] h-[34px] flex items-center justify-center rounded-[6px] hover:bg-white/[0.08] transition-all cursor-pointer text-[18px]"
|
|
247
|
+
>
|
|
248
|
+
{emoji}
|
|
249
|
+
</button>
|
|
250
|
+
))}
|
|
251
|
+
</div>
|
|
252
|
+
) : (
|
|
253
|
+
<div className="px-2 py-6 text-center text-[11px] text-text-3/60">
|
|
254
|
+
No category matches. Try terms like <span className="text-text-3">food</span>, <span className="text-text-3">travel</span>, or paste an emoji.
|
|
255
|
+
</div>
|
|
256
|
+
)
|
|
252
257
|
) : (
|
|
253
258
|
CATEGORIES.filter((c) => c.id === activeCategory).map((cat) => (
|
|
254
259
|
<div key={cat.id}>
|