@swarmclawai/swarmclaw 0.5.3 → 0.6.2

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 (224) hide show
  1. package/README.md +53 -9
  2. package/bin/server-cmd.js +1 -0
  3. package/bin/swarmclaw.js +76 -16
  4. package/next.config.ts +11 -1
  5. package/package.json +5 -2
  6. package/scripts/postinstall.mjs +18 -0
  7. package/src/app/api/canvas/[sessionId]/route.ts +31 -0
  8. package/src/app/api/chatrooms/[id]/chat/route.ts +284 -0
  9. package/src/app/api/chatrooms/[id]/members/route.ts +82 -0
  10. package/src/app/api/chatrooms/[id]/pins/route.ts +39 -0
  11. package/src/app/api/chatrooms/[id]/reactions/route.ts +42 -0
  12. package/src/app/api/chatrooms/[id]/route.ts +84 -0
  13. package/src/app/api/chatrooms/route.ts +50 -0
  14. package/src/app/api/connectors/[id]/route.ts +1 -0
  15. package/src/app/api/connectors/route.ts +2 -1
  16. package/src/app/api/credentials/route.ts +2 -3
  17. package/src/app/api/files/open/route.ts +43 -0
  18. package/src/app/api/knowledge/[id]/route.ts +13 -2
  19. package/src/app/api/knowledge/route.ts +8 -1
  20. package/src/app/api/memory/route.ts +8 -0
  21. package/src/app/api/notifications/route.ts +4 -0
  22. package/src/app/api/orchestrator/run/route.ts +1 -1
  23. package/src/app/api/plugins/install/route.ts +2 -2
  24. package/src/app/api/search/route.ts +53 -1
  25. package/src/app/api/sessions/[id]/chat/route.ts +2 -0
  26. package/src/app/api/sessions/[id]/edit-resend/route.ts +1 -1
  27. package/src/app/api/sessions/[id]/fork/route.ts +1 -1
  28. package/src/app/api/sessions/[id]/messages/route.ts +70 -2
  29. package/src/app/api/sessions/[id]/route.ts +4 -0
  30. package/src/app/api/sessions/route.ts +3 -3
  31. package/src/app/api/settings/route.ts +9 -0
  32. package/src/app/api/setup/check-provider/route.ts +3 -16
  33. package/src/app/api/skills/[id]/route.ts +6 -0
  34. package/src/app/api/skills/route.ts +6 -0
  35. package/src/app/api/tasks/[id]/route.ts +12 -0
  36. package/src/app/api/tasks/bulk/route.ts +100 -0
  37. package/src/app/api/tasks/metrics/route.ts +101 -0
  38. package/src/app/api/tasks/route.ts +18 -2
  39. package/src/app/api/tts/route.ts +3 -2
  40. package/src/app/api/tts/stream/route.ts +3 -2
  41. package/src/app/api/uploads/[filename]/route.ts +19 -34
  42. package/src/app/api/uploads/route.ts +94 -0
  43. package/src/app/api/webhooks/[id]/route.ts +15 -1
  44. package/src/app/globals.css +63 -15
  45. package/src/app/page.tsx +142 -13
  46. package/src/cli/index.js +40 -1
  47. package/src/cli/index.test.js +30 -0
  48. package/src/cli/spec.js +42 -0
  49. package/src/components/agents/agent-avatar.tsx +57 -10
  50. package/src/components/agents/agent-card.tsx +50 -17
  51. package/src/components/agents/agent-chat-list.tsx +148 -12
  52. package/src/components/agents/agent-list.tsx +50 -19
  53. package/src/components/agents/agent-sheet.tsx +120 -65
  54. package/src/components/agents/inspector-panel.tsx +81 -6
  55. package/src/components/agents/openclaw-skills-panel.tsx +32 -3
  56. package/src/components/agents/personality-builder.tsx +42 -14
  57. package/src/components/agents/soul-library-picker.tsx +89 -0
  58. package/src/components/auth/access-key-gate.tsx +10 -3
  59. package/src/components/auth/setup-wizard.tsx +2 -2
  60. package/src/components/auth/user-picker.tsx +31 -3
  61. package/src/components/canvas/canvas-panel.tsx +96 -0
  62. package/src/components/chat/activity-moment.tsx +173 -0
  63. package/src/components/chat/chat-area.tsx +46 -22
  64. package/src/components/chat/chat-header.tsx +457 -286
  65. package/src/components/chat/chat-preview-panel.tsx +1 -2
  66. package/src/components/chat/chat-tool-toggles.tsx +1 -1
  67. package/src/components/chat/delegation-banner.tsx +371 -0
  68. package/src/components/chat/file-path-chip.tsx +146 -0
  69. package/src/components/chat/heartbeat-history-panel.tsx +269 -0
  70. package/src/components/chat/markdown-utils.ts +9 -0
  71. package/src/components/chat/message-bubble.tsx +356 -315
  72. package/src/components/chat/message-list.tsx +230 -8
  73. package/src/components/chat/streaming-bubble.tsx +104 -47
  74. package/src/components/chat/suggestions-bar.tsx +1 -1
  75. package/src/components/chat/thinking-indicator.tsx +72 -10
  76. package/src/components/chat/tool-call-bubble.tsx +111 -73
  77. package/src/components/chat/tool-request-banner.tsx +31 -7
  78. package/src/components/chat/transfer-agent-picker.tsx +63 -0
  79. package/src/components/chatrooms/agent-hover-card.tsx +124 -0
  80. package/src/components/chatrooms/chatroom-input.tsx +320 -0
  81. package/src/components/chatrooms/chatroom-list.tsx +130 -0
  82. package/src/components/chatrooms/chatroom-message.tsx +432 -0
  83. package/src/components/chatrooms/chatroom-sheet.tsx +215 -0
  84. package/src/components/chatrooms/chatroom-tool-request-banner.tsx +134 -0
  85. package/src/components/chatrooms/chatroom-typing-bar.tsx +88 -0
  86. package/src/components/chatrooms/chatroom-view.tsx +344 -0
  87. package/src/components/chatrooms/reaction-picker.tsx +273 -0
  88. package/src/components/connectors/connector-list.tsx +168 -90
  89. package/src/components/connectors/connector-sheet.tsx +95 -56
  90. package/src/components/home/home-view.tsx +501 -0
  91. package/src/components/input/chat-input.tsx +107 -43
  92. package/src/components/knowledge/knowledge-list.tsx +31 -1
  93. package/src/components/knowledge/knowledge-sheet.tsx +83 -2
  94. package/src/components/layout/app-layout.tsx +194 -97
  95. package/src/components/layout/update-banner.tsx +2 -2
  96. package/src/components/logs/log-list.tsx +2 -2
  97. package/src/components/mcp-servers/mcp-server-sheet.tsx +1 -1
  98. package/src/components/memory/memory-agent-list.tsx +143 -0
  99. package/src/components/memory/memory-browser.tsx +205 -0
  100. package/src/components/memory/memory-card.tsx +34 -7
  101. package/src/components/memory/memory-detail.tsx +359 -120
  102. package/src/components/memory/memory-sheet.tsx +157 -23
  103. package/src/components/plugins/plugin-list.tsx +1 -1
  104. package/src/components/plugins/plugin-sheet.tsx +1 -1
  105. package/src/components/projects/project-detail.tsx +509 -0
  106. package/src/components/projects/project-list.tsx +195 -59
  107. package/src/components/providers/provider-list.tsx +2 -2
  108. package/src/components/providers/provider-sheet.tsx +3 -3
  109. package/src/components/schedules/schedule-card.tsx +1 -1
  110. package/src/components/schedules/schedule-list.tsx +1 -1
  111. package/src/components/schedules/schedule-sheet.tsx +259 -126
  112. package/src/components/secrets/secret-sheet.tsx +47 -24
  113. package/src/components/secrets/secrets-list.tsx +18 -8
  114. package/src/components/sessions/new-session-sheet.tsx +33 -65
  115. package/src/components/sessions/session-card.tsx +45 -14
  116. package/src/components/sessions/session-list.tsx +35 -18
  117. package/src/components/settings/gateway-disconnect-overlay.tsx +80 -0
  118. package/src/components/shared/agent-picker-list.tsx +90 -0
  119. package/src/components/shared/agent-switch-dialog.tsx +156 -0
  120. package/src/components/shared/attachment-chip.tsx +165 -0
  121. package/src/components/shared/avatar.tsx +10 -1
  122. package/src/components/shared/chatroom-picker-list.tsx +61 -0
  123. package/src/components/shared/check-icon.tsx +12 -0
  124. package/src/components/shared/confirm-dialog.tsx +1 -1
  125. package/src/components/shared/connector-platform-icon.tsx +51 -4
  126. package/src/components/shared/empty-state.tsx +32 -0
  127. package/src/components/shared/file-preview.tsx +34 -0
  128. package/src/components/shared/form-styles.ts +2 -0
  129. package/src/components/shared/icon-button.tsx +16 -2
  130. package/src/components/shared/keyboard-shortcuts-dialog.tsx +116 -0
  131. package/src/components/shared/notification-center.tsx +44 -6
  132. package/src/components/shared/profile-sheet.tsx +115 -0
  133. package/src/components/shared/reply-quote.tsx +26 -0
  134. package/src/components/shared/search-dialog.tsx +31 -15
  135. package/src/components/shared/section-label.tsx +12 -0
  136. package/src/components/shared/settings/plugin-manager.tsx +1 -1
  137. package/src/components/shared/settings/section-embedding.tsx +48 -13
  138. package/src/components/shared/settings/section-orchestrator.tsx +46 -15
  139. package/src/components/shared/settings/section-providers.tsx +1 -1
  140. package/src/components/shared/settings/section-secrets.tsx +1 -1
  141. package/src/components/shared/settings/section-storage.tsx +206 -0
  142. package/src/components/shared/settings/section-theme.tsx +95 -0
  143. package/src/components/shared/settings/section-user-preferences.tsx +57 -0
  144. package/src/components/shared/settings/section-voice.tsx +42 -21
  145. package/src/components/shared/settings/section-web-search.tsx +30 -6
  146. package/src/components/shared/settings/settings-page.tsx +182 -27
  147. package/src/components/shared/settings/settings-sheet.tsx +9 -73
  148. package/src/components/shared/settings/storage-browser.tsx +259 -0
  149. package/src/components/shared/sheet-footer.tsx +33 -0
  150. package/src/components/skills/skill-list.tsx +61 -30
  151. package/src/components/skills/skill-sheet.tsx +81 -2
  152. package/src/components/tasks/task-board.tsx +448 -26
  153. package/src/components/tasks/task-card.tsx +59 -9
  154. package/src/components/tasks/task-column.tsx +62 -3
  155. package/src/components/tasks/task-list.tsx +12 -4
  156. package/src/components/tasks/task-sheet.tsx +416 -74
  157. package/src/components/ui/hover-card.tsx +52 -0
  158. package/src/components/usage/metrics-dashboard.tsx +90 -6
  159. package/src/components/usage/usage-list.tsx +1 -1
  160. package/src/components/webhooks/webhook-sheet.tsx +1 -1
  161. package/src/hooks/use-continuous-speech.ts +10 -4
  162. package/src/hooks/use-view-router.ts +69 -19
  163. package/src/hooks/use-voice-conversation.ts +53 -10
  164. package/src/hooks/use-ws.ts +4 -2
  165. package/src/instrumentation.ts +15 -1
  166. package/src/lib/chat.ts +2 -0
  167. package/src/lib/memory.ts +3 -0
  168. package/src/lib/providers/anthropic.ts +13 -7
  169. package/src/lib/providers/index.ts +1 -0
  170. package/src/lib/providers/openai.ts +13 -7
  171. package/src/lib/server/chat-execution.ts +75 -15
  172. package/src/lib/server/chatroom-helpers.ts +146 -0
  173. package/src/lib/server/connectors/manager.ts +229 -7
  174. package/src/lib/server/context-manager.ts +225 -13
  175. package/src/lib/server/create-notification.ts +14 -2
  176. package/src/lib/server/daemon-state.ts +157 -10
  177. package/src/lib/server/execution-log.ts +1 -0
  178. package/src/lib/server/heartbeat-service.ts +48 -6
  179. package/src/lib/server/heartbeat-wake.ts +110 -0
  180. package/src/lib/server/langgraph-checkpoint.ts +1 -0
  181. package/src/lib/server/main-agent-loop.ts +1 -1
  182. package/src/lib/server/memory-consolidation.ts +105 -0
  183. package/src/lib/server/memory-db.ts +183 -10
  184. package/src/lib/server/mime.ts +51 -0
  185. package/src/lib/server/openclaw-gateway.ts +9 -1
  186. package/src/lib/server/orchestrator-lg.ts +2 -0
  187. package/src/lib/server/orchestrator.ts +5 -2
  188. package/src/lib/server/playwright-proxy.mjs +2 -3
  189. package/src/lib/server/prompt-runtime-context.ts +53 -0
  190. package/src/lib/server/provider-health.ts +125 -0
  191. package/src/lib/server/queue.ts +56 -10
  192. package/src/lib/server/scheduler.ts +8 -0
  193. package/src/lib/server/session-run-manager.ts +4 -0
  194. package/src/lib/server/session-tools/canvas.ts +67 -0
  195. package/src/lib/server/session-tools/chatroom.ts +136 -0
  196. package/src/lib/server/session-tools/connector.ts +83 -9
  197. package/src/lib/server/session-tools/context-mgmt.ts +36 -18
  198. package/src/lib/server/session-tools/crud.ts +21 -0
  199. package/src/lib/server/session-tools/delegate.ts +68 -4
  200. package/src/lib/server/session-tools/git.ts +71 -0
  201. package/src/lib/server/session-tools/http.ts +57 -0
  202. package/src/lib/server/session-tools/index.ts +10 -0
  203. package/src/lib/server/session-tools/memory.ts +7 -1
  204. package/src/lib/server/session-tools/search-providers.ts +16 -8
  205. package/src/lib/server/session-tools/subagent.ts +106 -0
  206. package/src/lib/server/session-tools/web.ts +115 -4
  207. package/src/lib/server/storage.ts +53 -29
  208. package/src/lib/server/stream-agent-chat.ts +185 -57
  209. package/src/lib/server/system-events.ts +49 -0
  210. package/src/lib/server/task-mention.ts +41 -0
  211. package/src/lib/server/ws-hub.ts +11 -0
  212. package/src/lib/sessions.ts +10 -0
  213. package/src/lib/soul-library.ts +103 -0
  214. package/src/lib/soul-suggestions.ts +109 -0
  215. package/src/lib/task-dedupe.ts +26 -0
  216. package/src/lib/tasks.ts +4 -1
  217. package/src/lib/tool-definitions.ts +2 -0
  218. package/src/lib/tts.ts +2 -2
  219. package/src/lib/view-routes.ts +36 -1
  220. package/src/lib/ws-client.ts +14 -4
  221. package/src/stores/use-app-store.ts +41 -3
  222. package/src/stores/use-chat-store.ts +113 -5
  223. package/src/stores/use-chatroom-store.ts +276 -0
  224. package/src/types/index.ts +88 -4
