@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.
Files changed (274) hide show
  1. package/README.md +116 -50
  2. package/bin/package-manager.js +157 -0
  3. package/bin/package-manager.test.js +90 -0
  4. package/bin/server-cmd.js +38 -7
  5. package/bin/swarmclaw.js +54 -4
  6. package/bin/update-cmd.js +48 -10
  7. package/bin/update-cmd.test.js +55 -0
  8. package/package.json +8 -3
  9. package/scripts/postinstall.mjs +26 -0
  10. package/src/app/api/agents/[id]/route.ts +43 -0
  11. package/src/app/api/agents/[id]/thread/route.ts +39 -8
  12. package/src/app/api/agents/route.ts +35 -2
  13. package/src/app/api/auth/route.ts +77 -8
  14. package/src/app/api/chatrooms/[id]/chat/route.ts +22 -6
  15. package/src/app/api/chatrooms/[id]/pins/route.ts +2 -1
  16. package/src/app/api/chatrooms/[id]/reactions/route.ts +2 -1
  17. package/src/app/api/chatrooms/[id]/route.ts +6 -0
  18. package/src/app/api/chats/[id]/browser/route.ts +5 -1
  19. package/src/app/api/chats/[id]/chat/route.ts +7 -3
  20. package/src/app/api/chats/[id]/messages/route.ts +19 -13
  21. package/src/app/api/chats/[id]/route.ts +30 -0
  22. package/src/app/api/chats/[id]/stop/route.ts +6 -1
  23. package/src/app/api/chats/heartbeat/route.ts +2 -1
  24. package/src/app/api/chats/route.ts +23 -1
  25. package/src/app/api/connectors/[id]/doctor/route.ts +26 -0
  26. package/src/app/api/connectors/doctor/route.ts +13 -0
  27. package/src/app/api/external-agents/[id]/heartbeat/route.ts +33 -0
  28. package/src/app/api/external-agents/[id]/route.ts +31 -0
  29. package/src/app/api/external-agents/register/route.ts +3 -0
  30. package/src/app/api/external-agents/route.ts +66 -0
  31. package/src/app/api/files/open/route.ts +16 -14
  32. package/src/app/api/gateways/[id]/health/route.ts +28 -0
  33. package/src/app/api/gateways/[id]/route.ts +79 -0
  34. package/src/app/api/gateways/route.ts +57 -0
  35. package/src/app/api/memory/maintenance/route.ts +11 -1
  36. package/src/app/api/openclaw/agent-files/route.ts +27 -4
  37. package/src/app/api/openclaw/gateway/route.ts +10 -7
  38. package/src/app/api/openclaw/skills/route.ts +12 -4
  39. package/src/app/api/plugins/dependencies/route.ts +24 -0
  40. package/src/app/api/plugins/install/route.ts +15 -92
  41. package/src/app/api/plugins/route.ts +3 -26
  42. package/src/app/api/plugins/settings/route.ts +17 -12
  43. package/src/app/api/plugins/ui/route.ts +1 -0
  44. package/src/app/api/providers/[id]/discover-models/route.ts +27 -0
  45. package/src/app/api/schedules/[id]/route.ts +38 -9
  46. package/src/app/api/schedules/route.ts +51 -28
  47. package/src/app/api/settings/route.ts +55 -17
  48. package/src/app/api/setup/doctor/route.ts +6 -4
  49. package/src/app/api/tasks/[id]/route.ts +16 -6
  50. package/src/app/api/tasks/bulk/route.ts +3 -3
  51. package/src/app/api/tasks/route.ts +9 -4
  52. package/src/app/api/webhooks/[id]/route.ts +8 -1
  53. package/src/app/page.tsx +135 -17
  54. package/src/cli/binary.test.js +142 -0
  55. package/src/cli/index.js +38 -11
  56. package/src/cli/index.test.js +195 -0
  57. package/src/cli/index.ts +21 -12
  58. package/src/cli/server-cmd.test.js +59 -0
  59. package/src/cli/spec.js +20 -2
  60. package/src/components/agents/agent-card.tsx +15 -12
  61. package/src/components/agents/agent-chat-list.tsx +101 -1
  62. package/src/components/agents/agent-list.tsx +46 -9
  63. package/src/components/agents/agent-sheet.tsx +456 -23
  64. package/src/components/agents/inspector-panel.tsx +110 -49
  65. package/src/components/agents/sandbox-env-panel.tsx +4 -1
  66. package/src/components/auth/access-key-gate.tsx +36 -97
  67. package/src/components/auth/setup-wizard.tsx +970 -275
  68. package/src/components/chat/chat-area.tsx +70 -27
  69. package/src/components/chat/chat-card.tsx +6 -21
  70. package/src/components/chat/chat-header.tsx +263 -366
  71. package/src/components/chat/chat-list.tsx +62 -26
  72. package/src/components/chat/checkpoint-timeline.tsx +1 -1
  73. package/src/components/chat/message-list.tsx +145 -19
  74. package/src/components/chatrooms/chatroom-input.tsx +96 -33
  75. package/src/components/chatrooms/chatroom-list.tsx +141 -72
  76. package/src/components/chatrooms/chatroom-message.tsx +7 -6
  77. package/src/components/chatrooms/chatroom-sheet.tsx +13 -1
  78. package/src/components/chatrooms/chatroom-tool-request-banner.tsx +5 -2
  79. package/src/components/chatrooms/chatroom-view.tsx +422 -209
  80. package/src/components/chatrooms/reaction-picker.tsx +38 -33
  81. package/src/components/connectors/connector-list.tsx +265 -127
  82. package/src/components/connectors/connector-sheet.tsx +217 -0
  83. package/src/components/gateways/gateway-sheet.tsx +567 -0
  84. package/src/components/home/home-view.tsx +128 -4
  85. package/src/components/input/chat-input.tsx +135 -86
  86. package/src/components/layout/app-layout.tsx +385 -194
  87. package/src/components/layout/mobile-header.tsx +26 -8
  88. package/src/components/memory/memory-browser.tsx +71 -6
  89. package/src/components/memory/memory-card.tsx +18 -0
  90. package/src/components/memory/memory-detail.tsx +58 -31
  91. package/src/components/memory/memory-sheet.tsx +32 -4
  92. package/src/components/plugins/plugin-list.tsx +15 -3
  93. package/src/components/plugins/plugin-sheet.tsx +118 -9
  94. package/src/components/projects/project-detail.tsx +189 -1
  95. package/src/components/providers/provider-list.tsx +158 -2
  96. package/src/components/providers/provider-sheet.tsx +81 -70
  97. package/src/components/shared/agent-picker-list.tsx +2 -2
  98. package/src/components/shared/bottom-sheet.tsx +31 -15
  99. package/src/components/shared/command-palette.tsx +111 -24
  100. package/src/components/shared/confirm-dialog.tsx +45 -30
  101. package/src/components/shared/model-combobox.tsx +90 -8
  102. package/src/components/shared/settings/plugin-manager.tsx +20 -4
  103. package/src/components/shared/settings/section-capability-policy.tsx +105 -0
  104. package/src/components/shared/settings/section-heartbeat.tsx +88 -6
  105. package/src/components/shared/settings/section-orchestrator.tsx +6 -3
  106. package/src/components/shared/settings/section-runtime-loop.tsx +5 -5
  107. package/src/components/shared/settings/section-secrets.tsx +6 -6
  108. package/src/components/shared/settings/section-user-preferences.tsx +1 -1
  109. package/src/components/shared/settings/section-voice.tsx +5 -1
  110. package/src/components/shared/settings/section-web-search.tsx +10 -2
  111. package/src/components/shared/settings/settings-page.tsx +248 -47
  112. package/src/components/tasks/approvals-panel.tsx +211 -18
  113. package/src/components/tasks/task-board.tsx +242 -46
  114. package/src/components/ui/dialog.tsx +2 -2
  115. package/src/components/usage/metrics-dashboard.tsx +74 -1
  116. package/src/components/wallets/wallet-approval-dialog.tsx +59 -54
  117. package/src/components/wallets/wallet-panel.tsx +17 -5
  118. package/src/components/webhooks/webhook-sheet.tsx +7 -7
  119. package/src/lib/auth.ts +17 -0
  120. package/src/lib/chat-streaming-state.test.ts +108 -0
  121. package/src/lib/chat-streaming-state.ts +108 -0
  122. package/src/lib/heartbeat-defaults.ts +48 -0
  123. package/src/lib/memory-presentation.ts +59 -0
  124. package/src/lib/openclaw-agent-id.test.ts +14 -0
  125. package/src/lib/openclaw-agent-id.ts +31 -0
  126. package/src/lib/provider-model-discovery-client.ts +29 -0
  127. package/src/lib/providers/index.ts +12 -5
  128. package/src/lib/runtime-loop.ts +105 -3
  129. package/src/lib/safe-storage.ts +6 -1
  130. package/src/lib/server/agent-assignment.test.ts +112 -0
  131. package/src/lib/server/agent-assignment.ts +169 -0
  132. package/src/lib/server/agent-runtime-config.test.ts +141 -0
  133. package/src/lib/server/agent-runtime-config.ts +277 -0
  134. package/src/lib/server/approval-connector-notify.test.ts +253 -0
  135. package/src/lib/server/approvals-auto-approve.test.ts +264 -0
  136. package/src/lib/server/approvals.ts +483 -75
  137. package/src/lib/server/autonomy-runtime.test.ts +341 -0
  138. package/src/lib/server/browser-state.test.ts +118 -0
  139. package/src/lib/server/browser-state.ts +123 -0
  140. package/src/lib/server/build-llm.test.ts +44 -0
  141. package/src/lib/server/build-llm.ts +11 -4
  142. package/src/lib/server/builtin-plugins.ts +34 -0
  143. package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
  144. package/src/lib/server/chat-execution-tool-events.test.ts +219 -0
  145. package/src/lib/server/chat-execution.ts +402 -125
  146. package/src/lib/server/chatroom-health.test.ts +26 -0
  147. package/src/lib/server/chatroom-health.ts +2 -3
  148. package/src/lib/server/chatroom-helpers.test.ts +74 -2
  149. package/src/lib/server/chatroom-helpers.ts +144 -11
  150. package/src/lib/server/chatroom-session-persistence.test.ts +87 -0
  151. package/src/lib/server/connectors/discord.ts +175 -11
  152. package/src/lib/server/connectors/doctor.test.ts +80 -0
  153. package/src/lib/server/connectors/doctor.ts +116 -0
  154. package/src/lib/server/connectors/manager.ts +994 -130
  155. package/src/lib/server/connectors/policy.test.ts +222 -0
  156. package/src/lib/server/connectors/policy.ts +452 -0
  157. package/src/lib/server/connectors/slack.ts +189 -10
  158. package/src/lib/server/connectors/telegram.ts +65 -15
  159. package/src/lib/server/connectors/thread-context.test.ts +44 -0
  160. package/src/lib/server/connectors/thread-context.ts +72 -0
  161. package/src/lib/server/connectors/types.ts +41 -11
  162. package/src/lib/server/daemon-state.ts +62 -3
  163. package/src/lib/server/data-dir.ts +13 -0
  164. package/src/lib/server/delegation-jobs.test.ts +140 -0
  165. package/src/lib/server/delegation-jobs.ts +248 -0
  166. package/src/lib/server/document-utils.test.ts +47 -0
  167. package/src/lib/server/document-utils.ts +397 -0
  168. package/src/lib/server/eval/agent-regression.test.ts +47 -0
  169. package/src/lib/server/eval/agent-regression.ts +1742 -0
  170. package/src/lib/server/eval/runner.ts +11 -1
  171. package/src/lib/server/eval/store.ts +2 -1
  172. package/src/lib/server/heartbeat-service.ts +23 -43
  173. package/src/lib/server/heartbeat-source.test.ts +22 -0
  174. package/src/lib/server/heartbeat-source.ts +7 -0
  175. package/src/lib/server/identity-continuity.test.ts +77 -0
  176. package/src/lib/server/identity-continuity.ts +127 -0
  177. package/src/lib/server/mailbox-utils.ts +347 -0
  178. package/src/lib/server/main-agent-loop.ts +31 -964
  179. package/src/lib/server/memory-db.ts +4 -6
  180. package/src/lib/server/memory-tiers.ts +40 -0
  181. package/src/lib/server/openclaw-agent-resolver.test.ts +70 -0
  182. package/src/lib/server/openclaw-agent-resolver.ts +128 -0
  183. package/src/lib/server/openclaw-exec-config.ts +6 -5
  184. package/src/lib/server/openclaw-gateway.ts +123 -36
  185. package/src/lib/server/openclaw-skills-normalize.test.ts +56 -0
  186. package/src/lib/server/openclaw-skills-normalize.ts +136 -0
  187. package/src/lib/server/openclaw-sync.ts +3 -2
  188. package/src/lib/server/orchestrator-lg.ts +18 -8
  189. package/src/lib/server/orchestrator.ts +5 -4
  190. package/src/lib/server/playwright-proxy.mjs +27 -3
  191. package/src/lib/server/plugins.test.ts +215 -0
  192. package/src/lib/server/plugins.ts +832 -69
  193. package/src/lib/server/provider-health.ts +33 -3
  194. package/src/lib/server/provider-model-discovery.ts +481 -0
  195. package/src/lib/server/queue.ts +4 -21
  196. package/src/lib/server/runtime-settings.test.ts +119 -0
  197. package/src/lib/server/runtime-settings.ts +12 -92
  198. package/src/lib/server/schedule-normalization.ts +187 -0
  199. package/src/lib/server/scheduler.ts +2 -0
  200. package/src/lib/server/session-archive-memory.test.ts +85 -0
  201. package/src/lib/server/session-archive-memory.ts +230 -0
  202. package/src/lib/server/session-mailbox.ts +8 -18
  203. package/src/lib/server/session-reset-policy.test.ts +99 -0
  204. package/src/lib/server/session-reset-policy.ts +311 -0
  205. package/src/lib/server/session-run-manager.ts +33 -80
  206. package/src/lib/server/session-tools/autonomy-tools.test.ts +128 -0
  207. package/src/lib/server/session-tools/calendar.ts +2 -12
  208. package/src/lib/server/session-tools/connector.ts +109 -8
  209. package/src/lib/server/session-tools/context.ts +14 -2
  210. package/src/lib/server/session-tools/crawl.ts +447 -0
  211. package/src/lib/server/session-tools/crud.ts +96 -34
  212. package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
  213. package/src/lib/server/session-tools/delegate.ts +406 -20
  214. package/src/lib/server/session-tools/discovery-approvals.test.ts +170 -0
  215. package/src/lib/server/session-tools/discovery.ts +40 -12
  216. package/src/lib/server/session-tools/document.ts +283 -0
  217. package/src/lib/server/session-tools/email.ts +1 -3
  218. package/src/lib/server/session-tools/extract.ts +137 -0
  219. package/src/lib/server/session-tools/file-normalize.test.ts +98 -0
  220. package/src/lib/server/session-tools/file-send.test.ts +84 -1
  221. package/src/lib/server/session-tools/file.ts +243 -24
  222. package/src/lib/server/session-tools/http.ts +9 -3
  223. package/src/lib/server/session-tools/human-loop.ts +227 -0
  224. package/src/lib/server/session-tools/image-gen.ts +1 -3
  225. package/src/lib/server/session-tools/index.ts +87 -2
  226. package/src/lib/server/session-tools/mailbox.ts +276 -0
  227. package/src/lib/server/session-tools/manage-schedules.test.ts +137 -0
  228. package/src/lib/server/session-tools/memory.ts +35 -3
  229. package/src/lib/server/session-tools/monitor.ts +162 -12
  230. package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
  231. package/src/lib/server/session-tools/openclaw-nodes.test.ts +111 -0
  232. package/src/lib/server/session-tools/openclaw-nodes.ts +86 -20
  233. package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
  234. package/src/lib/server/session-tools/platform.ts +142 -4
  235. package/src/lib/server/session-tools/plugin-creator.ts +95 -25
  236. package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
  237. package/src/lib/server/session-tools/replicate.ts +1 -3
  238. package/src/lib/server/session-tools/sandbox.ts +51 -92
  239. package/src/lib/server/session-tools/schedule.ts +20 -10
  240. package/src/lib/server/session-tools/session-info.ts +58 -4
  241. package/src/lib/server/session-tools/session-tools-wiring.test.ts +54 -17
  242. package/src/lib/server/session-tools/shell.ts +2 -2
  243. package/src/lib/server/session-tools/subagent.ts +195 -27
  244. package/src/lib/server/session-tools/table.ts +587 -0
  245. package/src/lib/server/session-tools/wallet.ts +13 -10
  246. package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
  247. package/src/lib/server/session-tools/web.ts +947 -108
  248. package/src/lib/server/storage.ts +255 -10
  249. package/src/lib/server/stream-agent-chat.test.ts +61 -0
  250. package/src/lib/server/stream-agent-chat.ts +185 -25
  251. package/src/lib/server/structured-extract.test.ts +72 -0
  252. package/src/lib/server/structured-extract.ts +373 -0
  253. package/src/lib/server/task-mention.test.ts +16 -2
  254. package/src/lib/server/task-mention.ts +61 -11
  255. package/src/lib/server/tool-aliases.ts +80 -12
  256. package/src/lib/server/tool-capability-policy.ts +7 -1
  257. package/src/lib/server/tool-retry.ts +2 -0
  258. package/src/lib/server/watch-jobs.test.ts +173 -0
  259. package/src/lib/server/watch-jobs.ts +532 -0
  260. package/src/lib/server/ws-hub.ts +5 -3
  261. package/src/lib/setup-defaults.ts +352 -11
  262. package/src/lib/tool-definitions.ts +3 -4
  263. package/src/lib/validation/schemas.test.ts +26 -0
  264. package/src/lib/validation/schemas.ts +62 -1
  265. package/src/lib/ws-client.ts +14 -12
  266. package/src/proxy.ts +5 -5
  267. package/src/stores/use-app-store.ts +43 -7
  268. package/src/stores/use-chat-store.ts +31 -2
  269. package/src/stores/use-chatroom-store.ts +153 -26
  270. package/src/types/index.ts +470 -44
  271. package/src/app/api/chats/[id]/main-loop/route.ts +0 -94
  272. package/src/components/chat/new-chat-sheet.tsx +0 -253
  273. package/src/lib/server/main-session.ts +0 -17
  274. 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
