@swarmclawai/swarmclaw 0.7.3 → 0.7.5

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 (152) hide show
  1. package/README.md +47 -40
  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 +17 -0
  11. package/src/app/api/agents/[id]/thread/route.ts +4 -87
  12. package/src/app/api/agents/route.ts +23 -1
  13. package/src/app/api/auth/route.ts +1 -1
  14. package/src/app/api/chatrooms/[id]/chat/route.ts +16 -5
  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]/route.ts +12 -0
  19. package/src/app/api/chats/heartbeat/route.ts +2 -1
  20. package/src/app/api/chats/route.ts +7 -1
  21. package/src/app/api/external-agents/[id]/heartbeat/route.ts +33 -0
  22. package/src/app/api/external-agents/[id]/route.ts +31 -0
  23. package/src/app/api/external-agents/register/route.ts +3 -0
  24. package/src/app/api/external-agents/route.ts +66 -0
  25. package/src/app/api/gateways/[id]/health/route.ts +28 -0
  26. package/src/app/api/gateways/[id]/route.ts +79 -0
  27. package/src/app/api/gateways/route.ts +57 -0
  28. package/src/app/api/openclaw/gateway/route.ts +10 -7
  29. package/src/app/api/openclaw/skills/route.ts +1 -1
  30. package/src/app/api/providers/[id]/discover-models/route.ts +27 -0
  31. package/src/app/api/schedules/[id]/route.ts +38 -9
  32. package/src/app/api/schedules/route.ts +51 -28
  33. package/src/app/api/settings/route.ts +6 -10
  34. package/src/app/api/setup/doctor/route.ts +6 -4
  35. package/src/app/api/tasks/[id]/route.ts +2 -1
  36. package/src/app/api/tasks/bulk/route.ts +2 -2
  37. package/src/app/page.tsx +126 -15
  38. package/src/cli/binary.test.js +142 -0
  39. package/src/cli/index.js +34 -11
  40. package/src/cli/index.test.js +195 -0
  41. package/src/cli/index.ts +20 -4
  42. package/src/cli/server-cmd.test.js +59 -0
  43. package/src/cli/spec.js +20 -2
  44. package/src/components/agents/agent-sheet.tsx +249 -7
  45. package/src/components/agents/inspector-panel.tsx +3 -2
  46. package/src/components/agents/sandbox-env-panel.tsx +4 -1
  47. package/src/components/auth/setup-wizard.tsx +970 -275
  48. package/src/components/chat/chat-area.tsx +41 -14
  49. package/src/components/chat/chat-card.tsx +2 -1
  50. package/src/components/chat/chat-header.tsx +8 -13
  51. package/src/components/chat/chat-list.tsx +58 -20
  52. package/src/components/chat/message-list.tsx +142 -18
  53. package/src/components/chatrooms/chatroom-input.tsx +96 -33
  54. package/src/components/chatrooms/chatroom-list.tsx +141 -72
  55. package/src/components/chatrooms/chatroom-message.tsx +7 -6
  56. package/src/components/chatrooms/chatroom-sheet.tsx +13 -1
  57. package/src/components/chatrooms/chatroom-tool-request-banner.tsx +5 -2
  58. package/src/components/chatrooms/chatroom-view.tsx +157 -86
  59. package/src/components/chatrooms/reaction-picker.tsx +38 -33
  60. package/src/components/gateways/gateway-sheet.tsx +567 -0
  61. package/src/components/input/chat-input.tsx +135 -86
  62. package/src/components/layout/app-layout.tsx +2 -0
  63. package/src/components/memory/memory-browser.tsx +71 -6
  64. package/src/components/memory/memory-card.tsx +18 -0
  65. package/src/components/memory/memory-detail.tsx +58 -31
  66. package/src/components/memory/memory-sheet.tsx +32 -4
  67. package/src/components/projects/project-detail.tsx +7 -2
  68. package/src/components/providers/provider-list.tsx +158 -2
  69. package/src/components/providers/provider-sheet.tsx +81 -70
  70. package/src/components/shared/bottom-sheet.tsx +31 -15
  71. package/src/components/shared/confirm-dialog.tsx +45 -30
  72. package/src/components/shared/model-combobox.tsx +90 -8
  73. package/src/components/shared/settings/section-heartbeat.tsx +11 -6
  74. package/src/components/shared/settings/section-orchestrator.tsx +3 -0
  75. package/src/components/shared/settings/settings-page.tsx +5 -3
  76. package/src/components/tasks/approvals-panel.tsx +7 -1
  77. package/src/components/ui/dialog.tsx +2 -2
  78. package/src/components/wallets/wallet-approval-dialog.tsx +59 -54
  79. package/src/lib/heartbeat-defaults.ts +48 -0
  80. package/src/lib/memory-presentation.ts +59 -0
  81. package/src/lib/provider-model-discovery-client.ts +29 -0
  82. package/src/lib/providers/index.ts +12 -5
  83. package/src/lib/runtime-loop.ts +105 -3
  84. package/src/lib/safe-storage.ts +6 -1
  85. package/src/lib/server/agent-runtime-config.test.ts +141 -0
  86. package/src/lib/server/agent-runtime-config.ts +277 -0
  87. package/src/lib/server/agent-thread-session.test.ts +85 -0
  88. package/src/lib/server/agent-thread-session.ts +123 -0
  89. package/src/lib/server/approvals-auto-approve.test.ts +59 -0
  90. package/src/lib/server/build-llm.test.ts +13 -5
  91. package/src/lib/server/chat-execution-tool-events.test.ts +87 -2
  92. package/src/lib/server/chat-execution.ts +159 -71
  93. package/src/lib/server/chatroom-helpers.test.ts +7 -0
  94. package/src/lib/server/chatroom-helpers.ts +99 -6
  95. package/src/lib/server/chatroom-session-persistence.test.ts +87 -0
  96. package/src/lib/server/connectors/manager.ts +89 -61
  97. package/src/lib/server/connectors/slack.ts +1 -1
  98. package/src/lib/server/daemon-state.ts +3 -2
  99. package/src/lib/server/data-dir.test.ts +56 -0
  100. package/src/lib/server/data-dir.ts +15 -9
  101. package/src/lib/server/eval/agent-regression.test.ts +47 -0
  102. package/src/lib/server/eval/agent-regression.ts +1742 -0
  103. package/src/lib/server/eval/runner.ts +11 -1
  104. package/src/lib/server/eval/store.ts +2 -1
  105. package/src/lib/server/heartbeat-service.ts +23 -8
  106. package/src/lib/server/heartbeat-wake.ts +6 -2
  107. package/src/lib/server/main-agent-loop.ts +13 -6
  108. package/src/lib/server/openclaw-exec-config.ts +4 -2
  109. package/src/lib/server/openclaw-gateway.ts +123 -36
  110. package/src/lib/server/orchestrator-lg.ts +1 -2
  111. package/src/lib/server/orchestrator.ts +3 -2
  112. package/src/lib/server/plugins.test.ts +9 -1
  113. package/src/lib/server/plugins.ts +12 -2
  114. package/src/lib/server/provider-model-discovery.ts +481 -0
  115. package/src/lib/server/queue.ts +1 -1
  116. package/src/lib/server/runtime-settings.test.ts +119 -0
  117. package/src/lib/server/runtime-settings.ts +12 -92
  118. package/src/lib/server/schedule-normalization.ts +187 -0
  119. package/src/lib/server/session-tools/autonomy-tools.test.ts +23 -0
  120. package/src/lib/server/session-tools/crud.ts +27 -3
  121. package/src/lib/server/session-tools/discovery-approvals.test.ts +170 -0
  122. package/src/lib/server/session-tools/discovery.ts +18 -8
  123. package/src/lib/server/session-tools/file-normalize.test.ts +5 -0
  124. package/src/lib/server/session-tools/file.ts +8 -2
  125. package/src/lib/server/session-tools/http.ts +9 -3
  126. package/src/lib/server/session-tools/index.ts +31 -1
  127. package/src/lib/server/session-tools/manage-schedules.test.ts +137 -0
  128. package/src/lib/server/session-tools/monitor.ts +14 -7
  129. package/src/lib/server/session-tools/openclaw-nodes.test.ts +111 -0
  130. package/src/lib/server/session-tools/openclaw-nodes.ts +86 -20
  131. package/src/lib/server/session-tools/platform.ts +1 -1
  132. package/src/lib/server/session-tools/plugin-creator.ts +9 -2
  133. package/src/lib/server/session-tools/sandbox.ts +51 -92
  134. package/src/lib/server/session-tools/session-info.ts +22 -1
  135. package/src/lib/server/session-tools/session-tools-wiring.test.ts +23 -0
  136. package/src/lib/server/session-tools/shell.ts +2 -2
  137. package/src/lib/server/session-tools/subagent.ts +3 -1
  138. package/src/lib/server/session-tools/web.ts +73 -30
  139. package/src/lib/server/storage.ts +29 -3
  140. package/src/lib/server/stream-agent-chat.test.ts +61 -0
  141. package/src/lib/server/stream-agent-chat.ts +139 -4
  142. package/src/lib/server/structured-extract.ts +1 -1
  143. package/src/lib/server/task-mention.ts +0 -1
  144. package/src/lib/server/tool-aliases.ts +37 -6
  145. package/src/lib/server/tool-capability-policy.ts +1 -1
  146. package/src/lib/setup-defaults.ts +352 -11
  147. package/src/lib/tool-definitions.ts +3 -4
  148. package/src/lib/validation/schemas.ts +55 -1
  149. package/src/stores/use-app-store.ts +43 -1
  150. package/src/stores/use-chatroom-store.ts +153 -26
  151. package/src/types/index.ts +189 -6
  152. package/src/app/api/chats/[id]/main-loop/route.ts +0 -13
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { useEffect, useRef, useCallback, useState, useMemo } from 'react'
4
4
  import { useChatroomStore } from '@/stores/use-chatroom-store'