@@ -1,11 +1,13 @@
1
1
  'use client'
2
2
 
3
- import { useEffect, useMemo, useState } from 'react'
3
+ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
4
4
  import { useAppStore } from '@/stores/use-app-store'
5
5
  import { useChatStore } from '@/stores/use-chat-store'
6
+ import { useChatroomStore } from '@/stores/use-chatroom-store'
6
7
  import { fetchMessages } from '@/lib/sessions'
7
8
  import type { Agent, Session } from '@/types'
8
9
  import { AgentAvatar } from './agent-avatar'
10
+ import { toast } from 'sonner'
9
11
 
10
12
  interface Props {
11
13
  inSidebar?: boolean
@@ -21,9 +23,25 @@ export function AgentChatList({ inSidebar, onSelect }: Props) {
21
23
  const setMessages = useChatStore((s) => s.setMessages)
22
24
  const setAgentSheetOpen = useAppStore((s) => s.setAgentSheetOpen)
23
25
  const tasks = useAppStore((s) => s.tasks)
26
+ const togglePinAgent = useAppStore((s) => s.togglePinAgent)
27
+ const appSettings = useAppStore((s) => s.appSettings)
28
+ const updateSettings = useAppStore((s) => s.updateSettings)
24
29
  const streamingSessionId = useChatStore((s) => s.streamingSessionId)
30
+ const chatFilter = useAppStore((s) => s.chatFilter ?? 'all')
31
+ const setChatFilter = useAppStore((s) => s.setChatFilter)
32
+ const chatrooms = useChatroomStore((s) => s.chatrooms)
33
+ const chatroomStreaming = useChatroomStore((s) => s.streamingAgents)
25
34
  const [search, setSearch] = useState('')
26
35
 
36
+ // FLIP animation refs
37
+ const rowRefs = useRef<Map<string, HTMLElement>>(new Map())
38
+ const previousTopRef = useRef<Map<string, number>>(new Map())
39
+
40
+ const setRowRef = useCallback((id: string, el: HTMLElement | null) => {
41
+ if (el) rowRefs.current.set(id, el)
42
+ else rowRefs.current.delete(id)
43
+ }, [])
44
+
27
45
  useEffect(() => { loadAgents() }, [loadAgents])
28
46
 
29
47
  // Build agent list sorted by last activity in their thread session
@@ -42,6 +60,21 @@ export function AgentChatList({ inSidebar, onSelect }: Props) {
42
60
  })
43
61
  }, [agents, sessions, search])
