@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.
- package/README.md +53 -9
- package/bin/server-cmd.js +1 -0
- package/bin/swarmclaw.js +76 -16
- package/next.config.ts +11 -1
- package/package.json +5 -2
- package/scripts/postinstall.mjs +18 -0
- package/src/app/api/canvas/[sessionId]/route.ts +31 -0
- package/src/app/api/chatrooms/[id]/chat/route.ts +284 -0
- package/src/app/api/chatrooms/[id]/members/route.ts +82 -0
- package/src/app/api/chatrooms/[id]/pins/route.ts +39 -0
- package/src/app/api/chatrooms/[id]/reactions/route.ts +42 -0
- package/src/app/api/chatrooms/[id]/route.ts +84 -0
- package/src/app/api/chatrooms/route.ts +50 -0
- package/src/app/api/connectors/[id]/route.ts +1 -0
- package/src/app/api/connectors/route.ts +2 -1
- package/src/app/api/credentials/route.ts +2 -3
- package/src/app/api/files/open/route.ts +43 -0
- package/src/app/api/knowledge/[id]/route.ts +13 -2
- package/src/app/api/knowledge/route.ts +8 -1
- package/src/app/api/memory/route.ts +8 -0
- package/src/app/api/notifications/route.ts +4 -0
- package/src/app/api/orchestrator/run/route.ts +1 -1
- package/src/app/api/plugins/install/route.ts +2 -2
- package/src/app/api/search/route.ts +53 -1
- package/src/app/api/sessions/[id]/chat/route.ts +2 -0
- package/src/app/api/sessions/[id]/edit-resend/route.ts +1 -1
- package/src/app/api/sessions/[id]/fork/route.ts +1 -1
- package/src/app/api/sessions/[id]/messages/route.ts +70 -2
- package/src/app/api/sessions/[id]/route.ts +4 -0
- package/src/app/api/sessions/route.ts +3 -3
- package/src/app/api/settings/route.ts +9 -0
- package/src/app/api/setup/check-provider/route.ts +3 -16
- package/src/app/api/skills/[id]/route.ts +6 -0
- package/src/app/api/skills/route.ts +6 -0
- package/src/app/api/tasks/[id]/route.ts +12 -0
- package/src/app/api/tasks/bulk/route.ts +100 -0
- package/src/app/api/tasks/metrics/route.ts +101 -0
- package/src/app/api/tasks/route.ts +18 -2
- package/src/app/api/tts/route.ts +3 -2
- package/src/app/api/tts/stream/route.ts +3 -2
- package/src/app/api/uploads/[filename]/route.ts +19 -34
- package/src/app/api/uploads/route.ts +94 -0
- package/src/app/api/webhooks/[id]/route.ts +15 -1
- package/src/app/globals.css +63 -15
- package/src/app/page.tsx +142 -13
- package/src/cli/index.js +40 -1
- package/src/cli/index.test.js +30 -0
- package/src/cli/spec.js +42 -0
- package/src/components/agents/agent-avatar.tsx +57 -10
- package/src/components/agents/agent-card.tsx +50 -17
- package/src/components/agents/agent-chat-list.tsx +148 -12
- package/src/components/agents/agent-list.tsx +50 -19
- package/src/components/agents/agent-sheet.tsx +120 -65
- package/src/components/agents/inspector-panel.tsx +81 -6
- package/src/components/agents/openclaw-skills-panel.tsx +32 -3
- package/src/components/agents/personality-builder.tsx +42 -14
- package/src/components/agents/soul-library-picker.tsx +89 -0
- package/src/components/auth/access-key-gate.tsx +10 -3
- package/src/components/auth/setup-wizard.tsx +2 -2
- package/src/components/auth/user-picker.tsx +31 -3
- package/src/components/canvas/canvas-panel.tsx +96 -0
- package/src/components/chat/activity-moment.tsx +173 -0
- package/src/components/chat/chat-area.tsx +46 -22
- package/src/components/chat/chat-header.tsx +457 -286
- package/src/components/chat/chat-preview-panel.tsx +1 -2
- package/src/components/chat/chat-tool-toggles.tsx +1 -1
- package/src/components/chat/delegation-banner.tsx +371 -0
- package/src/components/chat/file-path-chip.tsx +146 -0
- package/src/components/chat/heartbeat-history-panel.tsx +269 -0
- package/src/components/chat/markdown-utils.ts +9 -0
- package/src/components/chat/message-bubble.tsx +356 -315
- package/src/components/chat/message-list.tsx +230 -8
- package/src/components/chat/streaming-bubble.tsx +104 -47
- package/src/components/chat/suggestions-bar.tsx +1 -1
- package/src/components/chat/thinking-indicator.tsx +72 -10
- package/src/components/chat/tool-call-bubble.tsx +111 -73
- package/src/components/chat/tool-request-banner.tsx +31 -7
- package/src/components/chat/transfer-agent-picker.tsx +63 -0
- package/src/components/chatrooms/agent-hover-card.tsx +124 -0
- package/src/components/chatrooms/chatroom-input.tsx +320 -0
- package/src/components/chatrooms/chatroom-list.tsx +130 -0
- package/src/components/chatrooms/chatroom-message.tsx +432 -0
- package/src/components/chatrooms/chatroom-sheet.tsx +215 -0
- package/src/components/chatrooms/chatroom-tool-request-banner.tsx +134 -0
- package/src/components/chatrooms/chatroom-typing-bar.tsx +88 -0
- package/src/components/chatrooms/chatroom-view.tsx +344 -0
- package/src/components/chatrooms/reaction-picker.tsx +273 -0
- package/src/components/connectors/connector-list.tsx +168 -90
- package/src/components/connectors/connector-sheet.tsx +95 -56
- package/src/components/home/home-view.tsx +501 -0
- package/src/components/input/chat-input.tsx +107 -43
- package/src/components/knowledge/knowledge-list.tsx +31 -1
- package/src/components/knowledge/knowledge-sheet.tsx +83 -2
- package/src/components/layout/app-layout.tsx +194 -97
- package/src/components/layout/update-banner.tsx +2 -2
- package/src/components/logs/log-list.tsx +2 -2
- package/src/components/mcp-servers/mcp-server-sheet.tsx +1 -1
- package/src/components/memory/memory-agent-list.tsx +143 -0
- package/src/components/memory/memory-browser.tsx +205 -0
- package/src/components/memory/memory-card.tsx +34 -7
- package/src/components/memory/memory-detail.tsx +359 -120
- package/src/components/memory/memory-sheet.tsx +157 -23
- package/src/components/plugins/plugin-list.tsx +1 -1
- package/src/components/plugins/plugin-sheet.tsx +1 -1
- package/src/components/projects/project-detail.tsx +509 -0
- package/src/components/projects/project-list.tsx +195 -59
- package/src/components/providers/provider-list.tsx +2 -2
- package/src/components/providers/provider-sheet.tsx +3 -3
- package/src/components/schedules/schedule-card.tsx +1 -1
- package/src/components/schedules/schedule-list.tsx +1 -1
- package/src/components/schedules/schedule-sheet.tsx +259 -126
- package/src/components/secrets/secret-sheet.tsx +47 -24
- package/src/components/secrets/secrets-list.tsx +18 -8
- package/src/components/sessions/new-session-sheet.tsx +33 -65
- package/src/components/sessions/session-card.tsx +45 -14
- package/src/components/sessions/session-list.tsx +35 -18
- package/src/components/settings/gateway-disconnect-overlay.tsx +80 -0
- package/src/components/shared/agent-picker-list.tsx +90 -0
- package/src/components/shared/agent-switch-dialog.tsx +156 -0
- package/src/components/shared/attachment-chip.tsx +165 -0
- package/src/components/shared/avatar.tsx +10 -1
- package/src/components/shared/chatroom-picker-list.tsx +61 -0
- package/src/components/shared/check-icon.tsx +12 -0
- package/src/components/shared/confirm-dialog.tsx +1 -1
- package/src/components/shared/connector-platform-icon.tsx +51 -4
- package/src/components/shared/empty-state.tsx +32 -0
- package/src/components/shared/file-preview.tsx +34 -0
- package/src/components/shared/form-styles.ts +2 -0
- package/src/components/shared/icon-button.tsx +16 -2
- package/src/components/shared/keyboard-shortcuts-dialog.tsx +116 -0
- package/src/components/shared/notification-center.tsx +44 -6
- package/src/components/shared/profile-sheet.tsx +115 -0
- package/src/components/shared/reply-quote.tsx +26 -0
- package/src/components/shared/search-dialog.tsx +31 -15
- package/src/components/shared/section-label.tsx +12 -0
- package/src/components/shared/settings/plugin-manager.tsx +1 -1
- package/src/components/shared/settings/section-embedding.tsx +48 -13
- package/src/components/shared/settings/section-orchestrator.tsx +46 -15
- package/src/components/shared/settings/section-providers.tsx +1 -1
- package/src/components/shared/settings/section-secrets.tsx +1 -1
- package/src/components/shared/settings/section-storage.tsx +206 -0
- package/src/components/shared/settings/section-theme.tsx +95 -0
- package/src/components/shared/settings/section-user-preferences.tsx +57 -0
- package/src/components/shared/settings/section-voice.tsx +42 -21
- package/src/components/shared/settings/section-web-search.tsx +30 -6
- package/src/components/shared/settings/settings-page.tsx +182 -27
- package/src/components/shared/settings/settings-sheet.tsx +9 -73
- package/src/components/shared/settings/storage-browser.tsx +259 -0
- package/src/components/shared/sheet-footer.tsx +33 -0
- package/src/components/skills/skill-list.tsx +61 -30
- package/src/components/skills/skill-sheet.tsx +81 -2
- package/src/components/tasks/task-board.tsx +448 -26
- package/src/components/tasks/task-card.tsx +59 -9
- package/src/components/tasks/task-column.tsx +62 -3
- package/src/components/tasks/task-list.tsx +12 -4
- package/src/components/tasks/task-sheet.tsx +416 -74
- package/src/components/ui/hover-card.tsx +52 -0
- package/src/components/usage/metrics-dashboard.tsx +90 -6
- package/src/components/usage/usage-list.tsx +1 -1
- package/src/components/webhooks/webhook-sheet.tsx +1 -1
- package/src/hooks/use-continuous-speech.ts +10 -4
- package/src/hooks/use-view-router.ts +69 -19
- package/src/hooks/use-voice-conversation.ts +53 -10
- package/src/hooks/use-ws.ts +4 -2
- package/src/instrumentation.ts +15 -1
- package/src/lib/chat.ts +2 -0
- package/src/lib/memory.ts +3 -0
- package/src/lib/providers/anthropic.ts +13 -7
- package/src/lib/providers/index.ts +1 -0
- package/src/lib/providers/openai.ts +13 -7
- package/src/lib/server/chat-execution.ts +75 -15
- package/src/lib/server/chatroom-helpers.ts +146 -0
- package/src/lib/server/connectors/manager.ts +229 -7
- package/src/lib/server/context-manager.ts +225 -13
- package/src/lib/server/create-notification.ts +14 -2
- package/src/lib/server/daemon-state.ts +157 -10
- package/src/lib/server/execution-log.ts +1 -0
- package/src/lib/server/heartbeat-service.ts +48 -6
- package/src/lib/server/heartbeat-wake.ts +110 -0
- package/src/lib/server/langgraph-checkpoint.ts +1 -0
- package/src/lib/server/main-agent-loop.ts +1 -1
- package/src/lib/server/memory-consolidation.ts +105 -0
- package/src/lib/server/memory-db.ts +183 -10
- package/src/lib/server/mime.ts +51 -0
- package/src/lib/server/openclaw-gateway.ts +9 -1
- package/src/lib/server/orchestrator-lg.ts +2 -0
- package/src/lib/server/orchestrator.ts +5 -2
- package/src/lib/server/playwright-proxy.mjs +2 -3
- package/src/lib/server/prompt-runtime-context.ts +53 -0
- package/src/lib/server/provider-health.ts +125 -0
- package/src/lib/server/queue.ts +56 -10
- package/src/lib/server/scheduler.ts +8 -0
- package/src/lib/server/session-run-manager.ts +4 -0
- package/src/lib/server/session-tools/canvas.ts +67 -0
- package/src/lib/server/session-tools/chatroom.ts +136 -0
- package/src/lib/server/session-tools/connector.ts +83 -9
- package/src/lib/server/session-tools/context-mgmt.ts +36 -18
- package/src/lib/server/session-tools/crud.ts +21 -0
- package/src/lib/server/session-tools/delegate.ts +68 -4
- package/src/lib/server/session-tools/git.ts +71 -0
- package/src/lib/server/session-tools/http.ts +57 -0
- package/src/lib/server/session-tools/index.ts +10 -0
- package/src/lib/server/session-tools/memory.ts +7 -1
- package/src/lib/server/session-tools/search-providers.ts +16 -8
- package/src/lib/server/session-tools/subagent.ts +106 -0
- package/src/lib/server/session-tools/web.ts +115 -4
- package/src/lib/server/storage.ts +53 -29
- package/src/lib/server/stream-agent-chat.ts +185 -57
- package/src/lib/server/system-events.ts +49 -0
- package/src/lib/server/task-mention.ts +41 -0
- package/src/lib/server/ws-hub.ts +11 -0
- package/src/lib/sessions.ts +10 -0
- package/src/lib/soul-library.ts +103 -0
- package/src/lib/soul-suggestions.ts +109 -0
- package/src/lib/task-dedupe.ts +26 -0
- package/src/lib/tasks.ts +4 -1
- package/src/lib/tool-definitions.ts +2 -0
- package/src/lib/tts.ts +2 -2
- package/src/lib/view-routes.ts +36 -1
- package/src/lib/ws-client.ts +14 -4
- package/src/stores/use-app-store.ts +41 -3
- package/src/stores/use-chat-store.ts +113 -5
- package/src/stores/use-chatroom-store.ts +276 -0
- 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
|
|
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-
|
|
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
|
-
{
|
|
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
|
|
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
|
-
<
|
|
215
|
+
<div
|
|
125
216
|
key={agent.id}
|
|
126
|
-
|
|
127
|
-
className={`w-full text-left py-3 px-
|
|
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
|
-
|
|
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
|
-
{
|
|
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
|
-
</
|
|
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
|
-
|
|
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
|
-
<
|
|
131
|
-
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
{!inSidebar
|
|
140
|
-
|
|
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>
|