@swarmclawai/swarmclaw 0.7.2 → 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 +116 -50
- 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 +43 -0
- package/src/app/api/agents/[id]/thread/route.ts +39 -8
- package/src/app/api/agents/route.ts +35 -2
- package/src/app/api/auth/route.ts +77 -8
- package/src/app/api/chatrooms/[id]/chat/route.ts +22 -6
- 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]/browser/route.ts +5 -1
- package/src/app/api/chats/[id]/chat/route.ts +7 -3
- package/src/app/api/chats/[id]/messages/route.ts +19 -13
- package/src/app/api/chats/[id]/route.ts +30 -0
- package/src/app/api/chats/[id]/stop/route.ts +6 -1
- package/src/app/api/chats/heartbeat/route.ts +2 -1
- package/src/app/api/chats/route.ts +23 -1
- package/src/app/api/connectors/[id]/doctor/route.ts +26 -0
- package/src/app/api/connectors/doctor/route.ts +13 -0
- 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/files/open/route.ts +16 -14
- 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/memory/maintenance/route.ts +11 -1
- package/src/app/api/openclaw/agent-files/route.ts +27 -4
- package/src/app/api/openclaw/gateway/route.ts +10 -7
- package/src/app/api/openclaw/skills/route.ts +12 -4
- package/src/app/api/plugins/dependencies/route.ts +24 -0
- package/src/app/api/plugins/install/route.ts +15 -92
- package/src/app/api/plugins/route.ts +3 -26
- package/src/app/api/plugins/settings/route.ts +17 -12
- package/src/app/api/plugins/ui/route.ts +1 -0
- 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 +55 -17
- package/src/app/api/setup/doctor/route.ts +6 -4
- package/src/app/api/tasks/[id]/route.ts +16 -6
- package/src/app/api/tasks/bulk/route.ts +3 -3
- package/src/app/api/tasks/route.ts +9 -4
- package/src/app/api/webhooks/[id]/route.ts +8 -1
- package/src/app/page.tsx +135 -17
- package/src/cli/binary.test.js +142 -0
- package/src/cli/index.js +38 -11
- package/src/cli/index.test.js +195 -0
- package/src/cli/index.ts +21 -12
- package/src/cli/server-cmd.test.js +59 -0
- package/src/cli/spec.js +20 -2
- package/src/components/agents/agent-card.tsx +15 -12
- package/src/components/agents/agent-chat-list.tsx +101 -1
- package/src/components/agents/agent-list.tsx +46 -9
- package/src/components/agents/agent-sheet.tsx +456 -23
- package/src/components/agents/inspector-panel.tsx +110 -49
- package/src/components/agents/sandbox-env-panel.tsx +4 -1
- package/src/components/auth/access-key-gate.tsx +36 -97
- package/src/components/auth/setup-wizard.tsx +970 -275
- package/src/components/chat/chat-area.tsx +70 -27
- package/src/components/chat/chat-card.tsx +6 -21
- package/src/components/chat/chat-header.tsx +263 -366
- package/src/components/chat/chat-list.tsx +62 -26
- package/src/components/chat/checkpoint-timeline.tsx +1 -1
- package/src/components/chat/message-list.tsx +145 -19
- 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 +422 -209
- package/src/components/chatrooms/reaction-picker.tsx +38 -33
- package/src/components/connectors/connector-list.tsx +265 -127
- package/src/components/connectors/connector-sheet.tsx +217 -0
- package/src/components/gateways/gateway-sheet.tsx +567 -0
- package/src/components/home/home-view.tsx +128 -4
- package/src/components/input/chat-input.tsx +135 -86
- package/src/components/layout/app-layout.tsx +385 -194
- package/src/components/layout/mobile-header.tsx +26 -8
- 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/plugins/plugin-list.tsx +15 -3
- package/src/components/plugins/plugin-sheet.tsx +118 -9
- package/src/components/projects/project-detail.tsx +189 -1
- package/src/components/providers/provider-list.tsx +158 -2
- package/src/components/providers/provider-sheet.tsx +81 -70
- package/src/components/shared/agent-picker-list.tsx +2 -2
- package/src/components/shared/bottom-sheet.tsx +31 -15
- package/src/components/shared/command-palette.tsx +111 -24
- package/src/components/shared/confirm-dialog.tsx +45 -30
- package/src/components/shared/model-combobox.tsx +90 -8
- package/src/components/shared/settings/plugin-manager.tsx +20 -4
- package/src/components/shared/settings/section-capability-policy.tsx +105 -0
- package/src/components/shared/settings/section-heartbeat.tsx +88 -6
- package/src/components/shared/settings/section-orchestrator.tsx +6 -3
- package/src/components/shared/settings/section-runtime-loop.tsx +5 -5
- package/src/components/shared/settings/section-secrets.tsx +6 -6
- package/src/components/shared/settings/section-user-preferences.tsx +1 -1
- package/src/components/shared/settings/section-voice.tsx +5 -1
- package/src/components/shared/settings/section-web-search.tsx +10 -2
- package/src/components/shared/settings/settings-page.tsx +248 -47
- package/src/components/tasks/approvals-panel.tsx +211 -18
- package/src/components/tasks/task-board.tsx +242 -46
- package/src/components/ui/dialog.tsx +2 -2
- package/src/components/usage/metrics-dashboard.tsx +74 -1
- package/src/components/wallets/wallet-approval-dialog.tsx +59 -54
- package/src/components/wallets/wallet-panel.tsx +17 -5
- package/src/components/webhooks/webhook-sheet.tsx +7 -7
- package/src/lib/auth.ts +17 -0
- package/src/lib/chat-streaming-state.test.ts +108 -0
- package/src/lib/chat-streaming-state.ts +108 -0
- package/src/lib/heartbeat-defaults.ts +48 -0
- package/src/lib/memory-presentation.ts +59 -0
- package/src/lib/openclaw-agent-id.test.ts +14 -0
- package/src/lib/openclaw-agent-id.ts +31 -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-assignment.test.ts +112 -0
- package/src/lib/server/agent-assignment.ts +169 -0
- 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/approval-connector-notify.test.ts +253 -0
- package/src/lib/server/approvals-auto-approve.test.ts +264 -0
- package/src/lib/server/approvals.ts +483 -75
- package/src/lib/server/autonomy-runtime.test.ts +341 -0
- package/src/lib/server/browser-state.test.ts +118 -0
- package/src/lib/server/browser-state.ts +123 -0
- package/src/lib/server/build-llm.test.ts +44 -0
- package/src/lib/server/build-llm.ts +11 -4
- package/src/lib/server/builtin-plugins.ts +34 -0
- package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
- package/src/lib/server/chat-execution-tool-events.test.ts +219 -0
- package/src/lib/server/chat-execution.ts +402 -125
- package/src/lib/server/chatroom-health.test.ts +26 -0
- package/src/lib/server/chatroom-health.ts +2 -3
- package/src/lib/server/chatroom-helpers.test.ts +74 -2
- package/src/lib/server/chatroom-helpers.ts +144 -11
- package/src/lib/server/chatroom-session-persistence.test.ts +87 -0
- package/src/lib/server/connectors/discord.ts +175 -11
- package/src/lib/server/connectors/doctor.test.ts +80 -0
- package/src/lib/server/connectors/doctor.ts +116 -0
- package/src/lib/server/connectors/manager.ts +994 -130
- package/src/lib/server/connectors/policy.test.ts +222 -0
- package/src/lib/server/connectors/policy.ts +452 -0
- package/src/lib/server/connectors/slack.ts +189 -10
- package/src/lib/server/connectors/telegram.ts +65 -15
- package/src/lib/server/connectors/thread-context.test.ts +44 -0
- package/src/lib/server/connectors/thread-context.ts +72 -0
- package/src/lib/server/connectors/types.ts +41 -11
- package/src/lib/server/daemon-state.ts +62 -3
- package/src/lib/server/data-dir.ts +13 -0
- package/src/lib/server/delegation-jobs.test.ts +140 -0
- package/src/lib/server/delegation-jobs.ts +248 -0
- package/src/lib/server/document-utils.test.ts +47 -0
- package/src/lib/server/document-utils.ts +397 -0
- 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 +23 -43
- package/src/lib/server/heartbeat-source.test.ts +22 -0
- package/src/lib/server/heartbeat-source.ts +7 -0
- package/src/lib/server/identity-continuity.test.ts +77 -0
- package/src/lib/server/identity-continuity.ts +127 -0
- package/src/lib/server/mailbox-utils.ts +347 -0
- package/src/lib/server/main-agent-loop.ts +31 -964
- package/src/lib/server/memory-db.ts +4 -6
- package/src/lib/server/memory-tiers.ts +40 -0
- package/src/lib/server/openclaw-agent-resolver.test.ts +70 -0
- package/src/lib/server/openclaw-agent-resolver.ts +128 -0
- package/src/lib/server/openclaw-exec-config.ts +6 -5
- package/src/lib/server/openclaw-gateway.ts +123 -36
- package/src/lib/server/openclaw-skills-normalize.test.ts +56 -0
- package/src/lib/server/openclaw-skills-normalize.ts +136 -0
- package/src/lib/server/openclaw-sync.ts +3 -2
- package/src/lib/server/orchestrator-lg.ts +18 -8
- package/src/lib/server/orchestrator.ts +5 -4
- package/src/lib/server/playwright-proxy.mjs +27 -3
- package/src/lib/server/plugins.test.ts +215 -0
- package/src/lib/server/plugins.ts +832 -69
- package/src/lib/server/provider-health.ts +33 -3
- package/src/lib/server/provider-model-discovery.ts +481 -0
- package/src/lib/server/queue.ts +4 -21
- 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/scheduler.ts +2 -0
- package/src/lib/server/session-archive-memory.test.ts +85 -0
- package/src/lib/server/session-archive-memory.ts +230 -0
- package/src/lib/server/session-mailbox.ts +8 -18
- package/src/lib/server/session-reset-policy.test.ts +99 -0
- package/src/lib/server/session-reset-policy.ts +311 -0
- package/src/lib/server/session-run-manager.ts +33 -80
- package/src/lib/server/session-tools/autonomy-tools.test.ts +128 -0
- package/src/lib/server/session-tools/calendar.ts +2 -12
- package/src/lib/server/session-tools/connector.ts +109 -8
- package/src/lib/server/session-tools/context.ts +14 -2
- package/src/lib/server/session-tools/crawl.ts +447 -0
- package/src/lib/server/session-tools/crud.ts +96 -34
- package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
- package/src/lib/server/session-tools/delegate.ts +406 -20
- package/src/lib/server/session-tools/discovery-approvals.test.ts +170 -0
- package/src/lib/server/session-tools/discovery.ts +40 -12
- package/src/lib/server/session-tools/document.ts +283 -0
- package/src/lib/server/session-tools/email.ts +1 -3
- package/src/lib/server/session-tools/extract.ts +137 -0
- package/src/lib/server/session-tools/file-normalize.test.ts +98 -0
- package/src/lib/server/session-tools/file-send.test.ts +84 -1
- package/src/lib/server/session-tools/file.ts +243 -24
- package/src/lib/server/session-tools/http.ts +9 -3
- package/src/lib/server/session-tools/human-loop.ts +227 -0
- package/src/lib/server/session-tools/image-gen.ts +1 -3
- package/src/lib/server/session-tools/index.ts +87 -2
- package/src/lib/server/session-tools/mailbox.ts +276 -0
- package/src/lib/server/session-tools/manage-schedules.test.ts +137 -0
- package/src/lib/server/session-tools/memory.ts +35 -3
- package/src/lib/server/session-tools/monitor.ts +162 -12
- package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
- 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-normalize.test.ts +142 -0
- package/src/lib/server/session-tools/platform.ts +142 -4
- package/src/lib/server/session-tools/plugin-creator.ts +95 -25
- package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
- package/src/lib/server/session-tools/replicate.ts +1 -3
- package/src/lib/server/session-tools/sandbox.ts +51 -92
- package/src/lib/server/session-tools/schedule.ts +20 -10
- package/src/lib/server/session-tools/session-info.ts +58 -4
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +54 -17
- package/src/lib/server/session-tools/shell.ts +2 -2
- package/src/lib/server/session-tools/subagent.ts +195 -27
- package/src/lib/server/session-tools/table.ts +587 -0
- package/src/lib/server/session-tools/wallet.ts +13 -10
- package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
- package/src/lib/server/session-tools/web.ts +947 -108
- package/src/lib/server/storage.ts +255 -10
- package/src/lib/server/stream-agent-chat.test.ts +61 -0
- package/src/lib/server/stream-agent-chat.ts +185 -25
- package/src/lib/server/structured-extract.test.ts +72 -0
- package/src/lib/server/structured-extract.ts +373 -0
- package/src/lib/server/task-mention.test.ts +16 -2
- package/src/lib/server/task-mention.ts +61 -11
- package/src/lib/server/tool-aliases.ts +80 -12
- package/src/lib/server/tool-capability-policy.ts +7 -1
- package/src/lib/server/tool-retry.ts +2 -0
- package/src/lib/server/watch-jobs.test.ts +173 -0
- package/src/lib/server/watch-jobs.ts +532 -0
- package/src/lib/server/ws-hub.ts +5 -3
- package/src/lib/setup-defaults.ts +352 -11
- package/src/lib/tool-definitions.ts +3 -4
- package/src/lib/validation/schemas.test.ts +26 -0
- package/src/lib/validation/schemas.ts +62 -1
- package/src/lib/ws-client.ts +14 -12
- package/src/proxy.ts +5 -5
- package/src/stores/use-app-store.ts +43 -7
- package/src/stores/use-chat-store.ts +31 -2
- package/src/stores/use-chatroom-store.ts +153 -26
- package/src/types/index.ts +470 -44
- package/src/app/api/chats/[id]/main-loop/route.ts +0 -94
- package/src/components/chat/new-chat-sheet.tsx +0 -253
- package/src/lib/server/main-session.ts +0 -17
- package/src/lib/server/session-run-manager.test.ts +0 -26
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useEffect, useRef, useCallback, useState } from 'react'
|
|
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) {
|
|
@@ -41,7 +43,6 @@ function isAgentMuted(chatroom: Chatroom, agentId: string): boolean {
|
|
|
41
43
|
|
|
42
44
|
type MomentType = { kind: 'heartbeat' } | { kind: 'tool'; name: string; input: string }
|
|
43
45
|
|
|
44
|
-
/** Subscribe to a single agent heartbeat topic — one hook call per agent */
|
|
45
46
|
function useAgentHeartbeat(agentId: string, onPulse: (id: string) => void) {
|
|
46
47
|
const topic = agentId ? `heartbeat:agent:${agentId}` : ''
|
|
47
48
|
const onPulseRef = useRef(onPulse)
|
|
@@ -51,18 +52,22 @@ function useAgentHeartbeat(agentId: string, onPulse: (id: string) => void) {
|
|
|
51
52
|
useWs(topic, () => onPulseRef.current(agentId))
|
|
52
53
|
}
|
|
53
54
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
useAgentHeartbeat(agentIds[0] || '', onPulse)
|
|
57
|
-
useAgentHeartbeat(agentIds[1] || '', onPulse)
|
|
58
|
-
useAgentHeartbeat(agentIds[2] || '', onPulse)
|
|
59
|
-
useAgentHeartbeat(agentIds[3] || '', onPulse)
|
|
60
|
-
useAgentHeartbeat(agentIds[4] || '', onPulse)
|
|
61
|
-
useAgentHeartbeat(agentIds[5] || '', onPulse)
|
|
55
|
+
function AgentHeartbeatListener({ agentId, onPulse }: { agentId: string; onPulse: (id: string) => void }) {
|
|
56
|
+
useAgentHeartbeat(agentId, onPulse)
|
|
62
57
|
return null
|
|
63
58
|
}
|
|
64
59
|
|
|
65
|
-
|
|
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
|
+
|
|
70
|
+
const GROUP_THRESHOLD_MS = 2 * 60 * 1000
|
|
66
71
|
|
|
67
72
|
function dayLabel(ts: number): string {
|
|
68
73
|
const d = new Date(ts)
|
|
@@ -78,7 +83,6 @@ function dayLabel(ts: number): string {
|
|
|
78
83
|
export function ChatroomView() {
|
|
79
84
|
const currentChatroomId = useChatroomStore((s) => s.currentChatroomId)
|
|
80
85
|
const chatrooms = useChatroomStore((s) => s.chatrooms)
|
|
81
|
-
const streaming = useChatroomStore((s) => s.streaming)
|
|
82
86
|
const streamingAgents = useChatroomStore((s) => s.streamingAgents)
|
|
83
87
|
const sendMessage = useChatroomStore((s) => s.sendMessage)
|
|
84
88
|
const toggleReaction = useChatroomStore((s) => s.toggleReaction)
|
|
@@ -92,11 +96,13 @@ export function ChatroomView() {
|
|
|
92
96
|
const unmuteAgent = useChatroomStore((s) => s.unmuteAgent)
|
|
93
97
|
const setMemberRole = useChatroomStore((s) => s.setMemberRole)
|
|
94
98
|
const agents = useAppStore((s) => s.agents) as Record<string, Agent>
|
|
99
|
+
const lastReadTimestamps = useAppStore((s) => s.lastReadTimestamps)
|
|
100
|
+
const markChatRead = useAppStore((s) => s.markChatRead)
|
|
95
101
|
const scrollRef = useRef<HTMLDivElement>(null)
|
|
96
102
|
const [pinsExpanded, setPinsExpanded] = useState(false)
|
|
97
|
-
|
|
98
|
-
// Per-agent moment overlays (heartbeat or tool events)
|
|
103
|
+
const [isNearBottom, setIsNearBottom] = useState(true)
|
|
99
104
|
const [agentMoments, setAgentMoments] = useState<Record<string, MomentType>>({})
|
|
105
|
+
const [detailsOpen, setDetailsOpen] = useState(false)
|
|
100
106
|
|
|
101
107
|
const handleHeartbeatPulse = useCallback((agentId: string) => {
|
|
102
108
|
setAgentMoments((prev) => ({ ...prev, [agentId]: { kind: 'heartbeat' } }))
|
|
@@ -111,13 +117,11 @@ export function ChatroomView() {
|
|
|
111
117
|
}, [])
|
|
112
118
|
|
|
113
119
|
const chatroom = currentChatroomId ? (chatrooms[currentChatroomId] as Chatroom | undefined) : null
|
|
114
|
-
|
|
115
|
-
// Detect notable tool events from chatroom messages
|
|
116
120
|
const chatroomMessages = chatroom?.messages
|
|
117
121
|
const prevToolKeysRef = useRef<Record<string, string>>({})
|
|
122
|
+
|
|
118
123
|
useEffect(() => {
|
|
119
124
|
if (!chatroomMessages?.length) return
|
|
120
|
-
// Find the last message from each agent and check for notable tools
|
|
121
125
|
const lastByAgent = new Map<string, ChatroomMessage>()
|
|
122
126
|
for (const msg of chatroomMessages) {
|
|
123
127
|
if (msg.senderId !== 'user' && msg.senderId !== 'system') {
|
|
@@ -142,32 +146,77 @@ export function ChatroomView() {
|
|
|
142
146
|
|
|
143
147
|
const refreshChatroom = useCallback(() => {
|
|
144
148
|
loadChatrooms()
|
|
145
|
-
|
|
146
|
-
}, [])
|
|
149
|
+
}, [loadChatrooms])
|
|
147
150
|
|
|
148
151
|
useWs(currentChatroomId ? `chatroom:${currentChatroomId}` : '', refreshChatroom)
|
|
149
152
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
153
|
+
const memberAgents = useMemo(() => (
|
|
154
|
+
chatroom
|
|
155
|
+
? (chatroom.agentIds.map((id) => agents[id]).filter(Boolean) as Agent[])
|
|
156
|
+
: []
|
|
157
|
+
), [agents, chatroom])
|
|
158
|
+
|
|
159
|
+
const streamingAgentIds = useMemo(() => new Set(streamingAgents.keys()), [streamingAgents])
|
|
160
|
+
const chatroomId = chatroom?.id || null
|
|
161
|
+
const pinnedIds = useMemo(() => chatroom?.pinnedMessageIds ?? [], [chatroom?.pinnedMessageIds])
|
|
162
|
+
const pinnedMessages = useMemo(() => (
|
|
163
|
+
chatroom
|
|
164
|
+
? (pinnedIds.map((pid) => chatroom.messages.find((m) => m.id === pid)).filter(Boolean) as ChatroomMessage[])
|
|
165
|
+
: []
|
|
166
|
+
), [chatroom, pinnedIds])
|
|
167
|
+
const memberAgentIds = chatroom?.agentIds || []
|
|
168
|
+
const mutedCount = chatroom ? chatroom.agentIds.filter((agentId) => isAgentMuted(chatroom, agentId)).length : 0
|
|
169
|
+
const adminCount = chatroom ? chatroom.agentIds.filter((agentId) => getMemberRole(chatroom, agentId) === 'admin').length : 0
|
|
170
|
+
const lastReadAt = chatroom ? (lastReadTimestamps[chatroom.id] || 0) : 0
|
|
171
|
+
const unreadCount = useMemo(() => (
|
|
172
|
+
chatroom
|
|
173
|
+
? chatroom.messages.filter((msg) => msg.senderId !== 'user' && msg.senderId !== 'system' && (msg.time || 0) > lastReadAt).length
|
|
174
|
+
: 0
|
|
175
|
+
), [chatroom, lastReadAt])
|
|
176
|
+
|
|
177
|
+
const focusMessage = useCallback((messageId: string) => {
|
|
178
|
+
const el = document.getElementById(`chatroom-msg-${messageId}`)
|
|
179
|
+
if (el) {
|
|
180
|
+
el.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
|
181
|
+
el.classList.add('bg-accent-soft/20')
|
|
182
|
+
setTimeout(() => el.classList.remove('bg-accent-soft/20'), 2000)
|
|
154
183
|
}
|
|
155
|
-
}, [
|
|
184
|
+
}, [])
|
|
185
|
+
|
|
186
|
+
const scrollToLatest = useCallback((behavior: ScrollBehavior = 'smooth') => {
|
|
187
|
+
const node = scrollRef.current
|
|
188
|
+
if (!node || !chatroom) return
|
|
189
|
+
node.scrollTo({ top: node.scrollHeight, behavior })
|
|
190
|
+
markChatRead(chatroom.id)
|
|
191
|
+
}, [chatroom, markChatRead])
|
|
156
192
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
: []
|
|
193
|
+
useEffect(() => {
|
|
194
|
+
if (!chatroomId) return
|
|
195
|
+
markChatRead(chatroomId)
|
|
196
|
+
}, [chatroomId, markChatRead])
|
|
162
197
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
? (pinnedIds.map((pid) => chatroom.messages.find((m) => m.id === pid)).filter(Boolean) as ChatroomMessage[])
|
|
167
|
-
: []
|
|
198
|
+
useEffect(() => {
|
|
199
|
+
setDetailsOpen(false)
|
|
200
|
+
}, [chatroomId])
|
|
168
201
|
|
|
169
|
-
|
|
170
|
-
|
|
202
|
+
useEffect(() => {
|
|
203
|
+
const node = scrollRef.current
|
|
204
|
+
if (!node || !chatroomId) return
|
|
205
|
+
const handleScroll = () => {
|
|
206
|
+
const nearBottom = node.scrollHeight - node.scrollTop - node.clientHeight < 120
|
|
207
|
+
setIsNearBottom(nearBottom)
|
|
208
|
+
if (nearBottom) markChatRead(chatroomId)
|
|
209
|
+
}
|
|
210
|
+
handleScroll()
|
|
211
|
+
node.addEventListener('scroll', handleScroll)
|
|
212
|
+
return () => node.removeEventListener('scroll', handleScroll)
|
|
213
|
+
}, [chatroomId, markChatRead])
|
|
214
|
+
|
|
215
|
+
useEffect(() => {
|
|
216
|
+
if (chatroom && isNearBottom) {
|
|
217
|
+
scrollToLatest(chatroom.messages.length <= 1 ? 'auto' : 'smooth')
|
|
218
|
+
}
|
|
219
|
+
}, [chatroom, isNearBottom, scrollToLatest, streamingAgents.size])
|
|
171
220
|
|
|
172
221
|
if (!chatroom) {
|
|
173
222
|
return (
|
|
@@ -185,7 +234,6 @@ export function ChatroomView() {
|
|
|
185
234
|
}
|
|
186
235
|
|
|
187
236
|
const handleTransfer = (messageId: string, targetAgentId: string) => {
|
|
188
|
-
if (!chatroom) return
|
|
189
237
|
const msg = chatroom.messages.find((m) => m.id === messageId)
|
|
190
238
|
const targetAgent = agents[targetAgentId]
|
|
191
239
|
if (!msg || !targetAgent) return
|
|
@@ -194,196 +242,361 @@ export function ChatroomView() {
|
|
|
194
242
|
}
|
|
195
243
|
|
|
196
244
|
return (
|
|
197
|
-
<div className="flex-1 flex
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
245
|
+
<div className="flex-1 flex min-h-0 min-w-0">
|
|
246
|
+
<div className="min-w-0 flex-1 flex flex-col h-full">
|
|
247
|
+
<div className="flex items-center gap-3 px-4 py-3 border-b border-white/[0.06] shrink-0">
|
|
248
|
+
<div className="w-8 h-8 rounded-full bg-accent-soft flex items-center justify-center shrink-0">
|
|
249
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-accent-bright">
|
|
250
|
+
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
|
|
251
|
+
</svg>
|
|
252
|
+
</div>
|
|
253
|
+
<div className="flex-1 min-w-0">
|
|
254
|
+
<h3 className="text-[14px] font-700 text-text truncate">{chatroom.name}</h3>
|
|
255
|
+
<div className="flex flex-wrap items-center gap-2 mt-1">
|
|
256
|
+
<p className="text-[11px] text-text-3 truncate">
|
|
257
|
+
{memberAgents.length} agent{memberAgents.length !== 1 ? 's' : ''}
|
|
258
|
+
{chatroom.description ? ` · ${chatroom.description}` : ''}
|
|
259
|
+
</p>
|
|
260
|
+
<span className="px-1.5 py-0.5 rounded-[5px] bg-white/[0.04] text-[10px] font-700 uppercase tracking-[0.08em] text-text-3/70">
|
|
261
|
+
{chatroom.chatMode === 'parallel' ? 'Parallel' : 'Sequential'}
|
|
262
|
+
</span>
|
|
263
|
+
<span className={`px-1.5 py-0.5 rounded-[5px] text-[10px] font-700 uppercase tracking-[0.08em] ${
|
|
264
|
+
chatroom.autoAddress ? 'bg-emerald-500/10 text-emerald-400' : 'bg-white/[0.04] text-text-3/70'
|
|
265
|
+
}`}>
|
|
266
|
+
Auto-address {chatroom.autoAddress ? 'on' : 'off'}
|
|
267
|
+
</span>
|
|
268
|
+
{streamingAgents.size > 0 && (
|
|
269
|
+
<span className="px-1.5 py-0.5 rounded-[5px] bg-sky-500/10 text-[10px] font-700 uppercase tracking-[0.08em] text-sky-400">
|
|
270
|
+
{streamingAgents.size} active now
|
|
271
|
+
</span>
|
|
272
|
+
)}
|
|
273
|
+
</div>
|
|
274
|
+
</div>
|
|
275
|
+
|
|
276
|
+
<div className="flex -space-x-1.5 shrink-0">
|
|
277
|
+
{memberAgents.slice(0, 5).map((agent) => {
|
|
278
|
+
const role = getMemberRole(chatroom, agent.id)
|
|
279
|
+
const badge = getRoleBadge(role)
|
|
280
|
+
const muted = isAgentMuted(chatroom, agent.id)
|
|
281
|
+
return (
|
|
282
|
+
<Tooltip key={agent.id}>
|
|
283
|
+
<TooltipTrigger asChild>
|
|
284
|
+
<button
|
|
285
|
+
onClick={() => navigateToAgent(agent.id)}
|
|
286
|
+
className={`relative transition-all duration-200 hover:scale-110 hover:z-10 hover:-translate-y-0.5 cursor-pointer bg-transparent border-none p-0 ${muted ? 'opacity-40' : ''}`}
|
|
287
|
+
>
|
|
288
|
+
<AgentAvatar seed={agent.avatarSeed} avatarUrl={agent.avatarUrl} name={agent.name} size={22} status={streamingAgents.has(agent.id) ? 'busy' : 'online'} />
|
|
289
|
+
{badge && (
|
|
290
|
+
<span className={`absolute -bottom-1 -right-1 text-[7px] font-700 px-0.5 rounded border ${badge.className}`}>
|
|
291
|
+
{badge.label[0]}
|
|
292
|
+
</span>
|
|
293
|
+
)}
|
|
294
|
+
</button>
|
|
295
|
+
</TooltipTrigger>
|
|
296
|
+
<TooltipContent side="bottom" sideOffset={6}>
|
|
297
|
+
<div className="flex items-center gap-1.5">
|
|
298
|
+
<span>{agent.name}</span>
|
|
299
|
+
{badge && <span className={`text-[9px] font-600 px-1 py-0.5 rounded border ${badge.className}`}>{badge.label}</span>}
|
|
300
|
+
{muted && <span className="text-[9px] text-red-400">Muted</span>}
|
|
301
|
+
</div>
|
|
302
|
+
</TooltipContent>
|
|
303
|
+
</Tooltip>
|
|
304
|
+
)
|
|
305
|
+
})}
|
|
306
|
+
{memberAgents.length > 5 && (
|
|
307
|
+
<div className="w-[22px] h-[22px] rounded-full bg-white/[0.08] flex items-center justify-center text-[9px] text-text-3">
|
|
308
|
+
+{memberAgents.length - 5}
|
|
309
|
+
</div>
|
|
310
|
+
)}
|
|
311
|
+
</div>
|
|
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
|
+
|
|
321
|
+
<button
|
|
322
|
+
onClick={() => {
|
|
323
|
+
setEditingChatroomId(chatroom.id)
|
|
324
|
+
setChatroomSheetOpen(true)
|
|
325
|
+
}}
|
|
326
|
+
className="shrink-0 w-7 h-7 rounded-full flex items-center justify-center hover:bg-white/[0.08] transition-all cursor-pointer"
|
|
327
|
+
>
|
|
328
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-text-3">
|
|
329
|
+
<circle cx="12" cy="12" r="3" />
|
|
330
|
+
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" />
|
|
331
|
+
</svg>
|
|
332
|
+
</button>
|
|
211
333
|
</div>
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
334
|
+
|
|
335
|
+
{pinnedMessages.length > 0 && (
|
|
336
|
+
<div className="border-b border-white/[0.06] shrink-0">
|
|
337
|
+
<button
|
|
338
|
+
onClick={() => setPinsExpanded(!pinsExpanded)}
|
|
339
|
+
className="w-full flex items-center gap-2 px-4 py-2 hover:bg-white/[0.02] transition-colors cursor-pointer bg-transparent border-none text-left"
|
|
340
|
+
style={{ fontFamily: 'inherit' }}
|
|
341
|
+
>
|
|
342
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-amber-400 shrink-0">
|
|
343
|
+
<path d="M12 17v5" />
|
|
344
|
+
<path d="M9 10.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24V16h14v-.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V7a1 1 0 0 1 1-1 2 2 0 0 0 2-2H6a2 2 0 0 0 2 2 1 1 0 0 1 1 1z" />
|
|
345
|
+
</svg>
|
|
346
|
+
<span className="text-[12px] font-500 text-text-2">{pinnedMessages.length} pinned</span>
|
|
347
|
+
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className={`text-text-3 transition-transform ${pinsExpanded ? 'rotate-180' : ''}`}>
|
|
348
|
+
<polyline points="6 9 12 15 18 9" />
|
|
349
|
+
</svg>
|
|
350
|
+
</button>
|
|
351
|
+
{pinsExpanded && (
|
|
352
|
+
<div className="px-4 pb-2 flex flex-col gap-1">
|
|
353
|
+
{pinnedMessages.map((message) => (
|
|
221
354
|
<button
|
|
222
|
-
|
|
223
|
-
|
|
355
|
+
key={message.id}
|
|
356
|
+
onClick={() => focusMessage(message.id)}
|
|
357
|
+
className="flex items-center gap-2 px-2 py-1.5 rounded-[8px] hover:bg-white/[0.04] transition-colors cursor-pointer bg-transparent border-none text-left w-full"
|
|
358
|
+
style={{ fontFamily: 'inherit' }}
|
|
224
359
|
>
|
|
225
|
-
<
|
|
226
|
-
|
|
227
|
-
<span className={`absolute -bottom-1 -right-1 text-[7px] font-700 px-0.5 rounded border ${badge.className}`}>
|
|
228
|
-
{badge.label[0]}
|
|
229
|
-
</span>
|
|
230
|
-
)}
|
|
360
|
+
<span className="text-[11px] font-600 text-accent-bright shrink-0">{message.senderName}</span>
|
|
361
|
+
<span className="text-[11px] text-text-3 truncate flex-1">{message.text.slice(0, 80)}</span>
|
|
231
362
|
</button>
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
363
|
+
))}
|
|
364
|
+
</div>
|
|
365
|
+
)}
|
|
366
|
+
</div>
|
|
367
|
+
)}
|
|
368
|
+
|
|
369
|
+
<AgentHeartbeatListeners agentIds={memberAgentIds} onPulse={handleHeartbeatPulse} />
|
|
370
|
+
|
|
371
|
+
<div className="relative flex-1 min-h-0">
|
|
372
|
+
<div ref={scrollRef} className="absolute inset-0 overflow-y-auto py-3">
|
|
373
|
+
{chatroom.messages.length === 0 ? (
|
|
374
|
+
<div className="flex items-center justify-center h-full px-6">
|
|
375
|
+
<div className="text-center">
|
|
376
|
+
<p className="text-[13px] text-text-3 mb-1">No messages yet</p>
|
|
377
|
+
<p className="text-[12px] text-text-3/60">Use @AgentName to mention specific agents, or @all for everyone</p>
|
|
378
|
+
</div>
|
|
379
|
+
</div>
|
|
380
|
+
) : (
|
|
381
|
+
chatroom.messages.map((msg, i) => {
|
|
382
|
+
const prev = i > 0 ? chatroom.messages[i - 1] : null
|
|
383
|
+
const isGrouped = prev
|
|
384
|
+
? prev.senderId === msg.senderId && (msg.time - prev.time) < GROUP_THRESHOLD_MS
|
|
385
|
+
: false
|
|
386
|
+
const prevDay = prev ? new Date(prev.time).toDateString() : null
|
|
387
|
+
const msgDay = new Date(msg.time).toDateString()
|
|
388
|
+
const showDaySep = !prev || prevDay !== msgDay
|
|
389
|
+
|
|
390
|
+
const senderId = msg.senderId
|
|
391
|
+
const moment = agentMoments[senderId]
|
|
392
|
+
const isLastFromSender = !chatroom.messages.slice(i + 1).some((m) => m.senderId === senderId)
|
|
393
|
+
let momentOverlay: React.ReactNode = null
|
|
394
|
+
if (moment && isLastFromSender && senderId !== 'user' && senderId !== 'system') {
|
|
395
|
+
if (moment.kind === 'heartbeat') {
|
|
396
|
+
momentOverlay = <HeartbeatMoment onDismiss={() => clearAgentMoment(senderId)} />
|
|
397
|
+
} else {
|
|
398
|
+
momentOverlay = (
|
|
399
|
+
<ActivityMoment
|
|
400
|
+
key={`${moment.name}-${senderId}`}
|
|
401
|
+
toolName={moment.name}
|
|
402
|
+
toolInput={moment.input}
|
|
403
|
+
onDismiss={() => clearAgentMoment(senderId)}
|
|
404
|
+
/>
|
|
405
|
+
)
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return (
|
|
410
|
+
<div key={msg.id}>
|
|
411
|
+
{showDaySep && (
|
|
412
|
+
<div className="flex items-center gap-3 px-4 py-3">
|
|
413
|
+
<div className="flex-1 h-px bg-white/[0.06]" />
|
|
414
|
+
<span className="text-[10px] font-600 text-text-3 uppercase tracking-wider">{dayLabel(msg.time)}</span>
|
|
415
|
+
<div className="flex-1 h-px bg-white/[0.06]" />
|
|
416
|
+
</div>
|
|
417
|
+
)}
|
|
418
|
+
<ChatroomMessageBubble
|
|
419
|
+
message={msg}
|
|
420
|
+
agents={agents}
|
|
421
|
+
onToggleReaction={toggleReaction}
|
|
422
|
+
onReply={(message: ChatroomMessage) => setReplyingTo(message)}
|
|
423
|
+
onTogglePin={togglePin}
|
|
424
|
+
onTransfer={handleTransfer}
|
|
425
|
+
onDeleteMessage={(messageId, targetAgentId) => deleteMessage(messageId, targetAgentId)}
|
|
426
|
+
onMuteAgent={(agentId) => muteAgent(agentId)}
|
|
427
|
+
onUnmuteAgent={(agentId) => unmuteAgent(agentId)}
|
|
428
|
+
onSetRole={(agentId, role) => setMemberRole(agentId, role)}
|
|
429
|
+
chatroom={chatroom}
|
|
430
|
+
pinnedMessageIds={pinnedIds}
|
|
431
|
+
streamingAgentIds={streamingAgentIds}
|
|
432
|
+
messages={chatroom.messages}
|
|
433
|
+
grouped={isGrouped && !showDaySep}
|
|
434
|
+
momentOverlay={momentOverlay}
|
|
435
|
+
/>
|
|
238
436
|
</div>
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
)
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
437
|
+
)
|
|
438
|
+
})
|
|
439
|
+
)}
|
|
440
|
+
<ChatroomTypingBar streamingAgents={streamingAgents} />
|
|
441
|
+
</div>
|
|
442
|
+
|
|
443
|
+
{(!isNearBottom || unreadCount > 0) && (
|
|
444
|
+
<button
|
|
445
|
+
onClick={() => scrollToLatest('smooth')}
|
|
446
|
+
className="absolute bottom-4 right-4 px-3.5 py-2 rounded-[10px] bg-surface-2/95 backdrop-blur-xl border border-white/[0.1] text-[12px] font-700 text-text shadow-[0_8px_30px_rgba(0,0,0,0.4)] hover:bg-white/[0.08] transition-all cursor-pointer"
|
|
447
|
+
style={{ fontFamily: 'inherit' }}
|
|
448
|
+
>
|
|
449
|
+
Jump to latest{unreadCount > 0 ? ` · ${unreadCount} new` : ''}
|
|
450
|
+
</button>
|
|
247
451
|
)}
|
|
248
452
|
</div>
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
453
|
+
|
|
454
|
+
<ChatroomInput
|
|
455
|
+
agents={memberAgents}
|
|
456
|
+
onSend={sendMessage}
|
|
457
|
+
/>
|
|
458
|
+
</div>
|
|
459
|
+
|
|
460
|
+
<aside className="hidden xl:flex xl:w-[300px] xl:flex-col xl:border-l xl:border-white/[0.06] bg-surface/30">
|
|
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>
|
|
471
|
+
|
|
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)
|
|
253
483
|
}}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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>
|
|
524
|
+
</div>
|
|
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>
|
|
261
532
|
</div>
|
|
262
533
|
|
|
263
|
-
{
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
</svg>
|
|
275
|
-
<span className="text-[12px] font-500 text-text-2">{pinnedMessages.length} pinned</span>
|
|
276
|
-
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className={`text-text-3 transition-transform ${pinsExpanded ? 'rotate-180' : ''}`}>
|
|
277
|
-
<polyline points="6 9 12 15 18 9" />
|
|
278
|
-
</svg>
|
|
279
|
-
</button>
|
|
280
|
-
{pinsExpanded && (
|
|
281
|
-
<div className="px-4 pb-2 flex flex-col gap-1">
|
|
282
|
-
{pinnedMessages.map((pm) => (
|
|
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 (
|
|
283
545
|
<button
|
|
284
|
-
key={
|
|
285
|
-
onClick={() =>
|
|
286
|
-
|
|
287
|
-
if (el) {
|
|
288
|
-
el.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
|
289
|
-
el.classList.add('bg-accent-soft/20')
|
|
290
|
-
setTimeout(() => el.classList.remove('bg-accent-soft/20'), 2000)
|
|
291
|
-
}
|
|
292
|
-
}}
|
|
293
|
-
className="flex items-center gap-2 px-2 py-1.5 rounded-[8px] hover:bg-white/[0.04] transition-colors cursor-pointer bg-transparent border-none text-left w-full"
|
|
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"
|
|
294
549
|
style={{ fontFamily: 'inherit' }}
|
|
295
550
|
>
|
|
296
|
-
<
|
|
297
|
-
|
|
551
|
+
<div className="flex items-center gap-3">
|
|
552
|
+
<AgentAvatar seed={agent.avatarSeed} avatarUrl={agent.avatarUrl} name={agent.name} size={26} status={streamingAgents.has(agent.id) ? 'busy' : 'online'} />
|
|
553
|
+
<div className="min-w-0 flex-1">
|
|
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">
|
|
557
|
+
{role}
|
|
558
|
+
</span>
|
|
559
|
+
{muted && (
|
|
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">
|
|
561
|
+
Muted
|
|
562
|
+
</span>
|
|
563
|
+
)}
|
|
564
|
+
{streamingAgents.has(agent.id) && (
|
|
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">
|
|
566
|
+
Active
|
|
567
|
+
</span>
|
|
568
|
+
)}
|
|
569
|
+
</div>
|
|
570
|
+
</div>
|
|
571
|
+
</div>
|
|
298
572
|
</button>
|
|
299
|
-
)
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
</
|
|
303
|
-
)}
|
|
304
|
-
|
|
305
|
-
<AgentHeartbeatListeners agentIds={memberAgentIds} onPulse={handleHeartbeatPulse} />
|
|
573
|
+
)
|
|
574
|
+
})}
|
|
575
|
+
</div>
|
|
576
|
+
</section>
|
|
306
577
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
<p className="text-[13px] text-text-3 mb-1">No messages yet</p>
|
|
313
|
-
<p className="text-[12px] text-text-3/60">Use @AgentName to mention specific agents, or @all for everyone</p>
|
|
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>
|
|
314
583
|
</div>
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
const moment = agentMoments[senderId]
|
|
330
|
-
const isLastFromSender = !chatroom.messages.slice(i + 1).some((m) => m.senderId === senderId)
|
|
331
|
-
let momentOverlay: React.ReactNode = null
|
|
332
|
-
if (moment && isLastFromSender && senderId !== 'user' && senderId !== 'system') {
|
|
333
|
-
if (moment.kind === 'heartbeat') {
|
|
334
|
-
momentOverlay = <HeartbeatMoment onDismiss={() => clearAgentMoment(senderId)} />
|
|
335
|
-
} else {
|
|
336
|
-
momentOverlay = (
|
|
337
|
-
<ActivityMoment
|
|
338
|
-
key={`${moment.name}-${senderId}`}
|
|
339
|
-
toolName={moment.name}
|
|
340
|
-
toolInput={moment.input}
|
|
341
|
-
onDismiss={() => clearAgentMoment(senderId)}
|
|
342
|
-
/>
|
|
343
|
-
)
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
return (
|
|
348
|
-
<div key={msg.id}>
|
|
349
|
-
{showDaySep && (
|
|
350
|
-
<div className="flex items-center gap-3 px-4 py-3">
|
|
351
|
-
<div className="flex-1 h-px bg-white/[0.06]" />
|
|
352
|
-
<span className="text-[10px] font-600 text-text-3 uppercase tracking-wider">{dayLabel(msg.time)}</span>
|
|
353
|
-
<div className="flex-1 h-px bg-white/[0.06]" />
|
|
354
|
-
</div>
|
|
355
|
-
)}
|
|
356
|
-
<ChatroomMessageBubble
|
|
357
|
-
message={msg}
|
|
358
|
-
agents={agents}
|
|
359
|
-
onToggleReaction={toggleReaction}
|
|
360
|
-
onReply={(m: ChatroomMessage) => setReplyingTo(m)}
|
|
361
|
-
onTogglePin={togglePin}
|
|
362
|
-
onTransfer={handleTransfer}
|
|
363
|
-
onDeleteMessage={(messageId, targetAgentId) => deleteMessage(messageId, targetAgentId)}
|
|
364
|
-
onMuteAgent={(agentId) => muteAgent(agentId)}
|
|
365
|
-
onUnmuteAgent={(agentId) => unmuteAgent(agentId)}
|
|
366
|
-
onSetRole={(agentId, role) => setMemberRole(agentId, role)}
|
|
367
|
-
chatroom={chatroom}
|
|
368
|
-
pinnedMessageIds={pinnedIds}
|
|
369
|
-
streamingAgentIds={streamingAgentIds}
|
|
370
|
-
messages={chatroom.messages}
|
|
371
|
-
grouped={isGrouped && !showDaySep}
|
|
372
|
-
momentOverlay={momentOverlay}
|
|
373
|
-
/>
|
|
374
|
-
</div>
|
|
375
|
-
)
|
|
376
|
-
})
|
|
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
|
+
))}
|
|
596
|
+
</div>
|
|
597
|
+
</section>
|
|
377
598
|
)}
|
|
378
|
-
<ChatroomTypingBar streamingAgents={streamingAgents} />
|
|
379
599
|
</div>
|
|
380
|
-
|
|
381
|
-
{/* Input */}
|
|
382
|
-
<ChatroomInput
|
|
383
|
-
agents={memberAgents}
|
|
384
|
-
onSend={sendMessage}
|
|
385
|
-
disabled={streaming}
|
|
386
|
-
/>
|
|
387
600
|
</div>
|
|
388
601
|
)
|
|
389
602
|
}
|