- /** Subscribes up to 6 member agents to heartbeat topics */
55
- function AgentHeartbeatListeners({ agentIds, onPulse }: { agentIds: string[]; onPulse: (id: string) => void }) {
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
- const GROUP_THRESHOLD_MS = 2 * 60 * 1000 // 2 minutes
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
- // eslint-disable-next-line react-hooks/exhaustive-deps
146
- }, [])
149
+ }, [loadChatrooms])
147
150
 
148
151
  useWs(currentChatroomId ? `chatroom:${currentChatroomId}` : '', refreshChatroom)
149
152
 
150
- // Smooth auto-scroll on new messages
151
- useEffect(() => {
152
- if (scrollRef.current) {
153
- scrollRef.current.scrollTo({ top: scrollRef.current.scrollHeight, behavior: 'smooth' })
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
- }, [chatroom?.messages.length, streamingAgents.size])
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
- const memberAgents = chatroom
158
- ? (chatroom.agentIds
159
- .map((id) => agents[id])
160
- .filter(Boolean) as Agent[])
161
- : []
193
+ useEffect(() => {
194
+ if (!chatroomId) return
195
+ markChatRead(chatroomId)
196
+ }, [chatroomId, markChatRead])
162
197
 
163
- const streamingAgentIds = new Set(streamingAgents.keys())
164
- const pinnedIds = chatroom?.pinnedMessageIds || []
165
- const pinnedMessages = chatroom
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
- // Heartbeat subscriptions for up to 6 member agents
170
- const memberAgentIds = chatroom?.agentIds.slice(0, 6) || []
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 flex-col h-full min-w-0">
198
- {/* Header */}
199
- <div className="flex items-center gap-3 px-4 py-3 border-b border-white/[0.06] shrink-0">
200
- <div className="w-8 h-8 rounded-full bg-accent-soft flex items-center justify-center shrink-0">
201
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-accent-bright">
202
- <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
203
- </svg>
204
- </div>
205
- <div className="flex-1 min-w-0">
206
- <h3 className="text-[14px] font-700 text-text truncate">{chatroom.name}</h3>
207
- <p className="text-[11px] text-text-3 truncate">
208
- {memberAgents.length} agent{memberAgents.length !== 1 ? 's' : ''}
209
- {chatroom.description ? ` · ${chatroom.description}` : ''}
210
- </p>
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
- {/* Member avatars with role badges */}
213
- <div className="flex -space-x-1.5 shrink-0">
214
- {memberAgents.slice(0, 5).map((agent) => {
215
- const role = getMemberRole(chatroom, agent.id)
216
- const badge = getRoleBadge(role)
217
- const muted = isAgentMuted(chatroom, agent.id)
218
- return (
219
- <Tooltip key={agent.id}>
220
- <TooltipTrigger asChild>
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
- onClick={() => navigateToAgent(agent.id)}
223
- 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' : ''}`}
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
- <AgentAvatar seed={agent.avatarSeed} avatarUrl={agent.avatarUrl} name={agent.name} size={22} status={streamingAgents.has(agent.id) ? 'busy' : 'online'} />
226
- {badge && (
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
- </TooltipTrigger>
233
- <TooltipContent side="bottom" sideOffset={6}>
234
- <div className="flex items-center gap-1.5">
235
- <span>{agent.name}</span>
236
- {badge && <span className={`text-[9px] font-600 px-1 py-0.5 rounded border ${badge.className}`}>{badge.label}</span>}
237
- {muted && <span className="text-[9px] text-red-400">Muted</span>}
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
- </TooltipContent>
240
- </Tooltip>
241
- )
242
- })}
243
- {memberAgents.length > 5 && (
244
- <div className="w-[22px] h-[22px] rounded-full bg-white/[0.08] flex items-center justify-center text-[9px] text-text-3">
245
- +{memberAgents.length - 5}
246
- </div>
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
- <button
250
- onClick={() => {
251
- setEditingChatroomId(chatroom.id)
252
- setChatroomSheetOpen(true)
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
- className="shrink-0 w-7 h-7 rounded-full flex items-center justify-center hover:bg-white/[0.08] transition-all cursor-pointer"
255
- >
256
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-text-3">
257
- <circle cx="12" cy="12" r="3" />
258
- <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" />
259
- </svg>
260
- </button>
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
- {/* Pinned messages bar */}
264
- {pinnedMessages.length > 0 && (
265
- <div className="border-b border-white/[0.06] shrink-0">
266
- <button
267
- onClick={() => setPinsExpanded(!pinsExpanded)}
268
- 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"
269
- style={{ fontFamily: 'inherit' }}
270
- >
271
- <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">
272
- <path d="M12 17v5" />
273
- <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" />
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={pm.id}
285
- onClick={() => {
286
- const el = document.getElementById(`chatroom-msg-${pm.id}`)
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
- <span className="text-[11px] font-600 text-accent-bright shrink-0">{pm.senderName}</span>
297
- <span className="text-[11px] text-text-3 truncate flex-1">{pm.text.slice(0, 80)}</span>
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
- </div>
301
- )}
302
- </div>
303
- )}
304
-
305
- <AgentHeartbeatListeners agentIds={memberAgentIds} onPulse={handleHeartbeatPulse} />
573
+ )
574
+ })}
575
+ </div>
576
+ </section>
306
577
 
307
- {/* Messages */}
308
- <div ref={scrollRef} className="flex-1 overflow-y-auto py-3">
309
- {chatroom.messages.length === 0 ? (
310
- <div className="flex items-center justify-center h-full px-6">
311
- <div className="text-center">
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
- </div>
316
- ) : (
317
- chatroom.messages.map((msg, i) => {
318
- const prev = i > 0 ? chatroom.messages[i - 1] : null
319
- const isGrouped = prev
320
- ? prev.senderId === msg.senderId && (msg.time - prev.time) < GROUP_THRESHOLD_MS
321
- : false
322
- // Day separator: show when the date changes between messages
323
- const prevDay = prev ? new Date(prev.time).toDateString() : null
324
- const msgDay = new Date(msg.time).toDateString()
325
- const showDaySep = !prev || prevDay !== msgDay
326
-
327
- // Moment overlay — show on the last message from each agent that has an active moment
328
- const senderId = msg.senderId
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
  }