@swarmclawai/swarmclaw 0.7.2 → 0.7.3

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