@swarmclawai/swarmclaw 0.6.6 → 0.6.7

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 (80) hide show
  1. package/README.md +57 -27
  2. package/package.json +6 -1
  3. package/src/app/api/agents/[id]/clone/route.ts +40 -0
  4. package/src/app/api/agents/route.ts +39 -14
  5. package/src/app/api/chatrooms/[id]/chat/route.ts +17 -1
  6. package/src/app/api/chatrooms/[id]/moderate/route.ts +150 -0
  7. package/src/app/api/chatrooms/[id]/route.ts +19 -1
  8. package/src/app/api/chatrooms/route.ts +12 -2
  9. package/src/app/api/connectors/[id]/health/route.ts +64 -0
  10. package/src/app/api/connectors/route.ts +17 -2
  11. package/src/app/api/knowledge/route.ts +6 -1
  12. package/src/app/api/openclaw/doctor/route.ts +17 -0
  13. package/src/app/api/sessions/[id]/chat/route.ts +5 -1
  14. package/src/app/api/sessions/route.ts +11 -2
  15. package/src/app/api/tasks/[id]/route.ts +18 -13
  16. package/src/app/api/tasks/route.ts +20 -1
  17. package/src/app/api/usage/route.ts +16 -7
  18. package/src/cli/index.js +5 -0
  19. package/src/cli/index.ts +223 -39
  20. package/src/components/agents/agent-card.tsx +37 -6
  21. package/src/components/agents/agent-chat-list.tsx +78 -2
  22. package/src/components/agents/agent-sheet.tsx +79 -0
  23. package/src/components/auth/setup-wizard.tsx +268 -353
  24. package/src/components/chat/chat-area.tsx +22 -7
  25. package/src/components/chat/message-bubble.tsx +14 -14
  26. package/src/components/chat/message-list.tsx +1 -1
  27. package/src/components/chatrooms/chatroom-message.tsx +164 -22
  28. package/src/components/chatrooms/chatroom-sheet.tsx +288 -3
  29. package/src/components/chatrooms/chatroom-view.tsx +62 -17
  30. package/src/components/connectors/connector-health.tsx +120 -0
  31. package/src/components/connectors/connector-sheet.tsx +9 -0
  32. package/src/components/home/home-view.tsx +23 -2
  33. package/src/components/input/chat-input.tsx +8 -1
  34. package/src/components/layout/app-layout.tsx +17 -1
  35. package/src/components/schedules/schedule-list.tsx +55 -9
  36. package/src/components/schedules/schedule-sheet.tsx +134 -23
  37. package/src/components/shared/command-palette.tsx +237 -0
  38. package/src/components/shared/connector-platform-icon.tsx +1 -0
  39. package/src/components/tasks/task-card.tsx +22 -2
  40. package/src/components/tasks/task-sheet.tsx +91 -16
  41. package/src/components/usage/metrics-dashboard.tsx +13 -25
  42. package/src/hooks/use-swipe.ts +49 -0
  43. package/src/lib/providers/anthropic.ts +16 -2
  44. package/src/lib/providers/claude-cli.ts +7 -1
  45. package/src/lib/providers/index.ts +7 -0
  46. package/src/lib/providers/ollama.ts +16 -2
  47. package/src/lib/providers/openai.ts +7 -2
  48. package/src/lib/providers/openclaw.ts +6 -1
  49. package/src/lib/providers/provider-defaults.ts +7 -0
  50. package/src/lib/schedule-templates.ts +115 -0
  51. package/src/lib/server/alert-dispatch.ts +64 -0
  52. package/src/lib/server/chat-execution.ts +41 -1
  53. package/src/lib/server/chatroom-helpers.ts +22 -1
  54. package/src/lib/server/chatroom-routing.ts +65 -0
  55. package/src/lib/server/connectors/discord.ts +3 -0
  56. package/src/lib/server/connectors/email.ts +267 -0
  57. package/src/lib/server/connectors/manager.ts +159 -3
  58. package/src/lib/server/connectors/openclaw.ts +3 -0
  59. package/src/lib/server/connectors/slack.ts +6 -0
  60. package/src/lib/server/connectors/telegram.ts +18 -0
  61. package/src/lib/server/connectors/types.ts +2 -0
  62. package/src/lib/server/connectors/whatsapp.ts +9 -0
  63. package/src/lib/server/cost.ts +70 -0
  64. package/src/lib/server/create-notification.ts +2 -0
  65. package/src/lib/server/daemon-state.ts +124 -0
  66. package/src/lib/server/dag-validation.ts +115 -0
  67. package/src/lib/server/memory-db.ts +12 -7
  68. package/src/lib/server/openclaw-doctor.ts +48 -0
  69. package/src/lib/server/queue.ts +12 -0
  70. package/src/lib/server/session-run-manager.ts +22 -1
  71. package/src/lib/server/session-tools/index.ts +2 -0
  72. package/src/lib/server/session-tools/memory.ts +22 -3
  73. package/src/lib/server/session-tools/openclaw-workspace.ts +132 -0
  74. package/src/lib/server/storage.ts +120 -6
  75. package/src/lib/setup-defaults.ts +277 -0
  76. package/src/lib/validation/schemas.ts +69 -0
  77. package/src/stores/use-app-store.ts +7 -3
  78. package/src/stores/use-chatroom-store.ts +52 -2
  79. package/src/types/index.ts +38 -1
  80. package/tsconfig.json +2 -1
