@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
@@ -0,0 +1,501 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useMemo, useState } from 'react'
4
+ import { useAppStore } from '@/stores/use-app-store'
5
+ import { useChatStore } from '@/stores/use-chat-store'
6
+ import { AgentAvatar } from '@/components/agents/agent-avatar'
7
+ import { api } from '@/lib/api-client'
8
+ import type { Agent, Session, ActivityEntry, BoardTask, AppNotification } from '@/types'
9
+
10
+ function timeAgo(ts: number): string {
11
+ const diff = Date.now() - ts
12
+ const mins = Math.floor(diff / 60000)
13
+ if (mins < 1) return 'just now'
14
+ if (mins < 60) return `${mins}m ago`
15
+ const hours = Math.floor(mins / 60)
16
+ if (hours < 24) return `${hours}h ago`
17
+ const days = Math.floor(hours / 24)
18
+ return `${days}d ago`
19
+ }
20
+
21
+ function timeUntil(ts: number): string {
22
+ const diff = ts - Date.now()
23
+ if (diff <= 0) return 'now'
24
+ const mins = Math.floor(diff / 60000)
25
+ if (mins < 60) return `in ${mins}m`
26
+ const hours = Math.floor(mins / 60)
27
+ if (hours < 24) return `in ${hours}h`
28
+ const days = Math.floor(hours / 24)
29
+ return `in ${days}d`
30
+ }
31
+
32
+ const ACTIVITY_ICONS: Record<ActivityEntry['action'], string> = {
33
+ created: 'M12 5v14m-7-7h14',
34
+ updated: 'M17 3a2.85 2.85 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z',
35
+ deleted: 'M3 6h18m-2 0v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2',
36
+ started: 'M5 3l14 9-14 9V3z',
37
+ stopped: 'M6 4h4v16H6zm8 0h4v16h-4z',
38
+ queued: 'M12 6v6l4 2',
39
+ completed: 'M20 6L9 17l-5-5',
40
+ failed: 'M18 6L6 18M6 6l12 12',
41
+ approved: 'M22 11.08V12a10 10 0 1 1-5.93-9.14',
42
+ rejected: 'M10 15l5-5m0 5l-5-5',
43
+ }
44
+
45
+ const ACTIVITY_COLORS: Record<ActivityEntry['action'], string> = {
46
+ created: 'text-emerald-400',
47
+ updated: 'text-sky-400',
48
+ deleted: 'text-red-400',
49
+ started: 'text-emerald-400',
50
+ stopped: 'text-text-3',
51
+ queued: 'text-amber-400',
52
+ completed: 'text-emerald-400',
53
+ failed: 'text-red-400',
54
+ approved: 'text-emerald-400',
55
+ rejected: 'text-red-400',
56
+ }
57
+
58
+ const PLATFORM_LABELS: Record<string, string> = {
59
+ discord: 'Discord',
60
+ telegram: 'Telegram',
61
+ slack: 'Slack',
62
+ whatsapp: 'WhatsApp',
63
+ openclaw: 'OpenClaw',
64
+ }
65
+
66
+ export function HomeView() {
67
+ const agents = useAppStore((s) => s.agents)
68
+ const sessions = useAppStore((s) => s.sessions)
69
+ const tasks = useAppStore((s) => s.tasks)
70
+ const connectors = useAppStore((s) => s.connectors)
71
+ const schedules = useAppStore((s) => s.schedules)
72
+ const activityEntries = useAppStore((s) => s.activityEntries)
73
+ const notifications = useAppStore((s) => s.notifications)
74
+ const unreadNotificationCount = useAppStore((s) => s.unreadNotificationCount)
75
+ const streamingSessionId = useChatStore((s) => s.streamingSessionId)
76
+ const loadActivity = useAppStore((s) => s.loadActivity)
77
+ const loadSchedules = useAppStore((s) => s.loadSchedules)
78
+ const loadNotifications = useAppStore((s) => s.loadNotifications)
79
+ const loadConnectors = useAppStore((s) => s.loadConnectors)
80
+ const markNotificationRead = useAppStore((s) => s.markNotificationRead)
81
+ const setActiveView = useAppStore((s) => s.setActiveView)
82
+ const setCurrentAgent = useAppStore((s) => s.setCurrentAgent)
83
+ const setCurrentSession = useAppStore((s) => s.setCurrentSession)
84
+ const setEditingTaskId = useAppStore((s) => s.setEditingTaskId)
85
+ const setTaskSheetOpen = useAppStore((s) => s.setTaskSheetOpen)
86
+ const setMessages = useChatStore((s) => s.setMessages)
87
+ const [todayCost, setTodayCost] = useState(0)
88
+
89
+ const allAgents = Object.values(agents).filter((a) => !a.trashedAt)
90
+ const pinnedAgents = allAgents.filter((a) => a.pinned)
91
+
92
+ const recentChats = useMemo(
93
+ () =>
94
+ Object.values(sessions)
95
+ .sort((a, b) => (b.lastActiveAt || 0) - (a.lastActiveAt || 0))
96
+ .slice(0, 5),
97
+ [sessions],
98
+ )
99
+
100
+ // Quick stats
101
+ const agentCount = allAgents.length
102
+ const allTasks = Object.values(tasks)
103
+ const activeTaskCount = allTasks.filter((t) => t.status === 'running' || t.status === 'queued').length
104
+ const allConnectors = Object.values(connectors)
105
+ const activeConnectorCount = allConnectors.filter((c) => c.status === 'running').length
106
+
107
+ // Agents with running tasks
108
+ const runningAgentIds = useMemo(() => {
109
+ const set = new Set<string>()
110
+ for (const task of allTasks) {
111
+ if (task.status === 'running' && task.agentId) set.add(task.agentId)
112
+ }
113
+ return set
114
+ }, [allTasks])
115
+
116
+ // Running tasks for the running tasks section
117
+ const runningTasks = useMemo(
118
+ () => allTasks.filter((t) => t.status === 'running' || t.status === 'queued').slice(0, 5),
119
+ // eslint-disable-next-line react-hooks/exhaustive-deps
120
+ [tasks],
121
+ )
122
+
123
+ // Upcoming schedules
124
+ const upcomingSchedules = useMemo(() => {
125
+ const now = Date.now()
126
+ return Object.values(schedules)
127
+ .filter((s) => s.status === 'active' && s.nextRunAt && s.nextRunAt > now)
128
+ .sort((a, b) => (a.nextRunAt || 0) - (b.nextRunAt || 0))
129
+ .slice(0, 5)
130
+ }, [schedules])
131
+
132
+ // Unread notifications
133
+ const unreadNotifications = useMemo(
134
+ () => notifications.filter((n) => !n.read).slice(0, 5),
135
+ [notifications],
136
+ )
137
+
138
+ // Recent activity (last 8)
139
+ const recentActivity = useMemo(() => activityEntries.slice(0, 8), [activityEntries])
140
+
141
+ // Load data on mount
142
+ useEffect(() => {
143
+ void loadActivity({ limit: 8 })
144
+ void loadSchedules()
145
+ void loadNotifications()
146
+ void loadConnectors()
147
+ api<{ records: Array<{ estimatedCost: number }> }>('GET', '/usage?range=24h')
148
+ .then((data) => {
149
+ const total = (data.records || []).reduce((s, r) => s + (r.estimatedCost || 0), 0)
150
+ setTodayCost(total)
151
+ })
152
+ .catch(() => {})
153
+ // eslint-disable-next-line react-hooks/exhaustive-deps
154
+ }, [])
155
+
156
+ const handleAgentClick = (agent: Agent) => {
157
+ setMessages([])
158
+ void setCurrentAgent(agent.id)
159
+ setActiveView('agents')
160
+ }
161
+
162
+ const handleChatClick = (session: Session) => {
163
+ setCurrentSession(session.id)
164
+ setActiveView('agents')
165
+ }
166
+
167
+ const handleTaskClick = (task: BoardTask) => {
168
+ setEditingTaskId(task.id)
169
+ setTaskSheetOpen(true)
170
+ setActiveView('tasks')
171
+ }
172
+
173
+ const handleNotificationClick = (n: AppNotification) => {
174
+ if (!n.read) void markNotificationRead(n.id)
175
+ if (n.entityType === 'agent' && n.entityId) {
176
+ void setCurrentAgent(n.entityId)
177
+ setActiveView('agents')
178
+ } else if (n.entityType === 'task' && n.entityId) {
179
+ setEditingTaskId(n.entityId)
180
+ setTaskSheetOpen(true)
181
+ setActiveView('tasks')
182
+ }
183
+ }
184
+
185
+ return (
186
+ <div className="flex-1 overflow-y-auto">
187
+ <div className="max-w-[800px] mx-auto px-6 py-10">
188
+ {/* Header */}
189
+ <div className="mb-10">
190
+ <h1 className="font-display text-[28px] font-700 text-text tracking-[-0.03em]">
191
+ SwarmClaw
192
+ </h1>
193
+ <p className="text-[14px] text-text-3 mt-1">
194
+ Your AI agent orchestration dashboard
195
+ </p>
196
+ </div>
197
+
198
+ {/* Quick Stats */}
199
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-3 mb-10">
200
+ <StatCard label="Agents" value={String(agentCount)} />
201
+ <StatCard label="Active Tasks" value={String(activeTaskCount)} accent={activeTaskCount > 0} />
202
+ <StatCard label="Today's Cost" value={`$${todayCost.toFixed(2)}`} />
203
+ <StatCard label="Connectors" value={`${activeConnectorCount}/${allConnectors.length}`} accent={activeConnectorCount > 0} />
204
+ </div>
205
+
206
+ {/* Notifications banner */}
207
+ {unreadNotifications.length > 0 && (
208
+ <section className="mb-8">
209
+ <div className="rounded-[14px] border border-amber-400/20 bg-amber-400/[0.04] overflow-hidden">
210
+ <div className="flex items-center gap-2 px-4 py-2.5 border-b border-amber-400/10">
211
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-amber-400">
212
+ <path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9" />
213
+ <path d="M13.73 21a2 2 0 0 1-3.46 0" />
214
+ </svg>
215
+ <span className="text-[12px] font-600 text-amber-400">
216
+ {unreadNotificationCount} unread notification{unreadNotificationCount !== 1 ? 's' : ''}
217
+ </span>
218
+ </div>
219
+ <div className="flex flex-col">
220
+ {unreadNotifications.map((n) => (
221
+ <button
222
+ key={n.id}
223
+ onClick={() => handleNotificationClick(n)}
224
+ className="flex items-start gap-3 px-4 py-2.5 text-left bg-transparent border-none cursor-pointer
225
+ hover:bg-white/[0.03] transition-colors w-full"
226
+ style={{ fontFamily: 'inherit' }}
227
+ >
228
+ <div className={`w-1.5 h-1.5 rounded-full mt-1.5 shrink-0 ${
229
+ n.type === 'error' ? 'bg-red-400' : n.type === 'warning' ? 'bg-amber-400' : n.type === 'success' ? 'bg-emerald-400' : 'bg-sky-400'
230
+ }`} />
231
+ <div className="flex-1 min-w-0">
232
+ <span className="text-[13px] font-500 text-text">{n.title}</span>
233
+ {n.message && <p className="text-[11px] text-text-3/60 truncate mt-0.5 m-0">{n.message}</p>}
234
+ </div>
235
+ <span className="text-[10px] text-text-3/40 shrink-0 mt-0.5">{timeAgo(n.createdAt)}</span>
236
+ </button>
237
+ ))}
238
+ </div>
239
+ </div>
240
+ </section>
241
+ )}
242
+
243
+ {/* Connector Status */}
244
+ <section className="mb-8">
245
+ <SectionHeader label="Connectors" onViewAll={allConnectors.length > 0 ? () => setActiveView('connectors') : undefined} />
246
+ {allConnectors.length > 0 ? (
247
+ <div className="flex gap-2 flex-wrap">
248
+ {allConnectors.map((c) => (
249
+ <div
250
+ key={c.id}
251
+ className="flex items-center gap-2 px-3 py-2 rounded-[10px] bg-white/[0.03] border border-white/[0.06]"
252
+ >
253
+ <div className={`w-2 h-2 rounded-full ${
254
+ c.status === 'running' ? 'bg-emerald-400 shadow-[0_0_6px_rgba(52,211,153,0.4)]'
255
+ : c.status === 'error' ? 'bg-red-400' : 'bg-text-3/30'
256
+ }`} />
257
+ <span className="text-[12px] font-500 text-text">{c.name}</span>
258
+ <span className="text-[10px] text-text-3/50">{PLATFORM_LABELS[c.platform] || c.platform}</span>
259
+ </div>
260
+ ))}
261
+ </div>
262
+ ) : (
263
+ <EmptySection text="No connectors configured — bridge agents to Discord, Slack, Telegram, or WhatsApp" />
264
+ )}
265
+ </section>
266
+
267
+ {/* Two-column layout: Running Tasks + Upcoming Schedules */}
268
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
269
+ {/* Running Tasks */}
270
+ <section>
271
+ <SectionHeader label="Running Tasks" onViewAll={runningTasks.length > 0 ? () => setActiveView('tasks') : undefined} />
272
+ {runningTasks.length > 0 ? (
273
+ <div className="flex flex-col gap-1">
274
+ {runningTasks.map((task) => {
275
+ const agent = task.agentId ? agents[task.agentId] : null
276
+ return (
277
+ <button
278
+ key={task.id}
279
+ onClick={() => handleTaskClick(task)}
280
+ className="flex items-center gap-2.5 px-3 py-2.5 rounded-[10px] bg-transparent border-none
281
+ hover:bg-white/[0.04] transition-colors cursor-pointer w-full text-left"
282
+ style={{ fontFamily: 'inherit' }}
283
+ >
284
+ <div className={`w-2 h-2 rounded-full shrink-0 ${
285
+ task.status === 'running' ? 'bg-emerald-400 animate-pulse' : 'bg-amber-400'
286
+ }`} />
287
+ <div className="flex-1 min-w-0">
288
+ <span className="text-[13px] font-500 text-text truncate block">{task.title}</span>
289
+ <span className="text-[11px] text-text-3/50">
290
+ {agent?.name || 'Unassigned'} · {task.status === 'running' ? 'running' : 'queued'}{task.startedAt ? ` · ${timeAgo(task.startedAt)}` : ''}
291
+ </span>
292
+ </div>
293
+ </button>
294
+ )
295
+ })}
296
+ </div>
297
+ ) : (
298
+ <div className="py-4 px-3 text-[12px] text-text-3/40">No tasks running</div>
299
+ )}
300
+ </section>
301
+
302
+ {/* Upcoming Schedules */}
303
+ <section>
304
+ <SectionHeader label="Upcoming Schedules" onViewAll={upcomingSchedules.length > 0 ? () => setActiveView('schedules') : undefined} />
305
+ {upcomingSchedules.length > 0 ? (
306
+ <div className="flex flex-col gap-1">
307
+ {upcomingSchedules.map((sched) => {
308
+ const agent = sched.agentId ? agents[sched.agentId] : null
309
+ return (
310
+ <div
311
+ key={sched.id}
312
+ className="flex items-center gap-2.5 px-3 py-2.5 rounded-[10px]"
313
+ >
314
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-text-3/50 shrink-0">
315
+ <circle cx="12" cy="12" r="10" /><path d="M12 6v6l4 2" />
316
+ </svg>
317
+ <div className="flex-1 min-w-0">
318
+ <span className="text-[13px] font-500 text-text truncate block">{sched.name}</span>
319
+ <span className="text-[11px] text-text-3/50">
320
+ {agent?.name || 'No agent'} · {sched.nextRunAt ? timeUntil(sched.nextRunAt) : '—'}
321
+ </span>
322
+ </div>
323
+ </div>
324
+ )
325
+ })}
326
+ </div>
327
+ ) : (
328
+ <div className="py-4 px-3 text-[12px] text-text-3/40">No upcoming schedules</div>
329
+ )}
330
+ </section>
331
+ </div>
332
+
333
+ {/* Pinned Agents */}
334
+ <section className="mb-8">
335
+ <SectionHeader label="Pinned Agents" />
336
+ {pinnedAgents.length > 0 ? (
337
+ <div className="flex gap-3 overflow-x-auto pb-2">
338
+ {pinnedAgents.map((agent) => {
339
+ const threadSession = agent.threadSessionId ? sessions[agent.threadSessionId] as Session | undefined : undefined
340
+ const heartbeatOn = agent.heartbeatEnabled === true && (agent.tools?.length ?? 0) > 0
341
+ const recentlyActive = (threadSession?.lastActiveAt ?? 0) > Date.now() - 30 * 60 * 1000
342
+ const isOnline = runningAgentIds.has(agent.id) || (threadSession?.active ?? false) || heartbeatOn || recentlyActive
343
+ const isTyping = streamingSessionId === agent.threadSessionId
344
+ const lastActive = threadSession?.lastActiveAt || agent.lastUsedAt || agent.updatedAt
345
+ const modelLabel = agent.model ? agent.model.split('/').pop()?.split(':')[0] : agent.provider
346
+
347
+ return (
348
+ <button
349
+ key={agent.id}
350
+ onClick={() => handleAgentClick(agent)}
351
+ className="flex flex-col items-center gap-1.5 px-4 py-3.5 rounded-[14px] bg-white/[0.03] border border-white/[0.06]
352
+ hover:bg-white/[0.06] hover:border-white/[0.1] transition-all cursor-pointer min-w-[130px] shrink-0"
353
+ style={{ fontFamily: 'inherit' }}
354
+ >
355
+ <div className="relative">
356
+ <AgentAvatar seed={agent.avatarSeed} name={agent.name} size={36} />
357
+ <div className={`absolute -bottom-0.5 -right-0.5 w-2.5 h-2.5 rounded-full border-2 border-surface ${
358
+ isTyping ? 'bg-accent-bright animate-pulse'
359
+ : isOnline ? 'bg-emerald-400 shadow-[0_0_6px_rgba(52,211,153,0.4)]'
360
+ : 'bg-text-3/30'
361
+ }`} />
362
+ </div>
363
+ <span className="font-display text-[13px] font-600 text-text truncate max-w-[110px]">
364
+ {agent.name}
365
+ </span>
366
+ {isTyping ? (
367
+ <span className="text-[10px] text-accent-bright/70 flex items-center gap-1">
368
+ <span className="flex gap-0.5">
369
+ <span className="w-1 h-1 rounded-full bg-accent-bright/70 animate-bounce [animation-delay:0ms]" />
370
+ <span className="w-1 h-1 rounded-full bg-accent-bright/70 animate-bounce [animation-delay:150ms]" />
371
+ <span className="w-1 h-1 rounded-full bg-accent-bright/70 animate-bounce [animation-delay:300ms]" />
372
+ </span>
373
+ typing
374
+ </span>
375
+ ) : (
376
+ <span className={`text-[10px] ${isOnline ? 'text-emerald-400/80' : 'text-text-3/50'}`}>
377
+ {isOnline ? 'Online' : lastActive ? timeAgo(lastActive) : 'Idle'}
378
+ </span>
379
+ )}
380
+ {modelLabel && (
381
+ <span className="text-[9px] text-text-3/40 font-mono truncate max-w-[110px]">
382
+ {modelLabel}
383
+ </span>
384
+ )}
385
+ </button>
386
+ )
387
+ })}
388
+ </div>
389
+ ) : (
390
+ <div className="py-6 px-4 rounded-[14px] bg-white/[0.02] border border-dashed border-white/[0.06] text-center">
391
+ <p className="text-[13px] text-text-3/60">
392
+ Star agents from the chat list for quick access
393
+ </p>
394
+ </div>
395
+ )}
396
+ </section>
397
+
398
+ {/* Recent Chats */}
399
+ <section className="mb-8">
400
+ <SectionHeader label="Recent Chats" />
401
+ {recentChats.length > 0 ? (
402
+ <div className="flex flex-col gap-1">
403
+ {recentChats.map((session) => {
404
+ const agent = session.agentId ? agents[session.agentId] : null
405
+ const lastMsg = session.messages?.[session.messages.length - 1]
406
+ const displayName = agent?.name || 'Chat'
407
+ return (
408
+ <button
409
+ key={session.id}
410
+ onClick={() => handleChatClick(session)}
411
+ className="flex items-center gap-3 px-4 py-3 rounded-[12px] bg-transparent border-none
412
+ hover:bg-white/[0.04] transition-all cursor-pointer w-full text-left"
413
+ style={{ fontFamily: 'inherit' }}
414
+ >
415
+ <AgentAvatar
416
+ seed={agent?.avatarSeed}
417
+ name={displayName}
418
+ size={28}
419
+ />
420
+ <div className="flex-1 min-w-0">
421
+ <div className="flex items-center gap-2">
422
+ <span className="text-[13px] font-600 text-text truncate">
423
+ {displayName}
424
+ </span>
425
+ <span className="text-[11px] text-text-3/50 shrink-0">
426
+ {timeAgo(session.lastActiveAt || session.createdAt)}
427
+ </span>
428
+ </div>
429
+ {lastMsg && (
430
+ <p className="text-[12px] text-text-3/60 truncate mt-0.5 m-0">
431
+ {lastMsg.text.slice(0, 80)}
432
+ </p>
433
+ )}
434
+ </div>
435
+ </button>
436
+ )
437
+ })}
438
+ </div>
439
+ ) : (
440
+ <EmptySection text="No chats yet — start by clicking an agent" />
441
+ )}
442
+ </section>
443
+
444
+ {/* Activity Feed */}
445
+ {recentActivity.length > 0 && (
446
+ <section className="mb-10">
447
+ <SectionHeader label="Recent Activity" />
448
+ <div className="flex flex-col gap-0.5">
449
+ {recentActivity.map((entry) => (
450
+ <div key={entry.id} className="flex items-center gap-2.5 px-3 py-2 rounded-[10px]">
451
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"
452
+ className={`shrink-0 ${ACTIVITY_COLORS[entry.action] || 'text-text-3'}`}>
453
+ <path d={ACTIVITY_ICONS[entry.action] || ACTIVITY_ICONS.updated} />
454
+ </svg>
455
+ <span className="text-[12px] text-text-3/80 flex-1 truncate">{entry.summary}</span>
456
+ <span className="text-[10px] text-text-3/40 shrink-0">{timeAgo(entry.timestamp)}</span>
457
+ </div>
458
+ ))}
459
+ </div>
460
+ </section>
461
+ )}
462
+ </div>
463
+ </div>
464
+ )
465
+ }
466
+
467
+ function SectionHeader({ label, onViewAll }: { label: string; onViewAll?: () => void }) {
468
+ return (
469
+ <div className="flex items-center justify-between mb-3">
470
+ <h2 className="font-display text-[13px] font-600 text-text-2 uppercase tracking-[0.08em]">
471
+ {label}
472
+ </h2>
473
+ {onViewAll && (
474
+ <button
475
+ onClick={onViewAll}
476
+ className="text-[11px] text-text-3/50 hover:text-text-3 transition-colors bg-transparent border-none cursor-pointer"
477
+ style={{ fontFamily: 'inherit' }}
478
+ >
479
+ View all →
480
+ </button>
481
+ )}
482
+ </div>
483
+ )
484
+ }
485
+
486
+ function StatCard({ label, value, accent }: { label: string; value: string; accent?: boolean }) {
487
+ return (
488
+ <div className="px-4 py-3 rounded-[12px] bg-white/[0.03] border border-white/[0.06]">
489
+ <p className="text-[11px] font-600 text-text-3/60 uppercase tracking-wider mb-1">{label}</p>
490
+ <p className={`font-display text-[20px] font-700 tracking-[-0.02em] ${accent ? 'text-accent-bright' : 'text-text'}`}>{value}</p>
491
+ </div>
492
+ )
493
+ }
494
+
495
+ function EmptySection({ text }: { text: string }) {
496
+ return (
497
+ <div className="py-6 px-4 rounded-[14px] bg-white/[0.02] border border-dashed border-white/[0.06] text-center">
498
+ <p className="text-[13px] text-text-3/60">{text}</p>
499
+ </div>
500
+ )
501
+ }