@swarmclawai/swarmclaw 0.7.2 → 0.7.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (274) hide show
  1. package/README.md +116 -50
  2. package/bin/package-manager.js +157 -0
  3. package/bin/package-manager.test.js +90 -0
  4. package/bin/server-cmd.js +38 -7
  5. package/bin/swarmclaw.js +54 -4
  6. package/bin/update-cmd.js +48 -10
  7. package/bin/update-cmd.test.js +55 -0
  8. package/package.json +8 -3
  9. package/scripts/postinstall.mjs +26 -0
  10. package/src/app/api/agents/[id]/route.ts +43 -0
  11. package/src/app/api/agents/[id]/thread/route.ts +39 -8
  12. package/src/app/api/agents/route.ts +35 -2
  13. package/src/app/api/auth/route.ts +77 -8
  14. package/src/app/api/chatrooms/[id]/chat/route.ts +22 -6
  15. package/src/app/api/chatrooms/[id]/pins/route.ts +2 -1
  16. package/src/app/api/chatrooms/[id]/reactions/route.ts +2 -1
  17. package/src/app/api/chatrooms/[id]/route.ts +6 -0
  18. package/src/app/api/chats/[id]/browser/route.ts +5 -1
  19. package/src/app/api/chats/[id]/chat/route.ts +7 -3
  20. package/src/app/api/chats/[id]/messages/route.ts +19 -13
  21. package/src/app/api/chats/[id]/route.ts +30 -0
  22. package/src/app/api/chats/[id]/stop/route.ts +6 -1
  23. package/src/app/api/chats/heartbeat/route.ts +2 -1
  24. package/src/app/api/chats/route.ts +23 -1
  25. package/src/app/api/connectors/[id]/doctor/route.ts +26 -0
  26. package/src/app/api/connectors/doctor/route.ts +13 -0
  27. package/src/app/api/external-agents/[id]/heartbeat/route.ts +33 -0
  28. package/src/app/api/external-agents/[id]/route.ts +31 -0
  29. package/src/app/api/external-agents/register/route.ts +3 -0
  30. package/src/app/api/external-agents/route.ts +66 -0
  31. package/src/app/api/files/open/route.ts +16 -14
  32. package/src/app/api/gateways/[id]/health/route.ts +28 -0
  33. package/src/app/api/gateways/[id]/route.ts +79 -0
  34. package/src/app/api/gateways/route.ts +57 -0
  35. package/src/app/api/memory/maintenance/route.ts +11 -1
  36. package/src/app/api/openclaw/agent-files/route.ts +27 -4
  37. package/src/app/api/openclaw/gateway/route.ts +10 -7
  38. package/src/app/api/openclaw/skills/route.ts +12 -4
  39. package/src/app/api/plugins/dependencies/route.ts +24 -0
  40. package/src/app/api/plugins/install/route.ts +15 -92
  41. package/src/app/api/plugins/route.ts +3 -26
  42. package/src/app/api/plugins/settings/route.ts +17 -12
  43. package/src/app/api/plugins/ui/route.ts +1 -0
  44. package/src/app/api/providers/[id]/discover-models/route.ts +27 -0
  45. package/src/app/api/schedules/[id]/route.ts +38 -9
  46. package/src/app/api/schedules/route.ts +51 -28
  47. package/src/app/api/settings/route.ts +55 -17
  48. package/src/app/api/setup/doctor/route.ts +6 -4
  49. package/src/app/api/tasks/[id]/route.ts +16 -6
  50. package/src/app/api/tasks/bulk/route.ts +3 -3
  51. package/src/app/api/tasks/route.ts +9 -4
  52. package/src/app/api/webhooks/[id]/route.ts +8 -1
  53. package/src/app/page.tsx +135 -17
  54. package/src/cli/binary.test.js +142 -0
  55. package/src/cli/index.js +38 -11
  56. package/src/cli/index.test.js +195 -0
  57. package/src/cli/index.ts +21 -12
  58. package/src/cli/server-cmd.test.js +59 -0
  59. package/src/cli/spec.js +20 -2
  60. package/src/components/agents/agent-card.tsx +15 -12
  61. package/src/components/agents/agent-chat-list.tsx +101 -1
  62. package/src/components/agents/agent-list.tsx +46 -9
  63. package/src/components/agents/agent-sheet.tsx +456 -23
  64. package/src/components/agents/inspector-panel.tsx +110 -49
  65. package/src/components/agents/sandbox-env-panel.tsx +4 -1
  66. package/src/components/auth/access-key-gate.tsx +36 -97
  67. package/src/components/auth/setup-wizard.tsx +970 -275
  68. package/src/components/chat/chat-area.tsx +70 -27
  69. package/src/components/chat/chat-card.tsx +6 -21
  70. package/src/components/chat/chat-header.tsx +263 -366
  71. package/src/components/chat/chat-list.tsx +62 -26
  72. package/src/components/chat/checkpoint-timeline.tsx +1 -1
  73. package/src/components/chat/message-list.tsx +145 -19
  74. package/src/components/chatrooms/chatroom-input.tsx +96 -33
  75. package/src/components/chatrooms/chatroom-list.tsx +141 -72
  76. package/src/components/chatrooms/chatroom-message.tsx +7 -6
  77. package/src/components/chatrooms/chatroom-sheet.tsx +13 -1
  78. package/src/components/chatrooms/chatroom-tool-request-banner.tsx +5 -2
  79. package/src/components/chatrooms/chatroom-view.tsx +422 -209
  80. package/src/components/chatrooms/reaction-picker.tsx +38 -33
  81. package/src/components/connectors/connector-list.tsx +265 -127
  82. package/src/components/connectors/connector-sheet.tsx +217 -0
  83. package/src/components/gateways/gateway-sheet.tsx +567 -0
  84. package/src/components/home/home-view.tsx +128 -4
  85. package/src/components/input/chat-input.tsx +135 -86
  86. package/src/components/layout/app-layout.tsx +385 -194
  87. package/src/components/layout/mobile-header.tsx +26 -8
  88. package/src/components/memory/memory-browser.tsx +71 -6
  89. package/src/components/memory/memory-card.tsx +18 -0
  90. package/src/components/memory/memory-detail.tsx +58 -31
  91. package/src/components/memory/memory-sheet.tsx +32 -4
  92. package/src/components/plugins/plugin-list.tsx +15 -3
  93. package/src/components/plugins/plugin-sheet.tsx +118 -9
  94. package/src/components/projects/project-detail.tsx +189 -1
  95. package/src/components/providers/provider-list.tsx +158 -2
  96. package/src/components/providers/provider-sheet.tsx +81 -70
  97. package/src/components/shared/agent-picker-list.tsx +2 -2
  98. package/src/components/shared/bottom-sheet.tsx +31 -15
  99. package/src/components/shared/command-palette.tsx +111 -24
  100. package/src/components/shared/confirm-dialog.tsx +45 -30
  101. package/src/components/shared/model-combobox.tsx +90 -8
  102. package/src/components/shared/settings/plugin-manager.tsx +20 -4
  103. package/src/components/shared/settings/section-capability-policy.tsx +105 -0
  104. package/src/components/shared/settings/section-heartbeat.tsx +88 -6
  105. package/src/components/shared/settings/section-orchestrator.tsx +6 -3
  106. package/src/components/shared/settings/section-runtime-loop.tsx +5 -5
  107. package/src/components/shared/settings/section-secrets.tsx +6 -6
  108. package/src/components/shared/settings/section-user-preferences.tsx +1 -1
  109. package/src/components/shared/settings/section-voice.tsx +5 -1
  110. package/src/components/shared/settings/section-web-search.tsx +10 -2
  111. package/src/components/shared/settings/settings-page.tsx +248 -47
  112. package/src/components/tasks/approvals-panel.tsx +211 -18
  113. package/src/components/tasks/task-board.tsx +242 -46
  114. package/src/components/ui/dialog.tsx +2 -2
  115. package/src/components/usage/metrics-dashboard.tsx +74 -1
  116. package/src/components/wallets/wallet-approval-dialog.tsx +59 -54
  117. package/src/components/wallets/wallet-panel.tsx +17 -5
  118. package/src/components/webhooks/webhook-sheet.tsx +7 -7
  119. package/src/lib/auth.ts +17 -0
  120. package/src/lib/chat-streaming-state.test.ts +108 -0
  121. package/src/lib/chat-streaming-state.ts +108 -0
  122. package/src/lib/heartbeat-defaults.ts +48 -0
  123. package/src/lib/memory-presentation.ts +59 -0
  124. package/src/lib/openclaw-agent-id.test.ts +14 -0
  125. package/src/lib/openclaw-agent-id.ts +31 -0
  126. package/src/lib/provider-model-discovery-client.ts +29 -0
  127. package/src/lib/providers/index.ts +12 -5
  128. package/src/lib/runtime-loop.ts +105 -3
  129. package/src/lib/safe-storage.ts +6 -1
  130. package/src/lib/server/agent-assignment.test.ts +112 -0
  131. package/src/lib/server/agent-assignment.ts +169 -0
  132. package/src/lib/server/agent-runtime-config.test.ts +141 -0
  133. package/src/lib/server/agent-runtime-config.ts +277 -0
  134. package/src/lib/server/approval-connector-notify.test.ts +253 -0
  135. package/src/lib/server/approvals-auto-approve.test.ts +264 -0
  136. package/src/lib/server/approvals.ts +483 -75
  137. package/src/lib/server/autonomy-runtime.test.ts +341 -0
  138. package/src/lib/server/browser-state.test.ts +118 -0
  139. package/src/lib/server/browser-state.ts +123 -0
  140. package/src/lib/server/build-llm.test.ts +44 -0
  141. package/src/lib/server/build-llm.ts +11 -4
  142. package/src/lib/server/builtin-plugins.ts +34 -0
  143. package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
  144. package/src/lib/server/chat-execution-tool-events.test.ts +219 -0
  145. package/src/lib/server/chat-execution.ts +402 -125
  146. package/src/lib/server/chatroom-health.test.ts +26 -0
  147. package/src/lib/server/chatroom-health.ts +2 -3
  148. package/src/lib/server/chatroom-helpers.test.ts +74 -2
  149. package/src/lib/server/chatroom-helpers.ts +144 -11
  150. package/src/lib/server/chatroom-session-persistence.test.ts +87 -0
  151. package/src/lib/server/connectors/discord.ts +175 -11
  152. package/src/lib/server/connectors/doctor.test.ts +80 -0
  153. package/src/lib/server/connectors/doctor.ts +116 -0
  154. package/src/lib/server/connectors/manager.ts +994 -130
  155. package/src/lib/server/connectors/policy.test.ts +222 -0
  156. package/src/lib/server/connectors/policy.ts +452 -0
  157. package/src/lib/server/connectors/slack.ts +189 -10
  158. package/src/lib/server/connectors/telegram.ts +65 -15
  159. package/src/lib/server/connectors/thread-context.test.ts +44 -0
  160. package/src/lib/server/connectors/thread-context.ts +72 -0
  161. package/src/lib/server/connectors/types.ts +41 -11
  162. package/src/lib/server/daemon-state.ts +62 -3
  163. package/src/lib/server/data-dir.ts +13 -0
  164. package/src/lib/server/delegation-jobs.test.ts +140 -0
  165. package/src/lib/server/delegation-jobs.ts +248 -0
  166. package/src/lib/server/document-utils.test.ts +47 -0
  167. package/src/lib/server/document-utils.ts +397 -0
  168. package/src/lib/server/eval/agent-regression.test.ts +47 -0
  169. package/src/lib/server/eval/agent-regression.ts +1742 -0
  170. package/src/lib/server/eval/runner.ts +11 -1
  171. package/src/lib/server/eval/store.ts +2 -1
  172. package/src/lib/server/heartbeat-service.ts +23 -43
  173. package/src/lib/server/heartbeat-source.test.ts +22 -0
  174. package/src/lib/server/heartbeat-source.ts +7 -0
  175. package/src/lib/server/identity-continuity.test.ts +77 -0
  176. package/src/lib/server/identity-continuity.ts +127 -0
  177. package/src/lib/server/mailbox-utils.ts +347 -0
  178. package/src/lib/server/main-agent-loop.ts +31 -964
  179. package/src/lib/server/memory-db.ts +4 -6
  180. package/src/lib/server/memory-tiers.ts +40 -0
  181. package/src/lib/server/openclaw-agent-resolver.test.ts +70 -0
  182. package/src/lib/server/openclaw-agent-resolver.ts +128 -0
  183. package/src/lib/server/openclaw-exec-config.ts +6 -5
  184. package/src/lib/server/openclaw-gateway.ts +123 -36
  185. package/src/lib/server/openclaw-skills-normalize.test.ts +56 -0
  186. package/src/lib/server/openclaw-skills-normalize.ts +136 -0
  187. package/src/lib/server/openclaw-sync.ts +3 -2
  188. package/src/lib/server/orchestrator-lg.ts +18 -8
  189. package/src/lib/server/orchestrator.ts +5 -4
  190. package/src/lib/server/playwright-proxy.mjs +27 -3
  191. package/src/lib/server/plugins.test.ts +215 -0
  192. package/src/lib/server/plugins.ts +832 -69
  193. package/src/lib/server/provider-health.ts +33 -3
  194. package/src/lib/server/provider-model-discovery.ts +481 -0
  195. package/src/lib/server/queue.ts +4 -21
  196. package/src/lib/server/runtime-settings.test.ts +119 -0
  197. package/src/lib/server/runtime-settings.ts +12 -92
  198. package/src/lib/server/schedule-normalization.ts +187 -0
  199. package/src/lib/server/scheduler.ts +2 -0
  200. package/src/lib/server/session-archive-memory.test.ts +85 -0
  201. package/src/lib/server/session-archive-memory.ts +230 -0
  202. package/src/lib/server/session-mailbox.ts +8 -18
  203. package/src/lib/server/session-reset-policy.test.ts +99 -0
  204. package/src/lib/server/session-reset-policy.ts +311 -0
  205. package/src/lib/server/session-run-manager.ts +33 -80
  206. package/src/lib/server/session-tools/autonomy-tools.test.ts +128 -0
  207. package/src/lib/server/session-tools/calendar.ts +2 -12
  208. package/src/lib/server/session-tools/connector.ts +109 -8
  209. package/src/lib/server/session-tools/context.ts +14 -2
  210. package/src/lib/server/session-tools/crawl.ts +447 -0
  211. package/src/lib/server/session-tools/crud.ts +96 -34
  212. package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
  213. package/src/lib/server/session-tools/delegate.ts +406 -20
  214. package/src/lib/server/session-tools/discovery-approvals.test.ts +170 -0
  215. package/src/lib/server/session-tools/discovery.ts +40 -12
  216. package/src/lib/server/session-tools/document.ts +283 -0
  217. package/src/lib/server/session-tools/email.ts +1 -3
  218. package/src/lib/server/session-tools/extract.ts +137 -0
  219. package/src/lib/server/session-tools/file-normalize.test.ts +98 -0
  220. package/src/lib/server/session-tools/file-send.test.ts +84 -1
  221. package/src/lib/server/session-tools/file.ts +243 -24
  222. package/src/lib/server/session-tools/http.ts +9 -3
  223. package/src/lib/server/session-tools/human-loop.ts +227 -0
  224. package/src/lib/server/session-tools/image-gen.ts +1 -3
  225. package/src/lib/server/session-tools/index.ts +87 -2
  226. package/src/lib/server/session-tools/mailbox.ts +276 -0
  227. package/src/lib/server/session-tools/manage-schedules.test.ts +137 -0
  228. package/src/lib/server/session-tools/memory.ts +35 -3
  229. package/src/lib/server/session-tools/monitor.ts +162 -12
  230. package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
  231. package/src/lib/server/session-tools/openclaw-nodes.test.ts +111 -0
  232. package/src/lib/server/session-tools/openclaw-nodes.ts +86 -20
  233. package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
  234. package/src/lib/server/session-tools/platform.ts +142 -4
  235. package/src/lib/server/session-tools/plugin-creator.ts +95 -25
  236. package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
  237. package/src/lib/server/session-tools/replicate.ts +1 -3
  238. package/src/lib/server/session-tools/sandbox.ts +51 -92
  239. package/src/lib/server/session-tools/schedule.ts +20 -10
  240. package/src/lib/server/session-tools/session-info.ts +58 -4
  241. package/src/lib/server/session-tools/session-tools-wiring.test.ts +54 -17
  242. package/src/lib/server/session-tools/shell.ts +2 -2
  243. package/src/lib/server/session-tools/subagent.ts +195 -27
  244. package/src/lib/server/session-tools/table.ts +587 -0
  245. package/src/lib/server/session-tools/wallet.ts +13 -10
  246. package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
  247. package/src/lib/server/session-tools/web.ts +947 -108
  248. package/src/lib/server/storage.ts +255 -10
  249. package/src/lib/server/stream-agent-chat.test.ts +61 -0
  250. package/src/lib/server/stream-agent-chat.ts +185 -25
  251. package/src/lib/server/structured-extract.test.ts +72 -0
  252. package/src/lib/server/structured-extract.ts +373 -0
  253. package/src/lib/server/task-mention.test.ts +16 -2
  254. package/src/lib/server/task-mention.ts +61 -11
  255. package/src/lib/server/tool-aliases.ts +80 -12
  256. package/src/lib/server/tool-capability-policy.ts +7 -1
  257. package/src/lib/server/tool-retry.ts +2 -0
  258. package/src/lib/server/watch-jobs.test.ts +173 -0
  259. package/src/lib/server/watch-jobs.ts +532 -0
  260. package/src/lib/server/ws-hub.ts +5 -3
  261. package/src/lib/setup-defaults.ts +352 -11
  262. package/src/lib/tool-definitions.ts +3 -4
  263. package/src/lib/validation/schemas.test.ts +26 -0
  264. package/src/lib/validation/schemas.ts +62 -1
  265. package/src/lib/ws-client.ts +14 -12
  266. package/src/proxy.ts +5 -5
  267. package/src/stores/use-app-store.ts +43 -7
  268. package/src/stores/use-chat-store.ts +31 -2
  269. package/src/stores/use-chatroom-store.ts +153 -26
  270. package/src/types/index.ts +470 -44
  271. package/src/app/api/chats/[id]/main-loop/route.ts +0 -94
  272. package/src/components/chat/new-chat-sheet.tsx +0 -253
  273. package/src/lib/server/main-session.ts +0 -17
  274. package/src/lib/server/session-run-manager.test.ts +0 -26