@@ -324,17 +324,32 @@ export function ChatArea() {
324
324
  <DevServerBar status={devServerStatus} onStop={handleStopDevServer} />
325
325
 
326
326
  {messagesLoading && !messages.length ? (
327
- <div className="flex-1 flex items-center justify-center">
328
- <div className="flex flex-col items-center gap-3" style={{ animation: 'fade-in 0.2s ease' }}>
329
- <div className="relative w-10 h-10">
330
- <div className="absolute inset-0 rounded-full border-2 border-white/[0.06]" />
331
- <div className="absolute inset-0 rounded-full border-2 border-transparent border-t-accent-bright animate-spin" />
327
+ <div className="flex-1 flex flex-col gap-5 px-4 md:px-12 lg:px-16 py-8" style={{ animation: 'fade-in 0.2s ease' }}>
328
+ {/* Skeleton message bubbles */}
329
+ <div className="flex gap-3 max-w-[70%]">
330
+ <div className="w-8 h-8 rounded-full bg-white/[0.06] animate-pulse shrink-0" />
331
+ <div className="flex-1 space-y-2">
332
+ <div className="h-3 w-24 rounded bg-white/[0.06] animate-pulse" />
333
+ <div className="h-16 rounded-[12px] bg-white/[0.04] animate-pulse" />
334
+ </div>
335
+ </div>
336
+ <div className="flex gap-3 max-w-[60%] self-end flex-row-reverse">
337
+ <div className="w-8 h-8 rounded-full bg-white/[0.06] animate-pulse shrink-0" />
338
+ <div className="flex-1 space-y-2">
339
+ <div className="h-3 w-16 rounded bg-white/[0.06] animate-pulse ml-auto" />
340
+ <div className="h-10 rounded-[12px] bg-white/[0.04] animate-pulse" />
341
+ </div>
342
+ </div>
343
+ <div className="flex gap-3 max-w-[65%]">
344
+ <div className="w-8 h-8 rounded-full bg-white/[0.06] animate-pulse shrink-0" />
345
+ <div className="flex-1 space-y-2">
346
+ <div className="h-3 w-20 rounded bg-white/[0.06] animate-pulse" />
347
+ <div className="h-24 rounded-[12px] bg-white/[0.04] animate-pulse" />
332
348
  </div>
333
- <span className="text-[13px] text-text-3/50 font-500">Loading messages...</span>
334
349
  </div>
335
350
  </div>
336
351
  ) : isEmpty ? (
337
- <div className="flex-1 flex flex-col items-center justify-center px-6 pb-4 relative">
352
+ <div className="flex-1 flex flex-col items-center justify-center px-6 pb-[120px] md:pb-4 relative">
338
353
  {/* Atmospheric background glow */}
339
354
  <div className="absolute inset-0 pointer-events-none overflow-hidden">
340
355
  <div className="absolute top-[20%] left-[50%] -translate-x-1/2 w-[500px] h-[300px]"
@@ -516,7 +516,7 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
516
516
  )}
517
517
  </div>
518
518
  ) : (
519
- <div className={`msg-content text-[15px] break-words ${isUser ? 'leading-[1.6] text-white/95' : 'leading-[1.7] text-text'}`}>
519
+ <div className={`msg-content text-[15px] md:text-[14px] break-words ${isUser ? 'leading-[1.6] text-white/95' : 'leading-[1.7] text-text'}`}>
520
520
  <ReactMarkdown
521
521
  remarkPlugins={[remarkGfm]}
522
522
  rehypePlugins={[rehypeHighlight]}
@@ -660,12 +660,12 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
660
660
  )}