5
+ import type { StreamingAgent } from '@/stores/use-chatroom-store'
5
6
  import { useAppStore } from '@/stores/use-app-store'
6
7
  import { useWs } from '@/hooks/use-ws'
7
8
  import { ChatroomMessageBubble } from './chatroom-message'
@@ -10,6 +11,7 @@ import { ChatroomTypingBar } from './chatroom-typing-bar'
10
11
  import { AgentAvatar } from '@/components/agents/agent-avatar'
11
12
  import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip'
12
13
  import { HeartbeatMoment, ActivityMoment, isNotableTool } from '@/components/chat/activity-moment'
14
+ import { BottomSheet } from '@/components/shared/bottom-sheet'
13
15
  import type { Chatroom, ChatroomMessage, ChatroomMember, Agent } from '@/types'
14
16
 
15
17
  function navigateToAgent(agentId: string) {
@@ -50,16 +52,21 @@ function useAgentHeartbeat(agentId: string, onPulse: (id: string) => void) {
50
52
  useWs(topic, () => onPulseRef.current(agentId))
51
53
  }
52
54
 
53
- function AgentHeartbeatListeners({ agentIds, onPulse }: { agentIds: string[]; onPulse: (id: string) => void }) {
54
- useAgentHeartbeat(agentIds[0] || '', onPulse)
55
- useAgentHeartbeat(agentIds[1] || '', onPulse)
56
- useAgentHeartbeat(agentIds[2] || '', onPulse)
57
- useAgentHeartbeat(agentIds[3] || '', onPulse)
58
- useAgentHeartbeat(agentIds[4] || '', onPulse)
59
- useAgentHeartbeat(agentIds[5] || '', onPulse)
55
+ function AgentHeartbeatListener({ agentId, onPulse }: { agentId: string; onPulse: (id: string) => void }) {
56
+ useAgentHeartbeat(agentId, onPulse)
60
57
  return null
61
58
  }
62
59
 
60
+ function AgentHeartbeatListeners({ agentIds, onPulse }: { agentIds: string[]; onPulse: (id: string) => void }) {
61
+ return (
62
+ <>
63
+ {agentIds.map((agentId) => (
64
+ <AgentHeartbeatListener key={agentId} agentId={agentId} onPulse={onPulse} />
65
+ ))}
66
+ </>
67
+ )
68
+ }
69
+
63
70
  const GROUP_THRESHOLD_MS = 2 * 60 * 1000
64
71
 
65
72
  function dayLabel(ts: number): string {
@@ -76,7 +83,6 @@ function dayLabel(ts: number): string {
76
83
  export function ChatroomView() {
77
84
  const currentChatroomId = useChatroomStore((s) => s.currentChatroomId)
78
85
  const chatrooms = useChatroomStore((s) => s.chatrooms)
79
- const streaming = useChatroomStore((s) => s.streaming)
80
86
  const streamingAgents = useChatroomStore((s) => s.streamingAgents)
81
87
  const sendMessage = useChatroomStore((s) => s.sendMessage)
82
88
  const toggleReaction = useChatroomStore((s) => s.toggleReaction)
@@ -96,6 +102,7 @@ export function ChatroomView() {
96
102
  const [pinsExpanded, setPinsExpanded] = useState(false)
97
103
  const [isNearBottom, setIsNearBottom] = useState(true)
98
104
  const [agentMoments, setAgentMoments] = useState<Record<string, MomentType>>({})
105
+ const [detailsOpen, setDetailsOpen] = useState(false)
99
106
 
100
107
  const handleHeartbeatPulse = useCallback((agentId: string) => {
101
108
  setAgentMoments((prev) => ({ ...prev, [agentId]: { kind: 'heartbeat' } }))
@@ -150,13 +157,14 @@ export function ChatroomView() {
150
157
  ), [agents, chatroom])
151
158
 
152
159
  const streamingAgentIds = useMemo(() => new Set(streamingAgents.keys()), [streamingAgents])
153
- const pinnedIds = chatroom?.pinnedMessageIds || []
160
+ const chatroomId = chatroom?.id || null
161
+ const pinnedIds = useMemo(() => chatroom?.pinnedMessageIds ?? [], [chatroom?.pinnedMessageIds])
154
162
  const pinnedMessages = useMemo(() => (
155
163
  chatroom
156
164
  ? (pinnedIds.map((pid) => chatroom.messages.find((m) => m.id === pid)).filter(Boolean) as ChatroomMessage[])
157
165
  : []
158
166
  ), [chatroom, pinnedIds])
159
- const memberAgentIds = chatroom?.agentIds.slice(0, 6) || []
167
+ const memberAgentIds = chatroom?.agentIds || []
160
168
  const mutedCount = chatroom ? chatroom.agentIds.filter((agentId) => isAgentMuted(chatroom, agentId)).length : 0
161
169
  const adminCount = chatroom ? chatroom.agentIds.filter((agentId) => getMemberRole(chatroom, agentId) === 'admin').length : 0
162
170
  const lastReadAt = chatroom ? (lastReadTimestamps[chatroom.id] || 0) : 0
@@ -183,22 +191,26 @@ export function ChatroomView() {
183
191
  }, [chatroom, markChatRead])
184
192
 
185
193
  useEffect(() => {
186
- if (!chatroom) return
187
- markChatRead(chatroom.id)
188
- }, [chatroom?.id, markChatRead])
194
+ if (!chatroomId) return
195
+ markChatRead(chatroomId)
196
+ }, [chatroomId, markChatRead])
197
+
198
+ useEffect(() => {
199
+ setDetailsOpen(false)
200
+ }, [chatroomId])
189
201
 
190
202
  useEffect(() => {
191
203
  const node = scrollRef.current
192
- if (!node || !chatroom) return
204
+ if (!node || !chatroomId) return
193
205
  const handleScroll = () => {
194
206
  const nearBottom = node.scrollHeight - node.scrollTop - node.clientHeight < 120
195
207
  setIsNearBottom(nearBottom)
196
- if (nearBottom) markChatRead(chatroom.id)
208
+ if (nearBottom) markChatRead(chatroomId)
197
209
  }
198
210
  handleScroll()
199
211
  node.addEventListener('scroll', handleScroll)
200
212
  return () => node.removeEventListener('scroll', handleScroll)
201
- }, [chatroom?.id, markChatRead])
213
+ }, [chatroomId, markChatRead])
202
214
 
203
215
  useEffect(() => {
204
216
  if (chatroom && isNearBottom) {
@@ -298,6 +310,14 @@ export function ChatroomView() {
298
310
  )}
299
311
  </div>
300
312
 
313
+ <button
314
+ type="button"
315
+ onClick={() => setDetailsOpen(true)}
316
+ className="xl:hidden shrink-0 rounded-[9px] border border-white/[0.08] bg-white/[0.03] px-2.5 py-1.5 text-[11px] font-600 text-text-2 hover:bg-white/[0.06] cursor-pointer transition-colors"
317
+ >
318
+ Details
319
+ </button>
320
+
301
321
  <button
302
322
  onClick={() => {
303
323
  setEditingChatroomId(chatroom.id)
@@ -434,98 +454,149 @@ export function ChatroomView() {
434
454
  <ChatroomInput
435
455
  agents={memberAgents}
436
456
  onSend={sendMessage}
437
- disabled={streaming}
438
457
  />
439
458
  </div>
440
459
 
441
460
  <aside className="hidden xl:flex xl:w-[300px] xl:flex-col xl:border-l xl:border-white/[0.06] bg-surface/30">
442
- <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>
462
- </div>
461
+ <RoomDetailsPanel
462
+ chatroom={chatroom}
463
+ memberAgents={memberAgents}
464
+ streamingAgents={streamingAgents}
465
+ pinnedMessages={pinnedMessages}
466
+ mutedCount={mutedCount}
467
+ adminCount={adminCount}
468
+ onFocusMessage={focusMessage}
469
+ />
470
+ </aside>
463
471
 
464
- <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>
472
+ <BottomSheet open={detailsOpen} onClose={() => setDetailsOpen(false)}>
473
+ <RoomDetailsPanel
474
+ chatroom={chatroom}
475
+ memberAgents={memberAgents}
476
+ streamingAgents={streamingAgents}
477
+ pinnedMessages={pinnedMessages}
478
+ mutedCount={mutedCount}
479
+ adminCount={adminCount}
480
+ onFocusMessage={(messageId) => {
481
+ setDetailsOpen(false)
482
+ setTimeout(() => focusMessage(messageId), 50)
483
+ }}
484
+ compact
485
+ />
486
+ </BottomSheet>
487
+ </div>
488
+ )
489
+ }
490
+
491
+ function RoomDetailsPanel({
492
+ chatroom,
493
+ memberAgents,
494
+ streamingAgents,
495
+ pinnedMessages,
496
+ mutedCount,
497
+ adminCount,
498
+ onFocusMessage,
499
+ compact = false,
500
+ }: {
501
+ chatroom: Chatroom
502
+ memberAgents: Agent[]
503
+ streamingAgents: Map<string, StreamingAgent>
504
+ pinnedMessages: ChatroomMessage[]
505
+ mutedCount: number
506
+ adminCount: number
507
+ onFocusMessage: (messageId: string) => void
508
+ compact?: boolean
509
+ }) {
510
+ return (
511
+ <div className={`flex flex-col ${compact ? 'gap-5' : 'h-full'}`}>
512
+ <div className={compact ? '' : 'border-b border-white/[0.06] px-4 py-4'}>
513
+ <h3 className="text-[12px] font-700 uppercase tracking-[0.08em] text-text-3/60">Room Status</h3>
514
+ <div className="mt-3 grid grid-cols-2 gap-2">
515
+ {[
516
+ { label: 'Members', value: String(memberAgents.length), tone: 'text-text' },
517
+ { label: 'Active', value: String(streamingAgents.size), tone: 'text-sky-400' },
518
+ { label: 'Pinned', value: String(pinnedMessages.length), tone: 'text-amber-400' },
519
+ { label: 'Muted', value: String(mutedCount), tone: 'text-rose-400' },
520
+ ].map((item) => (
521
+ <div key={item.label} className="rounded-[10px] border border-white/[0.06] bg-white/[0.02] px-3 py-2.5">
522
+ <div className={`text-[18px] font-display font-700 tracking-[-0.02em] ${item.tone}`}>{item.value}</div>
523
+ <div className="mt-0.5 text-[10px] uppercase tracking-[0.08em] text-text-3/50">{item.label}</div>
469
524
  </div>
470
- <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
- >
525
+ ))}
526
+ </div>
527
+ <div className="mt-3 space-y-1 text-[11px] text-text-3/65">
528
+ <div>Mode: {chatroom.chatMode === 'parallel' ? 'Parallel replies' : 'Sequential replies'}</div>
529
+ <div>Auto-address: {chatroom.autoAddress ? 'Enabled' : 'Off'}</div>
530
+ <div>Admins: {adminCount}</div>
531
+ </div>
532
+ </div>
533
+
534
+ <div className={compact ? 'space-y-4' : 'flex-1 overflow-y-auto px-4 py-4 space-y-4'}>
535
+ <section>
536
+ <div className="mb-2 flex items-center justify-between">
537
+ <h4 className="text-[12px] font-700 uppercase tracking-[0.08em] text-text-3/60">Members</h4>
538
+ <span className="text-[11px] text-text-3/40">{memberAgents.length}</span>
539
+ </div>
540
+ <div className="space-y-2">
541
+ {memberAgents.map((agent) => {
542
+ const role = getMemberRole(chatroom, agent.id)
543
+ const muted = isAgentMuted(chatroom, agent.id)
544
+ return (
545
+ <button
546
+ key={agent.id}
547
+ onClick={() => navigateToAgent(agent.id)}
548
+ className="w-full rounded-[12px] border border-white/[0.06] bg-white/[0.02] px-3 py-2.5 text-left hover:bg-white/[0.05] transition-all cursor-pointer"
549
+ style={{ fontFamily: 'inherit' }}
550
+ >
551
+ <div className="flex items-center gap-3">
481
552
  <AgentAvatar seed={agent.avatarSeed} avatarUrl={agent.avatarUrl} name={agent.name} size={26} status={streamingAgents.has(agent.id) ? 'busy' : 'online'} />
482
553
  <div className="min-w-0 flex-1">
483
- <div className="text-[12px] font-600 text-text 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">
554
+ <div className="truncate text-[12px] font-600 text-text">{agent.name}</div>
555
+ <div className="mt-1 flex flex-wrap gap-1.5">
556
+ <span className="rounded-[5px] bg-white/[0.04] px-1.5 py-0.5 text-[10px] font-700 uppercase tracking-[0.08em] text-text-3/70">
486
557
  {role}
487
558
  </span>
488
559
  {muted && (
489
- <span className="px-1.5 py-0.5 rounded-[5px] bg-rose-500/10 text-[10px] font-700 uppercase tracking-[0.08em] text-rose-400">
560
+ <span className="rounded-[5px] bg-rose-500/10 px-1.5 py-0.5 text-[10px] font-700 uppercase tracking-[0.08em] text-rose-400">
490
561
  Muted
491
562
  </span>
492
563
  )}
493
564
  {streamingAgents.has(agent.id) && (
494
- <span className="px-1.5 py-0.5 rounded-[5px] bg-sky-500/10 text-[10px] font-700 uppercase tracking-[0.08em] text-sky-400">
565
+ <span className="rounded-[5px] bg-sky-500/10 px-1.5 py-0.5 text-[10px] font-700 uppercase tracking-[0.08em] text-sky-400">
495
566
  Active
496
567
  </span>
497
568
  )}
498
569
  </div>
499
570
  </div>
500
- </button>
501
- )
502
- })}
571
+ </div>
572
+ </button>
573
+ )
574
+ })}
575
+ </div>
576
+ </section>
577
+
578
+ {pinnedMessages.length > 0 && (
579
+ <section>
580
+ <div className="mb-2 flex items-center justify-between">
581
+ <h4 className="text-[12px] font-700 uppercase tracking-[0.08em] text-text-3/60">Pinned</h4>
582
+ <span className="text-[11px] text-text-3/40">{pinnedMessages.length}</span>
583
+ </div>
584
+ <div className="space-y-2">
585
+ {pinnedMessages.slice(0, compact ? pinnedMessages.length : 4).map((message) => (
586
+ <button
587
+ key={message.id}
588
+ onClick={() => onFocusMessage(message.id)}
589
+ className="w-full rounded-[12px] border border-white/[0.06] bg-white/[0.02] px-3 py-2.5 text-left hover:bg-white/[0.05] transition-all cursor-pointer"
590
+ style={{ fontFamily: 'inherit' }}
591
+ >
592
+ <div className="text-[11px] font-700 text-accent-bright">{message.senderName}</div>
593
+ <div className="mt-1 line-clamp-2 text-[12px] text-text-3">{message.text}</div>
594
+ </button>
595
+ ))}
503
596
  </div>
504
597
  </section>
505
-
506
- {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>
598
+ )}
599
+ </div>
529
600
  </div>
530
601
  )
531
602
  }
@@ -164,39 +164,33 @@ export function ReactionPicker({ onSelect, onClose }: Props) {
164
164
  const filteredEmojis = useMemo(() => {
165
165
  if (!search.trim()) return null
166
166
  const q = search.toLowerCase()
167
- // Simple search: match against category names or just return all emojis that are visible
168
- const results: string[] = []
167
+ const directEmojiMatches: string[] = []
169
168
  const seen = new Set<string>()
170
169
  for (const cat of CATEGORIES) {
171
170
  if (cat.id === 'frequent') continue
172
171
  for (const emoji of cat.emojis) {
173
- if (!seen.has(emoji)) {
174
- seen.add(emoji)
175
- results.push(emoji)
176
- }
172
+ if (seen.has(emoji)) continue
173
+ seen.add(emoji)
174
+ if (emoji.includes(search.trim())) directEmojiMatches.push(emoji)
177
175
  }
178
176
  }
179
- // For basic emoji search, filter by category label matching
180
- // Since emoji don't have text names in this simple implementation,
181
- // we filter categories that match and show all their emojis
177
+ if (directEmojiMatches.length > 0) return directEmojiMatches
178
+
179
+ // This lightweight picker only understands category labels, not emoji names.
182
180
  const matchingCats = CATEGORIES.filter(
183
181
  (c) => c.id !== 'frequent' && c.label.toLowerCase().includes(q)
184
182
  )
185
- if (matchingCats.length > 0) {
186
- const catResults: string[] = []
187
- const catSeen = new Set<string>()
188
- for (const cat of matchingCats) {
189
- for (const emoji of cat.emojis) {
190
- if (!catSeen.has(emoji)) {
191
- catSeen.add(emoji)
192
- catResults.push(emoji)
193
- }
183
+ const catResults: string[] = []
184
+ const catSeen = new Set<string>()
185
+ for (const cat of matchingCats) {
186
+ for (const emoji of cat.emojis) {
187
+ if (!catSeen.has(emoji)) {
188
+ catSeen.add(emoji)
189
+ catResults.push(emoji)
194
190
  }
195
191
  }
196
- return catResults
197
192
  }
198
- // If no category match, just return all emojis (user can visually scan)
199
- return results
193
+ return catResults
200
194
  }, [search])
201
195
 
202
196
  return (
@@ -212,9 +206,14 @@ export function ReactionPicker({ onSelect, onClose }: Props) {
212
206
  type="text"
213
207
  value={search}
214
208
  onChange={(e) => setSearch(e.target.value)}
215
- placeholder="Search emoji..."
209
+ placeholder="Filter by category or paste emoji..."
216
210
  className="w-full px-2.5 py-1.5 rounded-[8px] bg-white/[0.06] border border-white/[0.08] text-[12px] text-text placeholder:text-text-3 focus:outline-none focus:border-accent-bright/40"
217
211
  />
212
+ {search.trim() && (
213
+ <p className="mt-1 px-0.5 text-[10px] text-text-3/55">
214
+ This picker filters category labels rather than emoji names.
215
+ </p>
216
+ )}
218
217
  </div>
219
218
 
220
219
  {/* Category tabs */}
@@ -238,17 +237,23 @@ export function ReactionPicker({ onSelect, onClose }: Props) {
238
237
  {/* Emoji grid */}
239
238
  <div className="px-2 pb-2 max-h-[220px] overflow-y-auto">
240
239
  {search.trim() ? (
241
- <div className="grid grid-cols-8 gap-0.5">
242
- {filteredEmojis?.map((emoji, i) => (
243
- <button
244
- key={`${emoji}-${i}`}
245
- onClick={() => onSelect(emoji)}
246
- className="w-[34px] h-[34px] flex items-center justify-center rounded-[6px] hover:bg-white/[0.08] transition-all cursor-pointer text-[18px]"
247
- >
248
- {emoji}
249
- </button>
250
- ))}
251
- </div>
240
+ filteredEmojis && filteredEmojis.length > 0 ? (
241
+ <div className="grid grid-cols-8 gap-0.5">
242
+ {filteredEmojis.map((emoji, i) => (
243
+ <button
244
+ key={`${emoji}-${i}`}
245
+ onClick={() => onSelect(emoji)}
246
+ className="w-[34px] h-[34px] flex items-center justify-center rounded-[6px] hover:bg-white/[0.08] transition-all cursor-pointer text-[18px]"
247
+ >
248
+ {emoji}
249
+ </button>
250
+ ))}
251
+ </div>
252
+ ) : (
253
+ <div className="px-2 py-6 text-center text-[11px] text-text-3/60">
254
+ No category matches. Try terms like <span className="text-text-3">food</span>, <span className="text-text-3">travel</span>, or paste an emoji.
255
+ </div>
256
+ )
252
257
  ) : (
253
258
  CATEGORIES.filter((c) => c.id === activeCategory).map((cat) => (
254
259
  <div key={cat.id}>