@@ -7,6 +7,14 @@ import { useWs } from '@/hooks/use-ws'
7
7
  import type { Chatroom } from '@/types'
8
8
  import { EmptyState } from '@/components/shared/empty-state'
9
9
 
10
+ function formatRoomTime(ts: number): string {
11
+ const diff = Date.now() - ts
12
+ if (diff < 60_000) return 'Now'
13
+ if (diff < 3_600_000) return `${Math.max(1, Math.floor(diff / 60_000))}m`
14
+ if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)}h`
15
+ return new Date(ts).toLocaleDateString(undefined, { month: 'short', day: 'numeric' })
16
+ }
17
+
10
18
  export function ChatroomList() {
11
19
  const chatrooms = useChatroomStore((s) => s.chatrooms)
12
20
  const currentChatroomId = useChatroomStore((s) => s.currentChatroomId)
@@ -15,6 +23,9 @@ export function ChatroomList() {
15
23
  const setChatroomSheetOpen = useChatroomStore((s) => s.setChatroomSheetOpen)
16
24
  const setEditingChatroomId = useChatroomStore((s) => s.setEditingChatroomId)
17
25
  const agents = useAppStore((s) => s.agents)
26
+ const lastReadTimestamps = useAppStore((s) => s.lastReadTimestamps)
27
+ const [filter, setFilter] = useState<'all' | 'active' | 'recent' | 'unread'>('all')
28
+ const [search, setSearch] = useState('')
18
29
 
19
30
  const refresh = useCallback(() => {
20
31
  loadChatrooms()
@@ -24,32 +35,56 @@ export function ChatroomList() {
24
35
  useEffect(() => { refresh() }, [refresh])
25
36
  useWs('chatrooms', refresh, 15_000)
26
37
 
27
- // Auto-select the latest chatroom when none is selected
28
38
  useEffect(() => {
29
39
  if (currentChatroomId) return
30
40
  const latest = Object.values(chatrooms).sort((a, b) => b.updatedAt - a.updatedAt)[0]
31
41
  if (latest) setCurrentChatroom(latest.id)
32
42
  }, [chatrooms, currentChatroomId, setCurrentChatroom])
33
43
 
34
- const [filter, setFilter] = useState<'all' | 'active' | 'recent'>('all')
44
+ const enriched = useMemo(() => (
45
+ Object.values(chatrooms)
46
+ .map((chatroom: Chatroom) => {
47
+ const memberNames = chatroom.agentIds
48
+ .map((id) => agents[id]?.name)
49
+ .filter(Boolean)
50
+ const lastMsg = chatroom.messages[chatroom.messages.length - 1]
51
+ const lastReadAt = lastReadTimestamps[chatroom.id] || 0
52
+ const unreadCount = chatroom.messages.filter(
53
+ (msg) => msg.senderId !== 'user' && msg.senderId !== 'system' && (msg.time || 0) > lastReadAt,
54
+ ).length
35
55
 
36
- const sorted = useMemo(() =>
37
- Object.values(chatrooms).sort(
38
- (a: Chatroom, b: Chatroom) => b.updatedAt - a.updatedAt
39
- ), [chatrooms])
56
+ return {
57
+ chatroom,
58
+ memberNames,
59
+ lastMsg,
60
+ unreadCount,
61
+ searchText: [
62
+ chatroom.name,
63
+ chatroom.description,
64
+ memberNames.join(' '),
65
+ lastMsg?.senderName,
66
+ lastMsg?.text,
67
+ ].filter(Boolean).join(' ').toLowerCase(),
68
+ }
69
+ })
70
+ .sort((a, b) => b.chatroom.updatedAt - a.chatroom.updatedAt)
71
+ ), [agents, chatrooms, lastReadTimestamps])
40
72
 
41
73
  const filtered = useMemo(() => {
42
- if (filter === 'all') return sorted
74
+ const query = search.trim().toLowerCase()
43
75
  const now = Date.now()
44
- return sorted.filter((c) => {
45
- if (filter === 'active') return now - c.updatedAt < 3_600_000 // 1h
46
- return now - c.updatedAt < 86_400_000 // 24h
76
+ return enriched.filter((item) => {
77
+ if (query && !item.searchText.includes(query)) return false
78
+ if (filter === 'active') return now - item.chatroom.updatedAt < 3_600_000
79
+ if (filter === 'recent') return now - item.chatroom.updatedAt < 86_400_000
80
+ if (filter === 'unread') return item.unreadCount > 0
81
+ return true
47
82
  })
48
- }, [sorted, filter])
83
+ }, [enriched, filter, search])
49
84
 
50
85
  return (
51
86
  <div className="flex-1 overflow-y-auto">
52
- {sorted.length === 0 ? (
87
+ {enriched.length === 0 ? (
53
88
  <EmptyState
54
89
  icon={
55
90
  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" className="text-accent-bright">
@@ -61,75 +96,109 @@ export function ChatroomList() {
61
96
  action={{ label: '+ New Chatroom', onClick: () => { setEditingChatroomId(null); setChatroomSheetOpen(true) } }}
62
97
  />
63
98
  ) : (
64
- <div className="p-3 space-y-1">
65
- {sorted.length > 2 && (
66
- <div className="flex items-center gap-1 px-1 pb-2" style={{ animation: 'fade-up 0.4s var(--ease-spring)' }}>
67
- {(['all', 'active', 'recent'] as const).map((f) => (
99
+ <div className="p-3 space-y-3">
100
+ <div className="space-y-2">
101
+ <input
102
+ type="text"
103
+ value={search}
104
+ onChange={(e) => setSearch(e.target.value)}
105
+ placeholder="Search rooms, members, or recent messages..."
106
+ className="w-full rounded-[12px] border border-white/[0.06] bg-surface px-3 py-2.5 text-[13px] text-text placeholder:text-text-3/70 focus:outline-none focus:border-accent-bright/35"
107
+ />
108
+ <div className="flex flex-wrap items-center gap-1">
109
+ {(['all', 'active', 'recent', 'unread'] as const).map((value) => (
68
110
  <button
69
- key={f}
111
+ key={value}
70
112
  type="button"
71
- onClick={() => setFilter(f)}
72
- data-active={filter === f || undefined}
73
- className="px-3 py-1.5 rounded-[8px] text-[11px] font-600 border-none cursor-pointer transition-all focus-visible:ring-1 focus-visible:ring-accent-bright/50
113
+ onClick={() => setFilter(value)}
114
+ data-active={filter === value || undefined}
115
+ className="rounded-[8px] border-none px-3 py-1.5 text-[11px] font-600 capitalize cursor-pointer transition-all focus-visible:ring-1 focus-visible:ring-accent-bright/50
74
116
  data-[active]:bg-accent-soft data-[active]:text-accent-bright
75
117
  bg-transparent text-text-3 hover:text-text-2 hover:bg-white/[0.04]"
76
118
  >
77
- {f}
119
+ {value}
78
120
  </button>
79
121
  ))}
122
+ <span className="ml-auto text-[11px] text-text-3/55">
123
+ {filtered.length} room{filtered.length === 1 ? '' : 's'}
124
+ </span>
80
125
  </div>
81
- )}
82
- {filtered.map((chatroom, idx) => {
83
- const isActive = chatroom.id === currentChatroomId
84
- const memberNames = chatroom.agentIds
85
- .map((id) => agents[id]?.name)
86
- .filter(Boolean)
87
- .slice(0, 3)
88
- const lastMsg = chatroom.messages[chatroom.messages.length - 1]
126
+ </div>
89
127
 
90
- return (
91
- <button
92
- key={chatroom.id}
93
- onClick={() => setCurrentChatroom(chatroom.id)}
94
- className={`w-full text-left py-3.5 px-4 rounded-[14px] transition-all cursor-pointer group border border-transparent relative overflow-hidden ${
95
- isActive
96
- ? 'bg-accent-soft/60'
97
- : 'hover:bg-white/[0.04] hover:scale-[1.01]'
98
- }`}
99
- style={{
100
- animation: 'fade-up 0.4s var(--ease-spring) both',
101
- animationDelay: `${idx * 0.03}s`
102
- }}
103
- >
104
- <div className="flex items-center gap-2 mb-0.5">
105
- <div className="w-7 h-7 rounded-full bg-accent-soft flex items-center justify-center shrink-0">
106
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-accent-bright">
107
- <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
108
- </svg>
109
- </div>
110
- <span className={`text-[13px] font-600 truncate ${isActive ? 'text-accent-bright' : 'text-text'}`}>
111
- {chatroom.name}
112
- </span>
113
- {isActive && (
114
- <div className="absolute left-0 top-3 bottom-3 w-1 rounded-r-full bg-accent-bright" style={{ animation: 'spring-in 0.3s var(--ease-spring)' }} />
115
- )}
116
- <span className="label-mono ml-auto shrink-0">
117
- {chatroom.agentIds.length} agents
118
- </span>
119
- </div>
120
- {memberNames.length > 0 && (
121
- <p className="text-[11px] text-text-3 truncate pl-9">
122
- {memberNames.join(', ')}{chatroom.agentIds.length > 3 ? ` +${chatroom.agentIds.length - 3}` : ''}
123
- </p>
124
- )}
125
- {lastMsg && (
126
- <p className="text-[11px] text-text-3/70 truncate pl-9 mt-0.5">
127
- {lastMsg.senderName}: {lastMsg.text.slice(0, 60)}
128
- </p>
129
- )}
130
- </button>
131
- )
132
- })}
128
+ {filtered.length === 0 ? (
129
+ <div className="rounded-[14px] border border-white/[0.06] bg-white/[0.02] px-4 py-8 text-center">
130
+ <div className="text-[13px] font-600 text-text-2">No rooms match this view</div>
131
+ <div className="mt-1 text-[12px] text-text-3/65">
132
+ Clear the search or switch filters to see more chatrooms.
133
+ </div>
134
+ </div>
135
+ ) : (
136
+ <div className="space-y-1">
137
+ {filtered.map(({ chatroom, memberNames, lastMsg, unreadCount }, idx) => {
138
+ const isActive = chatroom.id === currentChatroomId
139
+ return (
140
+ <button
141
+ key={chatroom.id}
142
+ onClick={() => setCurrentChatroom(chatroom.id)}
143
+ className={`relative w-full overflow-hidden rounded-[14px] border px-4 py-3.5 text-left transition-all cursor-pointer ${
144
+ isActive
145
+ ? 'border-accent-bright/20 bg-accent-soft/55'
146
+ : 'border-transparent hover:bg-white/[0.04] hover:border-white/[0.05]'
147
+ }`}
148
+ style={{
149
+ animation: 'fade-up 0.4s var(--ease-spring) both',
150
+ animationDelay: `${idx * 0.03}s`,
151
+ }}
152
+ >
153
+ {isActive && (
154
+ <div className="absolute inset-y-3 left-0 w-1 rounded-r-full bg-accent-bright" />
155
+ )}
156
+ <div className="flex items-start gap-3">
157
+ <div className="mt-0.5 flex h-8 w-8 items-center justify-center rounded-full bg-accent-soft shrink-0">
158
+ <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-accent-bright">
159
+ <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
160
+ </svg>
161
+ </div>
162
+ <div className="min-w-0 flex-1">
163
+ <div className="flex items-center gap-2">
164
+ <span className={`truncate text-[13px] font-700 ${isActive ? 'text-accent-bright' : 'text-text'}`}>
165
+ {chatroom.name}
166
+ </span>
167
+ {unreadCount > 0 && (
168
+ <span className="inline-flex min-w-[18px] items-center justify-center rounded-full bg-accent-bright px-1.5 py-0.5 text-[10px] font-700 text-white">
169
+ {unreadCount > 99 ? '99+' : unreadCount}
170
+ </span>
171
+ )}
172
+ <span className="ml-auto shrink-0 text-[10px] font-mono text-text-3/55">
173
+ {formatRoomTime(lastMsg?.time || chatroom.updatedAt)}
174
+ </span>
175
+ </div>
176
+ <div className="mt-0.5 flex flex-wrap items-center gap-2 text-[11px] text-text-3">
177
+ <span>{chatroom.agentIds.length} agent{chatroom.agentIds.length === 1 ? '' : 's'}</span>
178
+ {chatroom.chatMode === 'parallel' && (
179
+ <span className="rounded-[6px] bg-sky-500/10 px-1.5 py-0.5 text-sky-300">Parallel</span>
180
+ )}
181
+ {chatroom.autoAddress && (
182
+ <span className="rounded-[6px] bg-emerald-500/10 px-1.5 py-0.5 text-emerald-300">Auto-address</span>
183
+ )}
184
+ </div>
185
+ {memberNames.length > 0 && (
186
+ <p className="mt-1 truncate text-[11px] text-text-3/80">
187
+ {memberNames.slice(0, 3).join(', ')}{memberNames.length > 3 ? ` +${memberNames.length - 3}` : ''}
188
+ </p>
189
+ )}
190
+ {lastMsg && (
191
+ <p className="mt-1 truncate text-[11px] text-text-3/65">
192
+ {lastMsg.senderName}: {lastMsg.text.slice(0, 72)}
193
+ </p>
194
+ )}
195
+ </div>
196
+ </div>
197
+ </button>
198
+ )
199
+ })}
200
+ </div>
201
+ )}
133
202
  </div>
134
203
  )}
135
204
  </div>
@@ -393,12 +393,12 @@ export function ChatroomMessageBubble({ message, agents, onToggleReaction, onRep
393
393
  </div>
394
394
 
395
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 }}>
396
+ <div className="relative shrink-0 mt-0.5 flex items-start gap-1" style={{ zIndex: showPicker || showTransferPicker || showModMenu ? 50 : undefined }}>
397
397
  {/* Reply button */}
398
398
  {onReply && (
399
399
  <button
400
400
  onClick={() => onReply(message)}
401
- 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"
401
+ className="w-7 h-7 rounded-[8px] border border-white/[0.06] bg-white/[0.02] flex items-center justify-center hover:bg-white/[0.08] transition-all cursor-pointer"
402
402
  title="Reply"
403
403
  >
404
404
  <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-text-3">
@@ -411,7 +411,7 @@ export function ChatroomMessageBubble({ message, agents, onToggleReaction, onRep
411
411
  {onTogglePin && (
412
412
  <button
413
413
  onClick={() => onTogglePin(message.id)}
414
- 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"
414
+ className="w-7 h-7 rounded-[8px] border border-white/[0.06] bg-white/[0.02] flex items-center justify-center hover:bg-white/[0.08] transition-all cursor-pointer"
415
415
  title={pinnedMessageIds?.includes(message.id) ? 'Unpin message' : 'Pin message'}
416
416
  >
417
417
  <svg width="12" height="12" viewBox="0 0 24 24" fill={pinnedMessageIds?.includes(message.id) ? 'currentColor' : 'none'} stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={pinnedMessageIds?.includes(message.id) ? 'text-amber-400' : 'text-text-3'}>
@@ -424,7 +424,7 @@ export function ChatroomMessageBubble({ message, agents, onToggleReaction, onRep
424
424
  {onTransfer && !isUser && (
425
425
  <button
426
426
  onClick={() => setShowTransferPicker(!showTransferPicker)}
427
- 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"
427
+ className="w-7 h-7 rounded-[8px] border border-white/[0.06] bg-white/[0.02] flex items-center justify-center hover:bg-white/[0.08] transition-all cursor-pointer"
428
428
  title="Transfer to agent"
429
429
  >
430
430
  <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-text-3">
@@ -449,7 +449,7 @@ export function ChatroomMessageBubble({ message, agents, onToggleReaction, onRep
449
449
  {!isUser && (onDeleteMessage || onMuteAgent || onSetRole) && (
450
450
  <button
451
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"
452
+ className="w-7 h-7 rounded-[8px] border border-white/[0.06] bg-white/[0.02] flex items-center justify-center hover:bg-white/[0.08] transition-all cursor-pointer"
453
453
  title="Moderate"
454
454
  >
455
455
  <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-text-3">
@@ -550,7 +550,8 @@ export function ChatroomMessageBubble({ message, agents, onToggleReaction, onRep
550
550
  {/* Reaction button */}
551
551
  <button
552
552
  onClick={() => setShowPicker(!showPicker)}
553
- 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"
553
+ className="w-7 h-7 rounded-[8px] border border-white/[0.06] bg-white/[0.02] flex items-center justify-center hover:bg-white/[0.08] transition-all cursor-pointer"
554
+ title="Add reaction"
554
555
  >
555
556
  <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-text-3">
556
557
  <circle cx="12" cy="12" r="10" />
@@ -190,6 +190,10 @@ export function ChatroomSheet() {
190
190
 
191
191
  const handleSave = async () => {
192
192
  if (!name.trim() || saving) return
193
+ if (selectedAgentIds.length === 0) {
194
+ toast.error('Select at least one chatroom member.')
195
+ return
196
+ }
193
197
  setSaving(true)
194
198
  try {
195
199
  const payload = {
@@ -363,6 +367,9 @@ export function ChatroomSheet() {
363
367
  <label className="block text-[12px] font-600 text-text-2 mb-1.5">
364
368
  Members ({selectedAgentIds.length} selected)
365
369
  </label>
370
+ <p className="mb-2 text-[11px] text-text-3">
371
+ Choose the agents who should be available in this room. Every chatroom needs at least one member.
372
+ </p>
366
373
  <div className="max-h-[240px] overflow-y-auto rounded-[8px] border border-white/[0.08] bg-white/[0.03]">
367
374
  {agentList.length === 0 ? (
368
375
  <p className="p-3 text-[12px] text-text-3">No agents available</p>
@@ -387,6 +394,11 @@ export function ChatroomSheet() {
387
394
  })
388
395
  )}
389
396
  </div>
397
+ {selectedAgentIds.length === 0 && (
398
+ <p className="mt-2 text-[11px] text-amber-300">
399
+ Select at least one member before creating the room.
400
+ </p>
401
+ )}
390
402
  </div>
391
403
 
392
404
  {/* Routing Rules */}
@@ -484,7 +496,7 @@ export function ChatroomSheet() {
484
496
  <div className="flex items-center gap-3 mt-6">
485
497
  <button
486
498
  onClick={handleSave}
487
- disabled={!name.trim() || saving}
499
+ disabled={!name.trim() || saving || selectedAgentIds.length === 0}
488
500
  className="flex-1 py-2.5 rounded-[8px] text-[13px] font-600 bg-accent-bright text-white hover:bg-accent-bright/90 transition-all disabled:opacity-50 cursor-pointer"
489
501
  >
490
502
  {saving ? 'Saving...' : editing ? 'Save Changes' : 'Create Chatroom'}
@@ -103,6 +103,9 @@ export function ChatroomToolRequestBanner({ agentId, agentName, text, toolOutput
103
103
  <span className="text-accent-bright">{agentName}</span> requesting <span className="text-amber-400">{label}</span>
104
104
  </p>
105
105
  {reason && <p className="text-[11px] text-text-3/60 mt-0.5 truncate">{reason}</p>}
106
+ <p className="text-[10px] text-text-3/45 mt-1">
107
+ Approving updates this agent&apos;s tool access and posts a follow-up continue message in the room.
108
+ </p>
106
109
  </div>
107
110
  {isGranted ? (
108
111
  <span className="text-[11px] text-emerald-400 font-600 shrink-0">Granted</span>
@@ -115,14 +118,14 @@ export function ChatroomToolRequestBanner({ agentId, agentName, text, toolOutput
115
118
  className="px-3 py-1.5 rounded-[8px] bg-amber-500/20 hover:bg-amber-500/30 text-amber-300 text-[11px] font-600 border-none cursor-pointer transition-colors"
116
119
  style={{ fontFamily: 'inherit' }}
117
120
  >
118
- Grant
121
+ Grant & Continue
119
122
  </button>
120
123
  <button
121
124
  onClick={() => handleDeny(toolId)}
122
125
  className="px-3 py-1.5 rounded-[8px] bg-red-500/15 hover:bg-red-500/25 text-red-400 text-[11px] font-600 border-none cursor-pointer transition-colors"
123
126
  style={{ fontFamily: 'inherit' }}
124
127
  >
125
- Deny
128
+ Deny & Reply
126
129
  </button>
127
130
  </div>
128
131
  )}