661
661
 
662
662
  {/* Action buttons */}
663
- <div className={`flex items-center gap-1 mt-1.5 px-1 opacity-0 group-hover:opacity-100 transition-opacity duration-200 ${isUser ? 'justify-end' : ''}`}>
663
+ <div className={`flex items-center gap-1 mt-1.5 px-1 opacity-100 md:opacity-0 md:group-hover:opacity-100 transition-opacity duration-200 ${isUser ? 'justify-end' : ''}`}>
664
664
  <button
665
665
  onClick={handleCopy}
666
666
  aria-label="Copy message"
667
- className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-[8px] border-none bg-transparent
668
- text-[11px] font-500 text-text-3 cursor-pointer hover:text-text-2 hover:bg-white/[0.04] transition-all"
667
+ className="flex items-center gap-1.5 px-2.5 py-1.5 min-h-[44px] min-w-[44px] md:min-h-0 md:min-w-0 rounded-[8px] border-none bg-transparent
668
+ text-[11px] font-500 text-text-3 cursor-pointer hover:text-text-2 hover:bg-white/[0.04] transition-all justify-center md:justify-start"
669
669
  style={{ fontFamily: 'inherit' }}
670
670
  >
671
671
  <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
@@ -678,8 +678,8 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
678
678
  <button
679
679
  onClick={() => onToggleBookmark(messageIndex)}
680
680
  aria-label={message.bookmarked ? 'Remove bookmark' : 'Bookmark message'}
681
- className={`flex items-center gap-1.5 px-2.5 py-1.5 rounded-[8px] border-none bg-transparent
682
- text-[11px] font-500 cursor-pointer hover:bg-white/[0.04] transition-all ${message.bookmarked ? 'text-amber-400' : ''}`}
681
+ className={`flex items-center gap-1.5 px-2.5 py-1.5 min-h-[44px] min-w-[44px] md:min-h-0 md:min-w-0 rounded-[8px] border-none bg-transparent
682
+ text-[11px] font-500 cursor-pointer hover:bg-white/[0.04] transition-all justify-center md:justify-start ${message.bookmarked ? 'text-amber-400' : ''}`}
683
683
  style={{ fontFamily: 'inherit' }}
684
684
  >
685
685
  <svg width="12" height="12" viewBox="0 0 24 24" fill={message.bookmarked ? 'currentColor' : 'none'} stroke="currentColor" strokeWidth="2" strokeLinecap="round">
@@ -692,8 +692,8 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
692
692
  <button
693
693
  onClick={() => { setEditText(message.text); setEditing(true) }}
694
694
  aria-label="Edit and resend"
695
- className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-[8px] border-none bg-transparent
696
- text-[11px] font-500 text-text-3 cursor-pointer hover:text-text-2 hover:bg-white/[0.04] transition-all"
695
+ className="flex items-center gap-1.5 px-2.5 py-1.5 min-h-[44px] min-w-[44px] md:min-h-0 md:min-w-0 rounded-[8px] border-none bg-transparent
696
+ text-[11px] font-500 text-text-3 cursor-pointer hover:text-text-2 hover:bg-white/[0.04] transition-all justify-center md:justify-start"
697
697
  style={{ fontFamily: 'inherit' }}