44
62
 
63
+ // Compute agents active in chatrooms (message in last 30min or currently streaming)
64
+ const chatroomActiveAgentIds = useMemo(() => {
65
+ const set = new Set<string>()
66
+ const cutoff = Date.now() - 30 * 60 * 1000
67
+ for (const chatroom of Object.values(chatrooms)) {
68
+ for (let i = chatroom.messages.length - 1; i >= 0; i--) {
69
+ const msg = chatroom.messages[i]
70
+ if (msg.time < cutoff) break
71
+ if (msg.role === 'assistant' && msg.senderId !== 'user') set.add(msg.senderId)
72
+ }
73
+ }
74
+ for (const agentId of chatroomStreaming.keys()) set.add(agentId)
75
+ return set
76
+ }, [chatrooms, chatroomStreaming])
77
+
45
78
  // Compute running tasks per agent
46
79
  const runningAgentIds = useMemo(() => {
47
80
  const set = new Set<string>()
@@ -51,6 +84,42 @@ export function AgentChatList({ inSidebar, onSelect }: Props) {
51
84
  return set
52
85
  }, [tasks])
53
86
 
87
+ // Apply chatFilter
88
+ const filteredAgents = useMemo(() => {
89
+ if (chatFilter === 'all') return sortedAgents
90
+ const now = Date.now()
91
+ return sortedAgents.filter((a) => {
92
+ const threadSession = a.threadSessionId ? sessions[a.threadSessionId] as Session | undefined : undefined
93
+ const isRunning = runningAgentIds.has(a.id) || (threadSession?.active ?? false)
94
+ const isStreaming = streamingSessionId === a.threadSessionId
95
+ const isChatroomActive = chatroomActiveAgentIds.has(a.id)
96
+ if (chatFilter === 'active') return isRunning || isStreaming || isChatroomActive
97
+ // 'recent' — activity within 24h
98
+ const lastActive = threadSession?.lastActiveAt || a.updatedAt
99
+ return now - lastActive < 86_400_000
100
+ })
101
+ }, [sortedAgents, chatFilter, sessions, runningAgentIds, streamingSessionId, chatroomActiveAgentIds])
102
+
103
+ // FLIP: animate row position changes
104
+ useLayoutEffect(() => {
105
+ const prevTop = previousTopRef.current
106
+ for (const agent of filteredAgents) {
107
+ const el = rowRefs.current.get(agent.id)
108
+ if (!el) continue
109
+ const newTop = el.getBoundingClientRect().top
110
+ const oldTop = prevTop.get(agent.id)
111
+ if (oldTop !== undefined && oldTop !== newTop) {
112
+ const delta = oldTop - newTop
113
+ el.animate(
114
+ [{ transform: `translateY(${delta}px)` }, { transform: 'translateY(0)' }],
115
+ { duration: 300, easing: 'cubic-bezier(0.16, 1, 0.3, 1)' },
116
+ )
117
+ }
118
+ prevTop.set(agent.id, newTop)
119
+ }
120
+ // eslint-disable-next-line react-hooks/exhaustive-deps
121
+ }, [filteredAgents.map((a) => a.id).join(',')])
122
+
54
123
  const handleSelect = async (agent: Agent) => {
55
124
  await setCurrentAgent(agent.id)
56
125
  // Load messages for the thread
@@ -59,7 +128,9 @@ export function AgentChatList({ inSidebar, onSelect }: Props) {
59
128
  try {
60
129
  const msgs = await fetchMessages(state.currentSessionId)
61
130
  setMessages(msgs)
62
- } catch { /* ignore */ }
131
+ } catch (err: unknown) {
132
+ console.error('[agent-chat-list] Failed to load messages:', err instanceof Error ? err.message : String(err))
133
+ }
63
134
  }
64
135
  onSelect?.()
65
136
  // Delay scroll so React renders the new messages first
@@ -84,7 +155,7 @@ export function AgentChatList({ inSidebar, onSelect }: Props) {
84
155
  {!inSidebar && (
85
156
  <button
86
157
  onClick={() => setAgentSheetOpen(true)}
87
- className="mt-3 px-8 py-3 rounded-[14px] border-none bg-[#6366F1] text-white
158
+ className="mt-3 px-8 py-3 rounded-[14px] border-none bg-accent-bright text-white
88
159
  text-[14px] font-600 cursor-pointer active:scale-95 transition-all duration-200
89
160
  shadow-[0_4px_16px_rgba(99,102,241,0.2)]"
90
161
  style={{ fontFamily: 'inherit' }}
@@ -98,6 +169,24 @@ export function AgentChatList({ inSidebar, onSelect }: Props) {
98
169
 
99
170
  return (
100
171
  <div className="flex-1 overflow-y-auto">
172
+ {/* Filter control */}
173
+ {sortedAgents.length > 2 && (
174
+ <div className="flex items-center gap-1 px-4 pt-2.5 pb-1">
175
+ {(['all', 'active', 'recent'] as const).map((f) => (
176
+ <button
177
+ key={f}
178
+ type="button"
179
+ onClick={() => setChatFilter(f)}
180
+ data-active={chatFilter === f || undefined}
181
+ className="label-mono px-2.5 py-1 rounded-[6px] border-none cursor-pointer transition-colors
182
+ data-[active]:bg-accent-soft data-[active]:text-accent-bright
183
+ bg-transparent text-text-3 hover:text-text-2 hover:bg-white/[0.04]"
184
+ >
185
+ {f}
186
+ </button>
187
+ ))}
188
+ </div>
189
+ )}
101
190
  {(sortedAgents.length > 5 || search) && (
102
191
  <div className="px-4 py-2.5">
103
192
  <input
@@ -112,26 +201,27 @@ export function AgentChatList({ inSidebar, onSelect }: Props) {
112
201
  </div>
113
202
  )}
114
203
  <div className="flex flex-col gap-0.5 px-2 pb-4">
115
- {sortedAgents.map((agent) => {
204
+ {filteredAgents.map((agent) => {
116
205
  const threadSession = agent.threadSessionId ? sessions[agent.threadSessionId] as Session | undefined : undefined
117
206
  const lastMsg = threadSession?.messages?.at(-1)
118
207
  const isActive = currentAgentId === agent.id
119
- const isWorking = runningAgentIds.has(agent.id) || (threadSession?.active ?? false) || (threadSession?.heartbeatEnabled ?? false)
208
+ const heartbeatOn = agent.heartbeatEnabled === true && (agent.tools?.length ?? 0) > 0
209
+ const recentlyActive = (threadSession?.lastActiveAt ?? 0) > Date.now() - 30 * 60 * 1000
210
+ const isWorking = runningAgentIds.has(agent.id) || (threadSession?.active ?? false) || heartbeatOn || recentlyActive || chatroomActiveAgentIds.has(agent.id)
120
211
  const isTyping = streamingSessionId === agent.threadSessionId
121
212
  const preview = lastMsg?.text?.slice(0, 80)?.replace(/\n/g, ' ') || ''
122
213
 
123
214
  return (
124
- <button
215
+ <div
125
216
  key={agent.id}
126
- onClick={() => handleSelect(agent)}
127
- className={`w-full text-left py-3 px-3.5 rounded-[12px] cursor-pointer transition-all duration-150 border-none
217
+ ref={(el) => setRowRef(agent.id, el)}
218
+ className={`group/row relative w-full text-left py-3 px-4 rounded-[12px] cursor-pointer transition-all duration-150 border-none
128
219
  ${isActive
129
220
  ? 'bg-accent-soft/80 border border-accent-bright/20'
130
221
  : 'bg-transparent hover:bg-white/[0.02]'}`}
131
- style={{ fontFamily: 'inherit' }}
222
+ onClick={() => handleSelect(agent)}
132
223
  >
133
224
  <div className="flex items-center gap-2.5">
134
- {/* Avatar with status dot */}
135
225
  <div className="relative shrink-0">
136
226
  <AgentAvatar seed={agent.avatarSeed || null} name={agent.name} size={36} />
137
227
  <div className={`absolute -bottom-0.5 -right-0.5 w-2.5 h-2.5 rounded-full border-2 border-bg ${
@@ -144,8 +234,54 @@ export function AgentChatList({ inSidebar, onSelect }: Props) {
144
234
  {agent.name}
145
235
  </span>
146
236
  <span className="text-[10px] text-text-3/60 font-mono shrink-0">
147
- {agent.model ? agent.model.split('/').pop()?.split(':')[0] : agent.provider}
237
+ {(threadSession?.model || agent.model)
238
+ ? (threadSession?.model || agent.model)!.split('/').pop()?.split(':')[0]
239
+ : agent.provider}
148
240
  </span>
241
+ {/* Set as default agent */}
242
+ {(() => {
243
+ const isDefault = appSettings.defaultAgentId === agent.id
244
+ return (
245
+ <button
246
+ onClick={async (e) => {
247
+ e.stopPropagation()
248
+ if (isDefault) {
249
+ await updateSettings({ defaultAgentId: null })
250
+ toast.success('Default agent cleared')
251
+ } else {
252
+ await updateSettings({ defaultAgentId: agent.id })
253
+ toast.success(`${agent.name} set as default`)
254
+ }
255
+ }}
256
+ aria-label={isDefault ? 'Remove as default' : 'Set as default agent'}
257
+ title={isDefault ? 'Default agent — click to clear' : 'Set as default agent'}
258
+ className={`shrink-0 p-1 rounded-[6px] transition-all bg-transparent border-none cursor-pointer hover:bg-white/[0.06]
259
+ ${isDefault ? 'opacity-100 text-accent-bright' : 'opacity-0 group-hover/row:opacity-60 hover:!opacity-100 text-text-3'}`}
260
+ style={{ fontFamily: 'inherit' }}
261
+ >
262
+ <svg width="11" height="11" viewBox="0 0 24 24" fill={isDefault ? 'currentColor' : 'none'} stroke="currentColor" strokeWidth="2" strokeLinecap="round">
263
+ <path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
264
+ {isDefault && <path d="M9 22V12h6v10" fill="rgba(0,0,0,0.3)" stroke="none" />}
265
+ </svg>
266
+ </button>
267
+ )
268
+ })()}
269
+ {/* Pin button — inline after model label */}
270
+ <button
271
+ onClick={(e) => {
272
+ e.stopPropagation()
273
+ togglePinAgent(agent.id)
274
+ toast.success(agent.pinned ? 'Agent unpinned' : 'Agent pinned')
275
+ }}
276
+ aria-label={agent.pinned ? 'Unpin agent' : 'Pin agent'}
277
+ className={`shrink-0 p-1 rounded-[6px] transition-all bg-transparent border-none cursor-pointer hover:bg-white/[0.06]
278
+ ${agent.pinned ? 'opacity-100 text-amber-400' : 'opacity-0 group-hover/row:opacity-60 hover:!opacity-100 text-text-3'}`}
279
+ style={{ fontFamily: 'inherit' }}
280
+ >
281
+ <svg width="11" height="11" viewBox="0 0 24 24" fill={agent.pinned ? 'currentColor' : 'none'} stroke="currentColor" strokeWidth="2" strokeLinecap="round">
282
+ <polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" />
283
+ </svg>
284
+ </button>
149
285
  </div>
150
286
  {isTyping ? (
151
287
  <div className="text-[12px] text-accent-bright/70 mt-0.5 flex items-center gap-1.5">
@@ -163,7 +299,7 @@ export function AgentChatList({ inSidebar, onSelect }: Props) {
163
299
  ) : null}
164
300
  </div>
165
301
  </div>
166
- </button>
302
+ </div>
167
303
  )
168
304
  })}
169
305
  </div>
@@ -6,6 +6,8 @@ import { api } from '@/lib/api-client'
6
6
  import { AgentCard } from './agent-card'
7
7
  import { TrashList } from './trash-list'
8
8
  import { useApprovalStore } from '@/stores/use-approval-store'
9
+ import { Skeleton } from '@/components/shared/skeleton'
10
+ import { EmptyState } from '@/components/shared/empty-state'
9
11
 
10
12
  interface Props {
11
13
  inSidebar?: boolean
@@ -49,7 +51,8 @@ export function AgentList({ inSidebar }: Props) {
49
51
  } catch { /* ignore */ }
50
52
  }, [mainSession, loadSessions])
51
53
 
52
- useEffect(() => { loadAgents() }, [])
54
+ const [loaded, setLoaded] = useState(Object.keys(agents).length > 0)
55
+ useEffect(() => { loadAgents().then(() => setLoaded(true)) }, [])
53
56
 
54
57
  // Compute which agents are "running" (have active sessions)
55
58
  const runningAgentIds = useMemo(() => {
@@ -60,6 +63,27 @@ export function AgentList({ inSidebar }: Props) {
60
63
  return ids
61
64
  }, [sessions])
62
65
 
66
+ // Re-evaluate online status periodically (Date.now() can't be called in useMemo directly)
67
+ const [now, setNow] = useState(() => Date.now())
68
+ useEffect(() => {
69
+ const id = setInterval(() => setNow(Date.now()), 60_000)
70
+ return () => clearInterval(id)
71
+ }, [])
72
+
73
+ // Agents that are "online": heartbeat enabled + tools, or recently active (within 30min)
74
+ const onlineAgentIds = useMemo(() => {
75
+ const ids = new Set<string>()
76
+ const recentThreshold = now - 30 * 60 * 1000
77
+ for (const a of Object.values(agents)) {
78
+ if (a.heartbeatEnabled === true && (a.tools?.length ?? 0) > 0) { ids.add(a.id); continue }
79
+ // Check if any session for this agent was active in the last 30 minutes
80
+ for (const s of Object.values(sessions)) {
81
+ if (s.agentId === a.id && (s.lastActiveAt ?? 0) > recentThreshold) { ids.add(a.id); break }
82
+ }
83
+ }
84
+ return ids
85
+ }, [agents, sessions, now])
86
+
63
87
  // Approval counts per agent
64
88
  const approvalsByAgent = useMemo(() => {
65
89
  const counts: Record<string, number> = {}
@@ -126,28 +150,35 @@ export function AgentList({ inSidebar }: Props) {
126
150
  }
127
151
 
128
152
  if (!filtered.length && !search) {
153
+ // Show skeleton cards while loading
154
+ if (!loaded) {
155
+ return (
156
+ <div className="flex-1 flex flex-col gap-1 px-2 pt-4">
157
+ {Array.from({ length: 4 }).map((_, i) => (
158
+ <div key={i} className="py-3.5 px-4 rounded-[14px] border border-transparent">
159
+ <div className="flex items-center gap-2.5">
160
+ <Skeleton className="rounded-full" width={28} height={28} />
161
+ <Skeleton className="rounded-[6px]" width={120} height={14} />
162
+ </div>
163
+ <Skeleton className="rounded-[6px] mt-2" width="80%" height={12} />
164
+ <Skeleton className="rounded-[6px] mt-1.5" width={80} height={11} />
165
+ </div>
166
+ ))}
167
+ </div>
168
+ )
169
+ }
129
170
  return (
130
- <div className="flex-1 flex flex-col items-center justify-center gap-4 text-text-3 p-8 text-center">
131
- <div className="w-12 h-12 rounded-[14px] bg-accent-soft flex items-center justify-center mb-1">
171
+ <EmptyState
172
+ icon={
132
173
  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-accent-bright">
133
174
  <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
134
175
  <circle cx="12" cy="7" r="4" />
135
176
  </svg>
136
- </div>
137
- <p className="font-display text-[15px] font-600 text-text-2">No agents yet</p>
138
- <p className="text-[13px] text-text-3/50">Create AI agents and orchestrators</p>
139
- {!inSidebar && (
140
- <button
141
- onClick={() => setAgentSheetOpen(true)}
142
- className="mt-3 px-8 py-3 rounded-[14px] border-none bg-[#6366F1] text-white
143
- text-[14px] font-600 cursor-pointer active:scale-95 transition-all duration-200
144
- shadow-[0_4px_16px_rgba(99,102,241,0.2)]"
145
- style={{ fontFamily: 'inherit' }}
146
- >
147
- + New Agent
148
- </button>
149
- )}
150
- </div>
177
+ }
178
+ title="No agents yet"
179
+ subtitle="Create AI agents and orchestrators"
180
+ action={!inSidebar ? { label: '+ New Agent', onClick: () => setAgentSheetOpen(true) } : undefined}
181
+ />
151
182
  )
152
183
  }
153
184
 
@@ -212,7 +243,7 @@ export function AgentList({ inSidebar }: Props) {
212
243
  <div className="flex flex-col gap-1 px-2 pb-4">
213
244
  {filtered.map((p) => (
214
245
  <div key={p.id} ref={(el) => { if (el) cardRefs.current.set(p.id, el); else cardRefs.current.delete(p.id) }}>
215
- <AgentCard agent={p} isDefault={p.id === defaultAgentId} isRunning={runningAgentIds.has(p.id)} isSelected={p.id === selectedAgentId} onSetDefault={handleSetDefault} />
246
+ <AgentCard agent={p} isDefault={p.id === defaultAgentId} isRunning={runningAgentIds.has(p.id)} isOnline={onlineAgentIds.has(p.id)} isSelected={p.id === selectedAgentId} onSetDefault={handleSetDefault} />
216
247
  </div>
217
248
  ))}
218
249
  </div>