698
698
  >
699
699
  <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
@@ -707,8 +707,8 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
707
707
  <button
708
708
  onClick={() => onFork(messageIndex)}
709
709
  aria-label="Fork conversation from here"
710
- className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-[8px] border-none bg-transparent
711
- text-[11px] font-500 text-text-3 cursor-pointer hover:text-text-2 hover:bg-white/[0.04] transition-all"
710
+ className="flex items-center gap-1.5 px-2.5 py-1.5 min-h-[44px] min-w-[44px] md:min-h-0 md:min-w-0 rounded-[8px] border-none bg-transparent
711
+ text-[11px] font-500 text-text-3 cursor-pointer hover:text-text-2 hover:bg-white/[0.04] transition-all justify-center md:justify-start"
712
712
  style={{ fontFamily: 'inherit' }}
713
713
  >
714
714
  <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
@@ -725,8 +725,8 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
725
725
  <button
726
726
  onClick={onRetry}
727
727
  aria-label="Retry message"
728
- className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-[8px] border-none bg-transparent
729
- text-[11px] font-500 text-text-3 cursor-pointer hover:text-text-2 hover:bg-white/[0.04] transition-all"
728
+ className="flex items-center gap-1.5 px-2.5 py-1.5 min-h-[44px] min-w-[44px] md:min-h-0 md:min-w-0 rounded-[8px] border-none bg-transparent
729
+ text-[11px] font-500 text-text-3 cursor-pointer hover:text-text-2 hover:bg-white/[0.04] transition-all justify-center md:justify-start"
730
730
  style={{ fontFamily: 'inherit' }}
731
731
  >
732
732
  <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
@@ -741,8 +741,8 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
741
741
  <button
742
742
  onClick={() => setTransferPickerOpen(!transferPickerOpen)}
743
743
  aria-label="Transfer to another agent"
744
- className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-[8px] border-none bg-transparent
745
- text-[11px] font-500 text-text-3 cursor-pointer hover:text-text-2 hover:bg-white/[0.04] transition-all"
744
+ className="flex items-center gap-1.5 px-2.5 py-1.5 min-h-[44px] min-w-[44px] md:min-h-0 md:min-w-0 rounded-[8px] border-none bg-transparent
745
+ text-[11px] font-500 text-text-3 cursor-pointer hover:text-text-2 hover:bg-white/[0.04] transition-all justify-center md:justify-start"
746
746
  style={{ fontFamily: 'inherit' }}
747
747
  >
748
748
  <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
@@ -397,7 +397,7 @@ export function MessageList({ messages, streaming, connectorFilter = null }: Pro
397
397
  <div
398
398
  ref={scrollRef}
399
399
  onScroll={updateScrollState}
400
- className="flex-1 min-h-0 overflow-y-auto overflow-x-hidden px-6 md:px-12 lg:px-16 pt-6 pb-10 fade-up"
400
+ className="flex-1 min-h-0 overflow-y-auto overflow-x-hidden px-4 md:px-12 lg:px-16 pt-6 pb-[120px] md:pb-10 fade-up"
401
401
  >
402
402
  <div className="flex flex-col gap-6 relative">
403
403
  {/* Chat spine — vertical line for assistant messages */}
@@ -15,7 +15,7 @@ import { ChatroomToolRequestBanner } from './chatroom-tool-request-banner'
15
15
  import { isStructuredMarkdown } from '@/components/chat/markdown-utils'
16
16
  import { TransferAgentPicker } from '@/components/chat/transfer-agent-picker'
17
17
  import { ConnectorPlatformIcon, CONNECTOR_PLATFORM_META } from '@/components/shared/connector-platform-icon'
18
- import type { ChatroomMessage, Agent } from '@/types'
18
+ import type { ChatroomMessage, Chatroom, Agent } from '@/types'
19
19
 
20
20
  interface Props {
21
21
  message: ChatroomMessage
@@ -24,6 +24,11 @@ interface Props {
24
24
  onReply?: (message: ChatroomMessage) => void
25
25
  onTogglePin?: (messageId: string) => void
26
26
  onTransfer?: (messageId: string, targetAgentId: string) => void
27
+ onDeleteMessage?: (messageId: string, targetAgentId: string) => void
28
+ onMuteAgent?: (agentId: string) => void
29
+ onUnmuteAgent?: (agentId: string) => void
30
+ onSetRole?: (agentId: string, role: 'admin' | 'moderator' | 'member') => void
31
+ chatroom?: Chatroom
27
32
  pinnedMessageIds?: string[]
28
33
  /** Set of agentIds currently streaming */
29
34
  streamingAgentIds?: Set<string>
@@ -51,6 +56,25 @@ function navigateToAgent(agentId: string) {
51
56
  useAppStore.getState().setCurrentAgent(agentId)
52
57
  }
53
58
 
59
+ function getMemberRoleFromChatroom(chatroom: Chatroom | undefined, agentId: string): string {
60
+ if (!chatroom?.members?.length) return 'member'
61
+ const member = chatroom.members.find((m) => m.agentId === agentId)
62
+ return member?.role || 'member'
63
+ }
64
+
65
+ function isAgentMutedInChatroom(chatroom: Chatroom | undefined, agentId: string): boolean {
66
+ if (!chatroom?.members?.length) return false
67
+ const member = chatroom.members.find((m) => m.agentId === agentId)
68
+ if (!member?.mutedUntil) return false
69
+ return new Date(member.mutedUntil).getTime() > Date.now()
70
+ }
71
+
72
+ function roleBadgeStyle(role: string): { label: string; className: string } | null {
73
+ if (role === 'admin') return { label: 'Admin', className: 'bg-purple-500/20 text-purple-400 border-purple-500/30' }
74
+ if (role === 'moderator') return { label: 'Mod', className: 'bg-blue-500/20 text-blue-400 border-blue-500/30' }
75
+ return null
76
+ }
77
+
54
78
  /** Pre-process @mentions into markdown-friendly format for ReactMarkdown */
55
79
  function preprocessMentions(text: string, agents: Record<string, Agent>): string {
56
80
  const nameToId = new Map<string, string>()
@@ -111,9 +135,10 @@ function renderChatroomAttachments(message: ChatroomMessage) {
111
135
  )
112
136
  }
113
137
 
114
- export function ChatroomMessageBubble({ message, agents, onToggleReaction, onReply, onTogglePin, onTransfer, pinnedMessageIds, streamingAgentIds, messages, grouped: isGrouped, momentOverlay }: Props) {
138
+ export function ChatroomMessageBubble({ message, agents, onToggleReaction, onReply, onTogglePin, onTransfer, onDeleteMessage, onMuteAgent, onUnmuteAgent, onSetRole, chatroom, pinnedMessageIds, streamingAgentIds, messages, grouped: isGrouped, momentOverlay }: Props) {
115
139
  const [showPicker, setShowPicker] = useState(false)
116
140
  const [showTransferPicker, setShowTransferPicker] = useState(false)
141
+ const [showModMenu, setShowModMenu] = useState(false)
117
142
  const userAvatarSeed = useAppStore((s) => s.appSettings.userAvatarSeed)
118
143
  const wide = isStructuredMarkdown(message.text)
119
144
 
@@ -185,26 +210,41 @@ export function ChatroomMessageBubble({ message, agents, onToggleReaction, onRep
185
210
 
186
211
  {/* Content */}
187
212
  <div className="flex-1 min-w-0">
188
- {!isGrouped && (
189
- <div className="flex items-baseline gap-2 mb-0.5">
190
- {!isUser && agent ? (
191
- <AgentHoverCard agent={agent}>
192
- <span className="text-[13px] font-600 text-accent-bright hover:underline cursor-pointer flex items-center gap-1.5">
213
+ {!isGrouped && (() => {
214
+ const role = !isUser ? getMemberRoleFromChatroom(chatroom, message.senderId) : 'member'
215
+ const badge = !isUser ? roleBadgeStyle(role) : null
216
+ const muted = !isUser ? isAgentMutedInChatroom(chatroom, message.senderId) : false
217
+ return (
218
+ <div className="flex items-baseline gap-2 mb-0.5">
219
+ {!isUser && agent ? (
220
+ <AgentHoverCard agent={agent}>
221
+ <span className="text-[13px] font-600 text-accent-bright hover:underline cursor-pointer flex items-center gap-1.5">
222
+ {message.source && <ConnectorPlatformIcon platform={message.source.platform} size={12} />}
223
+ {message.senderName}
224
+ </span>
225
+ </AgentHoverCard>
226
+ ) : (
227
+ <span className="text-[13px] font-600 text-text flex items-center gap-1.5">
193
228
  {message.source && <ConnectorPlatformIcon platform={message.source.platform} size={12} />}
194
- {message.senderName}
229
+ {isUser && message.source?.senderName
230
+ ? `${message.source.senderName} via ${CONNECTOR_PLATFORM_META[message.source.platform]?.label || message.source.platform}`
231
+ : message.senderName}
195
232
  </span>
196
- </AgentHoverCard>
197
- ) : (
198
- <span className="text-[13px] font-600 text-text flex items-center gap-1.5">
199
- {message.source && <ConnectorPlatformIcon platform={message.source.platform} size={12} />}
200
- {isUser && message.source?.senderName
201
- ? `${message.source.senderName} via ${CONNECTOR_PLATFORM_META[message.source.platform]?.label || message.source.platform}`
202
- : message.senderName}
203
- </span>
204
- )}
205
- <span className="label-mono" title={new Date(message.time).toLocaleString()}>{formatRelativeTime(message.time)}</span>
206
- </div>
207
- )}
233
+ )}
234
+ {badge && (
235
+ <span className={`text-[9px] font-600 px-1 py-0.5 rounded border leading-none ${badge.className}`}>
236
+ {badge.label}
237
+ </span>
238
+ )}
239
+ {muted && (
240
+ <span className="text-[9px] font-600 px-1 py-0.5 rounded border leading-none bg-red-500/20 text-red-400 border-red-500/30">
241
+ Muted
242
+ </span>
243
+ )}
244
+ <span className="label-mono" title={new Date(message.time).toLocaleString()}>{formatRelativeTime(message.time)}</span>
245
+ </div>
246
+ )
247
+ })()}
208
248
 
209
249
  {/* Reply quote */}
210
250
  {replyToMessage && (
@@ -352,8 +392,8 @@ export function ChatroomMessageBubble({ message, agents, onToggleReaction, onRep
352
392
  )}
353
393
  </div>
354
394
 
355
- {/* Action buttons (reply + pin + transfer + reaction) */}
356
- <div className="relative shrink-0 mt-0.5 flex items-start gap-0.5" style={{ zIndex: showPicker || showTransferPicker ? 50 : undefined }}>
395
+ {/* Action buttons (reply + pin + transfer + moderate + reaction) */}
396
+ <div className="relative shrink-0 mt-0.5 flex items-start gap-0.5" style={{ zIndex: showPicker || showTransferPicker || showModMenu ? 50 : undefined }}>
357
397
  {/* Reply button */}
358
398
  {onReply && (
359
399
  <button
@@ -405,6 +445,108 @@ export function ChatroomMessageBubble({ message, agents, onToggleReaction, onRep
405
445
  onClose={() => setShowTransferPicker(false)}
406
446
  />
407
447
  )}
448
+ {/* Moderation menu button */}
449
+ {!isUser && (onDeleteMessage || onMuteAgent || onSetRole) && (
450
+ <button
451
+ onClick={() => setShowModMenu(!showModMenu)}
452
+ className="opacity-0 group-hover:opacity-100 w-6 h-6 rounded-full flex items-center justify-center hover:bg-white/[0.08] transition-all cursor-pointer"
453
+ title="Moderate"
454
+ >
455
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-text-3">
456
+ <path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
457
+ </svg>
458
+ </button>
459
+ )}
460
+ {showModMenu && !isUser && (
461
+ <div className="absolute right-0 top-7 z-50 bg-[#1a1a2e] border border-white/[0.1] rounded-[8px] shadow-lg py-1 min-w-[160px]">
462
+ {onDeleteMessage && (
463
+ <button
464
+ onClick={() => {
465
+ onDeleteMessage(message.id, message.senderId)
466
+ setShowModMenu(false)
467
+ }}
468
+ className="w-full flex items-center gap-2 px-3 py-1.5 text-[12px] text-red-400 hover:bg-white/[0.06] transition-colors cursor-pointer bg-transparent border-none text-left"
469
+ style={{ fontFamily: 'inherit' }}
470
+ >
471
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
472
+ <polyline points="3 6 5 6 21 6" />
473
+ <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
474
+ </svg>
475
+ Delete message
476
+ </button>
477
+ )}
478
+ {onMuteAgent && onUnmuteAgent && (() => {
479
+ const muted = isAgentMutedInChatroom(chatroom, message.senderId)
480
+ return muted ? (
481
+ <button
482
+ onClick={() => {
483
+ onUnmuteAgent(message.senderId)
484
+ setShowModMenu(false)
485
+ }}
486
+ className="w-full flex items-center gap-2 px-3 py-1.5 text-[12px] text-green-400 hover:bg-white/[0.06] transition-colors cursor-pointer bg-transparent border-none text-left"
487
+ style={{ fontFamily: 'inherit' }}
488
+ >
489
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
490
+ <polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5" />
491
+ <path d="M19.07 4.93a10 10 0 0 1 0 14.14" />
492
+ </svg>
493
+ Unmute agent
494
+ </button>
495
+ ) : (
496
+ <button
497
+ onClick={() => {
498
+ onMuteAgent(message.senderId)
499
+ setShowModMenu(false)
500
+ }}
501
+ className="w-full flex items-center gap-2 px-3 py-1.5 text-[12px] text-amber-400 hover:bg-white/[0.06] transition-colors cursor-pointer bg-transparent border-none text-left"
502
+ style={{ fontFamily: 'inherit' }}
503
+ >
504
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
505
+ <polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5" />
506
+ <line x1="23" y1="9" x2="17" y2="15" />
507
+ <line x1="17" y1="9" x2="23" y2="15" />
508
+ </svg>
509
+ Mute 30 min
510
+ </button>
511
+ )
512
+ })()}
513
+ {onSetRole && (() => {
514
+ const currentRole = getMemberRoleFromChatroom(chatroom, message.senderId)
515
+ const roleOptions: Array<{ value: 'admin' | 'moderator' | 'member'; label: string }> = [
516
+ { value: 'admin', label: 'Set Admin' },
517
+ { value: 'moderator', label: 'Set Moderator' },
518
+ { value: 'member', label: 'Set Member' },
519
+ ]
520
+ return roleOptions
521
+ .filter((opt) => opt.value !== currentRole)
522
+ .map((opt) => (
523
+ <button
524
+ key={opt.value}
525
+ onClick={() => {
526
+ onSetRole(message.senderId, opt.value)
527
+ setShowModMenu(false)
528
+ }}
529
+ className="w-full flex items-center gap-2 px-3 py-1.5 text-[12px] text-text-2 hover:bg-white/[0.06] transition-colors cursor-pointer bg-transparent border-none text-left"
530
+ style={{ fontFamily: 'inherit' }}
531
+ >
532
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
533
+ <path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
534
+ <circle cx="8.5" cy="7" r="4" />
535
+ <polyline points="17 11 19 13 23 9" />
536
+ </svg>
537
+ {opt.label}
538
+ </button>
539
+ ))
540
+ })()}
541
+ <button
542
+ onClick={() => setShowModMenu(false)}
543
+ className="w-full px-3 py-1.5 text-[11px] text-text-3 hover:bg-white/[0.06] transition-colors cursor-pointer bg-transparent border-none text-left border-t border-white/[0.06] mt-1"
544
+ style={{ fontFamily: 'inherit' }}
545
+ >
546
+ Cancel
547
+ </button>
548
+ </div>
549
+ )}
408
550
  {/* Reaction button */}
409
551
  <button
410
552
  onClick={() => setShowPicker(!showPicker)}