@swarmclawai/swarmclaw 0.5.2 → 0.6.0
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 +42 -7
- package/bin/swarmclaw.js +76 -16
- package/next.config.ts +11 -1
- package/package.json +4 -2
- package/public/screenshots/agents.png +0 -0
- package/public/screenshots/dashboard.png +0 -0
- package/public/screenshots/providers.png +0 -0
- package/public/screenshots/tasks.png +0 -0
- package/scripts/postinstall.mjs +18 -0
- package/src/app/api/chatrooms/[id]/chat/route.ts +410 -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/credentials/route.ts +2 -3
- 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/[id]/route.ts +27 -0
- package/src/app/api/notifications/route.ts +68 -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 +155 -0
- 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/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 +20 -0
- package/src/app/api/tasks/bulk/route.ts +100 -0
- package/src/app/api/tasks/route.ts +1 -0
- package/src/app/api/usage/route.ts +45 -0
- package/src/app/api/webhooks/[id]/route.ts +15 -1
- package/src/app/globals.css +58 -15
- package/src/app/page.tsx +142 -13
- package/src/cli/index.js +42 -0
- package/src/cli/index.test.js +30 -0
- package/src/cli/spec.js +32 -0
- package/src/components/agents/agent-avatar.tsx +57 -10
- package/src/components/agents/agent-card.tsx +48 -15
- package/src/components/agents/agent-chat-list.tsx +123 -10
- package/src/components/agents/agent-list.tsx +50 -19
- package/src/components/agents/agent-sheet.tsx +56 -63
- 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/chat/activity-moment.tsx +169 -0
- package/src/components/chat/chat-header.tsx +2 -0
- package/src/components/chat/chat-tool-toggles.tsx +1 -1
- package/src/components/chat/file-path-chip.tsx +125 -0
- package/src/components/chat/markdown-utils.ts +9 -0
- package/src/components/chat/message-bubble.tsx +46 -295
- package/src/components/chat/message-list.tsx +50 -1
- package/src/components/chat/streaming-bubble.tsx +36 -46
- 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 +66 -70
- 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 +123 -0
- package/src/components/chatrooms/chatroom-message.tsx +427 -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-sheet.tsx +34 -47
- package/src/components/home/home-view.tsx +501 -0
- package/src/components/input/chat-input.tsx +79 -41
- 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 +209 -83
- package/src/components/layout/mobile-header.tsx +2 -0
- 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 +3 -2
- package/src/components/schedules/schedule-list.tsx +1 -1
- package/src/components/schedules/schedule-sheet.tsx +25 -25
- 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/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/check-icon.tsx +12 -0
- package/src/components/shared/confirm-dialog.tsx +1 -1
- 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/keyboard-shortcuts-dialog.tsx +116 -0
- package/src/components/shared/notification-center.tsx +223 -0
- 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 +296 -0
- 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-providers.tsx +1 -1
- package/src/components/shared/settings/section-secrets.tsx +1 -1
- package/src/components/shared/settings/section-theme.tsx +95 -0
- package/src/components/shared/settings/section-user-preferences.tsx +39 -0
- package/src/components/shared/settings/settings-page.tsx +180 -27
- package/src/components/shared/settings/settings-sheet.tsx +9 -73
- 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 +46 -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 +89 -72
- package/src/components/ui/hover-card.tsx +52 -0
- package/src/components/usage/metrics-dashboard.tsx +78 -0
- package/src/components/usage/usage-list.tsx +1 -1
- package/src/components/webhooks/webhook-sheet.tsx +1 -1
- package/src/hooks/use-view-router.ts +69 -19
- package/src/instrumentation.ts +15 -1
- package/src/lib/chat.ts +2 -0
- package/src/lib/cron-human.ts +114 -0
- package/src/lib/memory.ts +3 -0
- package/src/lib/server/chat-execution.ts +24 -4
- package/src/lib/server/connectors/manager.ts +11 -0
- package/src/lib/server/context-manager.ts +225 -13
- package/src/lib/server/create-notification.ts +42 -0
- package/src/lib/server/daemon-state.ts +165 -10
- package/src/lib/server/execution-log.ts +1 -0
- package/src/lib/server/heartbeat-service.ts +40 -5
- package/src/lib/server/heartbeat-wake.ts +110 -0
- package/src/lib/server/langgraph-checkpoint.ts +1 -0
- package/src/lib/server/memory-consolidation.ts +92 -0
- package/src/lib/server/memory-db.ts +51 -6
- package/src/lib/server/openclaw-gateway.ts +9 -1
- package/src/lib/server/provider-health.ts +125 -0
- package/src/lib/server/queue.ts +5 -4
- package/src/lib/server/scheduler.ts +8 -0
- package/src/lib/server/session-run-manager.ts +4 -0
- package/src/lib/server/session-tools/chatroom.ts +136 -0
- package/src/lib/server/session-tools/context-mgmt.ts +36 -18
- package/src/lib/server/session-tools/index.ts +2 -0
- package/src/lib/server/session-tools/memory.ts +6 -1
- package/src/lib/server/storage.ts +80 -29
- package/src/lib/server/stream-agent-chat.ts +153 -47
- package/src/lib/server/system-events.ts +49 -0
- package/src/lib/server/ws-hub.ts +11 -0
- package/src/lib/soul-suggestions.ts +109 -0
- package/src/lib/tasks.ts +4 -1
- package/src/lib/view-routes.ts +36 -1
- package/src/lib/ws-client.ts +14 -4
- package/src/proxy.ts +79 -2
- package/src/stores/use-app-store.ts +94 -3
- package/src/stores/use-chat-store.ts +48 -3
- package/src/stores/use-chatroom-store.ts +276 -0
- package/src/types/index.ts +69 -2
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { useEffect, useState } from 'react'
|
|
4
4
|
import { useAppStore } from '@/stores/use-app-store'
|
|
5
5
|
import { BottomSheet } from '@/components/shared/bottom-sheet'
|
|
6
|
+
import { AgentAvatar } from '@/components/agents/agent-avatar'
|
|
6
7
|
import { api } from '@/lib/api-client'
|
|
7
8
|
|
|
8
9
|
const inputClass = 'w-full px-4 py-3 rounded-[14px] bg-bg border border-white/[0.06] text-text text-[14px] outline-none focus:border-accent-bright/40 transition-colors placeholder:text-text-3/70'
|
|
@@ -25,7 +26,7 @@ export function SecretSheet() {
|
|
|
25
26
|
const [saving, setSaving] = useState(false)
|
|
26
27
|
|
|
27
28
|
const editing = editingId ? secrets[editingId] : null
|
|
28
|
-
const
|
|
29
|
+
const agentList = Object.values(agents)
|
|
29
30
|
|
|
30
31
|
useEffect(() => {
|
|
31
32
|
if (open) loadAgents()
|
|
@@ -74,8 +75,8 @@ export function SecretSheet() {
|
|
|
74
75
|
}
|
|
75
76
|
await loadSecrets()
|
|
76
77
|
handleClose()
|
|
77
|
-
} catch (err:
|
|
78
|
-
console.error('Failed to save secret:', err.message)
|
|
78
|
+
} catch (err: unknown) {
|
|
79
|
+
console.error('Failed to save secret:', err instanceof Error ? err.message : String(err))
|
|
79
80
|
} finally {
|
|
80
81
|
setSaving(false)
|
|
81
82
|
}
|
|
@@ -87,11 +88,21 @@ export function SecretSheet() {
|
|
|
87
88
|
await api('DELETE', `/secrets/${editing.id}`)
|
|
88
89
|
await loadSecrets()
|
|
89
90
|
handleClose()
|
|
90
|
-
} catch (err:
|
|
91
|
-
console.error('Failed to delete secret:', err.message)
|
|
91
|
+
} catch (err: unknown) {
|
|
92
|
+
console.error('Failed to delete secret:', err instanceof Error ? err.message : String(err))
|
|
92
93
|
}
|
|
93
94
|
}
|
|
94
95
|
|
|
96
|
+
const toggleAgent = (id: string) => {
|
|
97
|
+
setAgentIds((prev) => prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id])
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const scopeHelperText = scope === 'global'
|
|
101
|
+
? 'This secret will be accessible to all agents'
|
|
102
|
+
: agentIds.length === 0
|
|
103
|
+
? 'Select which agents can access this secret'
|
|
104
|
+
: `${agentIds.length} agent(s) selected`
|
|
105
|
+
|
|
95
106
|
return (
|
|
96
107
|
<BottomSheet open={open} onClose={handleClose}>
|
|
97
108
|
<div className="space-y-5">
|
|
@@ -125,30 +136,42 @@ export function SecretSheet() {
|
|
|
125
136
|
}`}
|
|
126
137
|
style={{ fontFamily: 'inherit' }}
|
|
127
138
|
>
|
|
128
|
-
{s === 'global' ? '
|
|
139
|
+
{s === 'global' ? 'Global' : 'Specific'}
|
|
129
140
|
</button>
|
|
130
141
|
))}
|
|
131
142
|
</div>
|
|
143
|
+
<p className="text-[11px] text-text-3/60 mt-1.5 pl-1">{scopeHelperText}</p>
|
|
132
144
|
</div>
|
|
133
145
|
|
|
134
|
-
{scope === 'agent' &&
|
|
146
|
+
{scope === 'agent' && (
|
|
135
147
|
<div>
|
|
136
|
-
<label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">
|
|
137
|
-
<div className="
|
|
138
|
-
{
|
|
139
|
-
<
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
148
|
+
<label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">Agents</label>
|
|
149
|
+
<div className="max-h-[240px] overflow-y-auto rounded-[12px] border border-white/[0.06] bg-white/[0.03]">
|
|
150
|
+
{agentList.length === 0 ? (
|
|
151
|
+
<p className="p-3 text-[12px] text-text-3">No agents available</p>
|
|
152
|
+
) : (
|
|
153
|
+
agentList.map((agent) => {
|
|
154
|
+
const selected = agentIds.includes(agent.id)
|
|
155
|
+
return (
|
|
156
|
+
<button
|
|
157
|
+
key={agent.id}
|
|
158
|
+
onClick={() => toggleAgent(agent.id)}
|
|
159
|
+
className={`w-full flex items-center gap-2.5 px-3 py-2 text-left transition-all cursor-pointer ${
|
|
160
|
+
selected ? 'bg-accent-soft/40' : 'hover:bg-white/[0.04]'
|
|
161
|
+
}`}
|
|
162
|
+
style={{ fontFamily: 'inherit' }}
|
|
163
|
+
>
|
|
164
|
+
<AgentAvatar seed={agent.avatarSeed} name={agent.name} size={24} />
|
|
165
|
+
<span className="text-[13px] text-text flex-1 truncate">{agent.name}</span>
|
|
166
|
+
{selected && (
|
|
167
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" className="text-accent-bright shrink-0">
|
|
168
|
+
<polyline points="20 6 9 17 4 12" />
|
|
169
|
+
</svg>
|
|
170
|
+
)}
|
|
171
|
+
</button>
|
|
172
|
+
)
|
|
173
|
+
})
|
|
174
|
+
)}
|
|
152
175
|
</div>
|
|
153
176
|
</div>
|
|
154
177
|
)}
|
|
@@ -168,7 +191,7 @@ export function SecretSheet() {
|
|
|
168
191
|
<button
|
|
169
192
|
onClick={handleSave}
|
|
170
193
|
disabled={saving || !name.trim() || (!editing && !value.trim())}
|
|
171
|
-
className="px-8 py-3 rounded-[14px] border-none bg-
|
|
194
|
+
className="px-8 py-3 rounded-[14px] border-none bg-accent-bright text-white text-[14px] font-600 cursor-pointer disabled:opacity-30 transition-all hover:brightness-110"
|
|
172
195
|
style={{ fontFamily: 'inherit' }}
|
|
173
196
|
>
|
|
174
197
|
{saving ? 'Saving...' : editing ? 'Update' : 'Save'}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useEffect } from 'react'
|
|
4
4
|
import { useAppStore } from '@/stores/use-app-store'
|
|
5
|
+
import { AgentAvatar } from '@/components/agents/agent-avatar'
|
|
5
6
|
import { api } from '@/lib/api-client'
|
|
6
7
|
|
|
7
8
|
interface Props {
|
|
@@ -37,7 +38,7 @@ export function SecretsList({ inSidebar }: Props) {
|
|
|
37
38
|
</svg>
|
|
38
39
|
</div>
|
|
39
40
|
<p className="text-[13px] text-text-3 mb-1 font-600">No secrets yet</p>
|
|
40
|
-
<p className="text-[12px] text-text-3/60">Add API keys & credentials for
|
|
41
|
+
<p className="text-[12px] text-text-3/60">Add API keys & credentials for your agents</p>
|
|
41
42
|
<button
|
|
42
43
|
onClick={() => { setEditingSecretId(null); setSecretSheetOpen(true) }}
|
|
43
44
|
className="mt-3 px-4 py-2 rounded-[10px] bg-transparent text-accent-bright text-[13px] font-600 cursor-pointer border border-accent-bright/20 hover:bg-accent-soft transition-all"
|
|
@@ -54,11 +55,11 @@ export function SecretsList({ inSidebar }: Props) {
|
|
|
54
55
|
<div className={inSidebar ? 'space-y-2' : 'grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3'}>
|
|
55
56
|
{secretList.map((secret) => {
|
|
56
57
|
const scopeLabel = secret.scope === 'global'
|
|
57
|
-
? '
|
|
58
|
-
: `${secret.agentIds.length}
|
|
59
|
-
const
|
|
60
|
-
? secret.agentIds.map((id) => agents[id]
|
|
61
|
-
:
|
|
58
|
+
? 'Global'
|
|
59
|
+
: `${secret.agentIds.length} agent(s)`
|
|
60
|
+
const scopedAgents = secret.scope === 'agent'
|
|
61
|
+
? secret.agentIds.map((id) => agents[id]).filter(Boolean)
|
|
62
|
+
: []
|
|
62
63
|
return (
|
|
63
64
|
<button
|
|
64
65
|
key={secret.id}
|
|
@@ -97,8 +98,17 @@ export function SecretsList({ inSidebar }: Props) {
|
|
|
97
98
|
{scopeLabel}
|
|
98
99
|
</span>
|
|
99
100
|
</div>
|
|
100
|
-
{
|
|
101
|
-
<div className="
|
|
101
|
+
{scopedAgents.length > 0 && (
|
|
102
|
+
<div className="flex items-center gap-1.5 mt-1.5 pl-[22px]">
|
|
103
|
+
<div className="flex items-center -space-x-1.5">
|
|
104
|
+
{scopedAgents.slice(0, 5).map((agent) => (
|
|
105
|
+
<AgentAvatar key={agent.id} seed={agent.avatarSeed} name={agent.name} size={16} className="ring-1 ring-surface" />
|
|
106
|
+
))}
|
|
107
|
+
</div>
|
|
108
|
+
{scopedAgents.length > 5 && (
|
|
109
|
+
<span className="text-[10px] font-600 text-text-3/60 ml-0.5">+{scopedAgents.length - 5}</span>
|
|
110
|
+
)}
|
|
111
|
+
</div>
|
|
102
112
|
)}
|
|
103
113
|
</button>
|
|
104
114
|
)
|
|
@@ -8,7 +8,11 @@ import { BottomSheet } from '@/components/shared/bottom-sheet'
|
|
|
8
8
|
import { DirBrowser } from '@/components/shared/dir-browser'
|
|
9
9
|
import { TOOL_LABELS, TOOL_DESCRIPTIONS } from '@/components/chat/tool-call-bubble'
|
|
10
10
|
import { ModelCombobox } from '@/components/shared/model-combobox'
|
|
11
|
+
import { AgentPickerList } from '@/components/shared/agent-picker-list'
|
|
12
|
+
import { SheetFooter } from '@/components/shared/sheet-footer'
|
|
13
|
+
import { inputClass } from '@/components/shared/form-styles'
|
|
11
14
|
import type { ProviderType, SessionTool } from '@/types'
|
|
15
|
+
import { SectionLabel } from '@/components/shared/section-label'
|
|
12
16
|
|
|
13
17
|
export function NewSessionSheet() {
|
|
14
18
|
const open = useAppStore((s) => s.newSessionOpen)
|
|
@@ -61,14 +65,14 @@ export function NewSessionSheet() {
|
|
|
61
65
|
// Auto-select last used agent, or default agent if no history
|
|
62
66
|
const agentsList = Object.values(agents)
|
|
63
67
|
const lastAgentId = typeof window !== 'undefined' ? localStorage.getItem('swarmclaw-last-agent') : null
|
|
64
|
-
const lastAgent = lastAgentId ? agentsList.find((a
|
|
65
|
-
const defaultAgent = lastAgent || agentsList.find((a
|
|
68
|
+
const lastAgent = lastAgentId ? agentsList.find((a) => a.id === lastAgentId) : null
|
|
69
|
+
const defaultAgent = lastAgent || agentsList.find((a) => a.id === 'default') || agentsList[0]
|
|
66
70
|
if (defaultAgent) {
|
|
67
|
-
setSelectedAgentId(
|
|
68
|
-
setProvider(
|
|
69
|
-
setModel(
|
|
70
|
-
setCredentialId(
|
|
71
|
-
if (
|
|
71
|
+
setSelectedAgentId(defaultAgent.id)
|
|
72
|
+
setProvider(defaultAgent.provider || 'claude-cli')
|
|
73
|
+
setModel(defaultAgent.model || '')
|
|
74
|
+
setCredentialId(defaultAgent.credentialId || null)
|
|
75
|
+
if (defaultAgent.apiEndpoint) setEndpoint(defaultAgent.apiEndpoint)
|
|
72
76
|
} else {
|
|
73
77
|
setSelectedAgentId(null)
|
|
74
78
|
}
|
|
@@ -168,8 +172,6 @@ export function NewSessionSheet() {
|
|
|
168
172
|
return true
|
|
169
173
|
}
|
|
170
174
|
|
|
171
|
-
const inputClass = "w-full px-4 py-3.5 rounded-[14px] border border-white/[0.08] bg-surface text-text text-[15px] outline-none transition-all duration-200 placeholder:text-text-3/50 focus-glow"
|
|
172
|
-
|
|
173
175
|
return (
|
|
174
176
|
<BottomSheet open={open} onClose={onClose} wide>
|
|
175
177
|
{/* Header */}
|
|
@@ -180,9 +182,7 @@ export function NewSessionSheet() {
|
|
|
180
182
|
|
|
181
183
|
{/* Name */}
|
|
182
184
|
<div className="mb-8">
|
|
183
|
-
<
|
|
184
|
-
Chat Name
|
|
185
|
-
</label>
|
|
185
|
+
<SectionLabel>Chat Name</SectionLabel>
|
|
186
186
|
<input
|
|
187
187
|
type="text"
|
|
188
188
|
value={name}
|
|
@@ -196,20 +196,14 @@ export function NewSessionSheet() {
|
|
|
196
196
|
{/* Agent (optional) */}
|
|
197
197
|
{Object.keys(agents).length > 0 && (
|
|
198
198
|
<div className="mb-8">
|
|
199
|
-
<
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
>
|
|
208
|
-
<option value="">None — manual configuration</option>
|
|
209
|
-
{Object.values(agents).map((p) => (
|
|
210
|
-
<option key={p.id} value={p.id}>{p.name}{p.isOrchestrator ? ' (Orchestrator)' : ''}</option>
|
|
211
|
-
))}
|
|
212
|
-
</select>
|
|
199
|
+
<SectionLabel>Agent <span className="normal-case tracking-normal font-normal text-text-3">(optional)</span></SectionLabel>
|
|
200
|
+
<AgentPickerList
|
|
201
|
+
agents={Object.values(agents).sort((a, b) => a.name.localeCompare(b.name))}
|
|
202
|
+
selected={selectedAgentId || ''}
|
|
203
|
+
onSelect={(id) => handleSelectAgent(id)}
|
|
204
|
+
noneOption={{ label: 'None — manual config', onSelect: () => handleSelectAgent(null) }}
|
|
205
|
+
showOrchBadge={true}
|
|
206
|
+
/>
|
|
213
207
|
</div>
|
|
214
208
|
)}
|
|
215
209
|
|
|
@@ -218,9 +212,7 @@ export function NewSessionSheet() {
|
|
|
218
212
|
<>
|
|
219
213
|
{/* Provider */}
|
|
220
214
|
<div className="mb-8">
|
|
221
|
-
<
|
|
222
|
-
Provider
|
|
223
|
-
</label>
|
|
215
|
+
<SectionLabel>Provider</SectionLabel>
|
|
224
216
|
<div className="grid grid-cols-3 gap-3">
|
|
225
217
|
{providers.map((p) => (
|
|
226
218
|
<button
|
|
@@ -242,9 +234,7 @@ export function NewSessionSheet() {
|
|
|
242
234
|
{/* Ollama Mode Toggle */}
|
|
243
235
|
{provider === 'ollama' && (
|
|
244
236
|
<div className="mb-8">
|
|
245
|
-
<
|
|
246
|
-
Mode
|
|
247
|
-
</label>
|
|
237
|
+
<SectionLabel>Mode</SectionLabel>
|
|
248
238
|
<div className="flex p-1 rounded-[14px] bg-surface border border-white/[0.06]">
|
|
249
239
|
{(['local', 'cloud'] as const).map((mode) => (
|
|
250
240
|
<button
|
|
@@ -267,9 +257,7 @@ export function NewSessionSheet() {
|
|
|
267
257
|
{/* Model */}
|
|
268
258
|
{currentProvider && currentProvider.models.length > 0 && (
|
|
269
259
|
<div className="mb-8">
|
|
270
|
-
<
|
|
271
|
-
Model
|
|
272
|
-
</label>
|
|
260
|
+
<SectionLabel>Model</SectionLabel>
|
|
273
261
|
<ModelCombobox
|
|
274
262
|
providerId={currentProvider.id}
|
|
275
263
|
value={model}
|
|
@@ -284,9 +272,7 @@ export function NewSessionSheet() {
|
|
|
284
272
|
{/* API Key */}
|
|
285
273
|
{(currentProvider?.requiresApiKey || (currentProvider?.optionalApiKey && ollamaMode === 'cloud')) && (
|
|
286
274
|
<div className="mb-8">
|
|
287
|
-
<
|
|
288
|
-
API Key
|
|
289
|
-
</label>
|
|
275
|
+
<SectionLabel>API Key</SectionLabel>
|
|
290
276
|
{providerCredentials.length > 0 && !addingKey ? (
|
|
291
277
|
<select
|
|
292
278
|
value={credentialId || ''}
|
|
@@ -336,7 +322,7 @@ export function NewSessionSheet() {
|
|
|
336
322
|
<button
|
|
337
323
|
onClick={handleAddKey}
|
|
338
324
|
disabled={!newKeyValue.trim()}
|
|
339
|
-
className="flex-1 py-3 rounded-[14px] border-none bg-
|
|
325
|
+
className="flex-1 py-3 rounded-[14px] border-none bg-accent-bright text-white text-[14px] font-600 cursor-pointer disabled:opacity-30 transition-all hover:brightness-110"
|
|
340
326
|
style={{ fontFamily: 'inherit' }}
|
|
341
327
|
>
|
|
342
328
|
Save Key
|
|
@@ -350,9 +336,7 @@ export function NewSessionSheet() {
|
|
|
350
336
|
{/* Endpoint — show for providers that require it (Ollama local, OpenClaw) */}
|
|
351
337
|
{currentProvider?.requiresEndpoint && (provider === 'openclaw' || (provider === 'ollama' && ollamaMode === 'local')) && (
|
|
352
338
|
<div className="mb-8">
|
|
353
|
-
<
|
|
354
|
-
{provider === 'openclaw' ? 'OpenClaw Endpoint' : 'Endpoint'}
|
|
355
|
-
</label>
|
|
339
|
+
<SectionLabel>{provider === 'openclaw' ? 'OpenClaw Endpoint' : 'Endpoint'}</SectionLabel>
|
|
356
340
|
<input
|
|
357
341
|
type="text"
|
|
358
342
|
value={endpoint}
|
|
@@ -433,9 +417,7 @@ export function NewSessionSheet() {
|
|
|
433
417
|
|
|
434
418
|
{/* Project */}
|
|
435
419
|
<div className="mb-10">
|
|
436
|
-
<
|
|
437
|
-
Directory {provider !== 'claude-cli' && <span className="normal-case tracking-normal font-normal text-text-3">(optional)</span>}
|
|
438
|
-
</label>
|
|
420
|
+
<SectionLabel>Directory {provider !== 'claude-cli' && <span className="normal-case tracking-normal font-normal text-text-3">(optional)</span>}</SectionLabel>
|
|
439
421
|
<DirBrowser
|
|
440
422
|
value={selectedDir}
|
|
441
423
|
file={selectedFile}
|
|
@@ -452,26 +434,12 @@ export function NewSessionSheet() {
|
|
|
452
434
|
</div>
|
|
453
435
|
|
|
454
436
|
{/* Actions */}
|
|
455
|
-
<
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
>
|
|
462
|
-
Cancel
|
|
463
|
-
</button>
|
|
464
|
-
<button
|
|
465
|
-
onClick={handleCreate}
|
|
466
|
-
disabled={!canCreate()}
|
|
467
|
-
className="flex-1 py-3.5 rounded-[14px] border-none bg-[#6366F1] text-white text-[15px] font-600 cursor-pointer
|
|
468
|
-
active:scale-[0.97] disabled:opacity-30 transition-all duration-200
|
|
469
|
-
shadow-[0_4px_20px_rgba(99,102,241,0.25)] hover:brightness-110"
|
|
470
|
-
style={{ fontFamily: 'inherit' }}
|
|
471
|
-
>
|
|
472
|
-
Create Chat
|
|
473
|
-
</button>
|
|
474
|
-
</div>
|
|
437
|
+
<SheetFooter
|
|
438
|
+
onCancel={onClose}
|
|
439
|
+
onSave={handleCreate}
|
|
440
|
+
saveLabel="Create Chat"
|
|
441
|
+
saveDisabled={!canCreate()}
|
|
442
|
+
/>
|
|
475
443
|
</BottomSheet>
|
|
476
444
|
)
|
|
477
445
|
}
|
|
@@ -5,6 +5,7 @@ import { api } from '@/lib/api-client'
|
|
|
5
5
|
import { useAppStore } from '@/stores/use-app-store'
|
|
6
6
|
import { useChatStore } from '@/stores/use-chat-store'
|
|
7
7
|
import { ConnectorPlatformBadge, getSessionConnector } from '@/components/shared/connector-platform-icon'
|
|
8
|
+
import { AgentAvatar } from '@/components/agents/agent-avatar'
|
|
8
9
|
|
|
9
10
|
function timeAgo(ts: number): string {
|
|
10
11
|
if (!ts) return ''
|
|
@@ -38,6 +39,9 @@ export function SessionCard({ session, active, onClick }: Props) {
|
|
|
38
39
|
const agents = useAppStore((s) => s.agents)
|
|
39
40
|
const connectors = useAppStore((s) => s.connectors)
|
|
40
41
|
const streamingSessionId = useChatStore((s) => s.streamingSessionId)
|
|
42
|
+
const streamPhase = useChatStore((s) => s.streamPhase)
|
|
43
|
+
const streamToolName = useChatStore((s) => s.streamToolName)
|
|
44
|
+
const lastReadTimestamps = useAppStore((s) => s.lastReadTimestamps)
|
|
41
45
|
const isTyping = streamingSessionId === session.id
|
|
42
46
|
|
|
43
47
|
const handleDelete = async (e: React.MouseEvent) => {
|
|
@@ -56,14 +60,16 @@ export function SessionCard({ session, active, onClick }: Props) {
|
|
|
56
60
|
const agent = session.agentId ? agents[session.agentId] : null
|
|
57
61
|
const connector = getSessionConnector(session, connectors)
|
|
58
62
|
const loopIsOngoing = appSettings.loopMode === 'ongoing'
|
|
63
|
+
const explicitOptIn = session.heartbeatEnabled === true || agent?.heartbeatEnabled === true
|
|
59
64
|
const intervalRaw = session.heartbeatIntervalSec ?? agent?.heartbeatIntervalSec ?? appSettings.heartbeatIntervalSec ?? 120
|
|
60
65
|
const intervalNum = typeof intervalRaw === 'number' ? intervalRaw : Number.parseInt(String(intervalRaw), 10)
|
|
61
66
|
const intervalEnabled = Number.isFinite(intervalNum) ? intervalNum > 0 : true
|
|
62
67
|
const heartbeatEnabled =
|
|
63
|
-
loopIsOngoing
|
|
68
|
+
(loopIsOngoing || explicitOptIn)
|
|
64
69
|
&& (session.tools?.length ?? 0) > 0
|
|
65
70
|
&& intervalEnabled
|
|
66
|
-
&&
|
|
71
|
+
&& session.heartbeatEnabled !== false
|
|
72
|
+
&& agent?.heartbeatEnabled !== false
|
|
67
73
|
|
|
68
74
|
return (
|
|
69
75
|
<div
|
|
@@ -78,17 +84,13 @@ export function SessionCard({ session, active, onClick }: Props) {
|
|
|
78
84
|
<div className="absolute left-0 top-3.5 bottom-3.5 w-[2.5px] rounded-full bg-accent-bright" />
|
|
79
85
|
)}
|
|
80
86
|
<div className="flex items-center gap-2.5">
|
|
81
|
-
{
|
|
82
|
-
<
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
title="Heartbeat enabled"
|
|
89
|
-
>
|
|
90
|
-
<span className="w-[4px] h-[4px] rounded-full bg-emerald-400" />
|
|
91
|
-
</span>
|
|
87
|
+
{agent && (
|
|
88
|
+
<div className="relative shrink-0">
|
|
89
|
+
<AgentAvatar seed={agent.avatarSeed} name={agent.name} size={24} />
|
|
90
|
+
{(heartbeatEnabled || session.active) && (
|
|
91
|
+
<span className="absolute -bottom-0.5 -right-0.5 w-2 h-2 rounded-full bg-emerald-400 ring-2 ring-[#0f0f1a]" />
|
|
92
|
+
)}
|
|
93
|
+
</div>
|
|
92
94
|
)}
|
|
93
95
|
{connector && (
|
|
94
96
|
<ConnectorPlatformBadge
|
|
@@ -100,6 +102,20 @@ export function SessionCard({ session, active, onClick }: Props) {
|
|
|
100
102
|
/>
|
|
101
103
|
)}
|
|
102
104
|
<span className="font-display text-[14px] font-600 truncate flex-1 tracking-[-0.01em]">{session.name}</span>
|
|
105
|
+
{session.mainLoopState?.status && session.mainLoopState.status !== 'idle' && (
|
|
106
|
+
<span className={`shrink-0 flex items-center gap-1 text-[9px] font-600 uppercase tracking-wider px-1.5 py-0.5 rounded-[5px] ${
|
|
107
|
+
session.mainLoopState.status === 'progress' ? 'text-blue-400/90 bg-blue-400/[0.08]'
|
|
108
|
+
: session.mainLoopState.status === 'blocked' ? 'text-amber-400/90 bg-amber-400/[0.08]'
|
|
109
|
+
: 'text-emerald-400/90 bg-emerald-400/[0.08]'
|
|
110
|
+
}`}>
|
|
111
|
+
<span className={`w-[5px] h-[5px] rounded-full ${
|
|
112
|
+
session.mainLoopState.status === 'progress' ? 'bg-blue-400'
|
|
113
|
+
: session.mainLoopState.status === 'blocked' ? 'bg-amber-400'
|
|
114
|
+
: 'bg-emerald-400'
|
|
115
|
+
}`} />
|
|
116
|
+
{session.mainLoopState.status}
|
|
117
|
+
</span>
|
|
118
|
+
)}
|
|
103
119
|
{session.sessionType === 'orchestrated' && (
|
|
104
120
|
<span className="shrink-0 text-[10px] font-600 uppercase tracking-wider text-amber-400/80 bg-amber-400/[0.08] px-2 py-0.5 rounded-[6px]">
|
|
105
121
|
AI
|
|
@@ -110,6 +126,17 @@ export function SessionCard({ session, active, onClick }: Props) {
|
|
|
110
126
|
{providerLabel}
|
|
111
127
|
</span>
|
|
112
128
|
)}
|
|
129
|
+
{(() => {
|
|
130
|
+
const lastRead = lastReadTimestamps[session.id] || 0
|
|
131
|
+
const unread = (session.messages || []).filter(
|
|
132
|
+
(m) => m.role === 'assistant' && (m.time || 0) > lastRead,
|
|
133
|
+
).length
|
|
134
|
+
return unread > 0 ? (
|
|
135
|
+
<span className="shrink-0 min-w-[18px] h-[18px] flex items-center justify-center rounded-full bg-accent-bright text-white text-[10px] font-600 px-1">
|
|
136
|
+
{unread > 99 ? '99+' : unread}
|
|
137
|
+
</span>
|
|
138
|
+
) : null
|
|
139
|
+
})()}
|
|
113
140
|
<span className="text-[11px] text-text-3/70 shrink-0 tabular-nums font-mono">
|
|
114
141
|
{timeAgo(session.lastActiveAt)}
|
|
115
142
|
</span>
|
|
@@ -134,7 +161,11 @@ export function SessionCard({ session, active, onClick }: Props) {
|
|
|
134
161
|
<span className="w-1 h-1 rounded-full bg-accent-bright/70 animate-bounce [animation-delay:150ms]" />
|
|
135
162
|
<span className="w-1 h-1 rounded-full bg-accent-bright/70 animate-bounce [animation-delay:300ms]" />
|
|
136
163
|
</span>
|
|
137
|
-
|
|
164
|
+
{streamPhase === 'tool' && streamToolName
|
|
165
|
+
? `Using ${streamToolName}...`
|
|
166
|
+
: streamPhase === 'responding'
|
|
167
|
+
? 'Responding...'
|
|
168
|
+
: 'Thinking...'}
|
|
138
169
|
</div>
|
|
139
170
|
) : (
|
|
140
171
|
<div className="text-[13px] text-text-2/50 truncate mt-1 leading-relaxed">{preview}</div>
|
|
@@ -5,6 +5,9 @@ import { useAppStore } from '@/stores/use-app-store'
|
|
|
5
5
|
import { useChatStore } from '@/stores/use-chat-store'
|
|
6
6
|
import { SessionCard } from './session-card'
|
|
7
7
|
import { fetchMessages } from '@/lib/sessions'
|
|
8
|
+
import { toast } from 'sonner'
|
|
9
|
+
import { Skeleton } from '@/components/shared/skeleton'
|
|
10
|
+
import { EmptyState } from '@/components/shared/empty-state'
|
|
8
11
|
|
|
9
12
|
interface Props {
|
|
10
13
|
inSidebar?: boolean
|
|
@@ -24,10 +27,16 @@ export function SessionList({ inSidebar, onSelect }: Props) {
|
|
|
24
27
|
const setNewSessionOpen = useAppStore((s) => s.setNewSessionOpen)
|
|
25
28
|
const clearSessions = useAppStore((s) => s.clearSessions)
|
|
26
29
|
const togglePinSession = useAppStore((s) => s.togglePinSession)
|
|
30
|
+
const markChatRead = useAppStore((s) => s.markChatRead)
|
|
27
31
|
const setMessages = useChatStore((s) => s.setMessages)
|
|
28
32
|
const [search, setSearch] = useState('')
|
|
29
33
|
const [typeFilter, setTypeFilter] = useState<SessionFilter>('all')
|
|
30
34
|
const [sortMode, setSortMode] = useState<SortMode>('lastActive')
|
|
35
|
+
const [loaded, setLoaded] = useState(Object.keys(sessions).length > 0)
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (Object.keys(sessions).length > 0 && !loaded) setLoaded(true)
|
|
39
|
+
}, [sessions, loaded])
|
|
31
40
|
|
|
32
41
|
useEffect(() => {
|
|
33
42
|
void loadConnectors()
|
|
@@ -67,6 +76,7 @@ export function SessionList({ inSidebar, onSelect }: Props) {
|
|
|
67
76
|
|
|
68
77
|
const handleSelect = async (id: string) => {
|
|
69
78
|
setCurrentSession(id)
|
|
79
|
+
markChatRead(id)
|
|
70
80
|
if (typeof window !== 'undefined') {
|
|
71
81
|
window.dispatchEvent(new CustomEvent('swarmclaw:scroll-bottom'))
|
|
72
82
|
}
|
|
@@ -82,27 +92,33 @@ export function SessionList({ inSidebar, onSelect }: Props) {
|
|
|
82
92
|
|
|
83
93
|
// Truly empty — no sessions at all for this user
|
|
84
94
|
if (!allUserSessions.length) {
|
|
95
|
+
// Show skeleton cards while data is loading
|
|
96
|
+
if (!loaded) {
|
|
97
|
+
return (
|
|
98
|
+
<div className="flex-1 flex flex-col gap-1 px-2 pt-4">
|
|
99
|
+
{Array.from({ length: 3 }).map((_, i) => (
|
|
100
|
+
<div key={i} className="py-3 px-4 rounded-[14px]">
|
|
101
|
+
<div className="flex items-center gap-2.5">
|
|
102
|
+
<Skeleton className="rounded-full" width={28} height={28} />
|
|
103
|
+
<Skeleton className="rounded-[6px]" width={140} height={14} />
|
|
104
|
+
</div>
|
|
105
|
+
<Skeleton className="rounded-[6px] mt-2" width="70%" height={12} />
|
|
106
|
+
</div>
|
|
107
|
+
))}
|
|
108
|
+
</div>
|
|
109
|
+
)
|
|
110
|
+
}
|
|
85
111
|
return (
|
|
86
|
-
<
|
|
87
|
-
|
|
112
|
+
<EmptyState
|
|
113
|
+
icon={
|
|
88
114
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" className="text-accent-bright">
|
|
89
115
|
<path d="M12 2L14.5 9.5L22 12L14.5 14.5L12 22L9.5 14.5L2 12L9.5 9.5L12 2Z" fill="currentColor" />
|
|
90
116
|
</svg>
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
{!inSidebar
|
|
95
|
-
|
|
96
|
-
onClick={() => setNewSessionOpen(true)}
|
|
97
|
-
className="mt-3 px-8 py-3 rounded-[14px] border-none bg-[#6366F1] text-white
|
|
98
|
-
text-[14px] font-600 cursor-pointer active:scale-95 transition-all duration-200
|
|
99
|
-
shadow-[0_4px_16px_rgba(99,102,241,0.2)]"
|
|
100
|
-
style={{ fontFamily: 'inherit' }}
|
|
101
|
-
>
|
|
102
|
-
+ New Chat
|
|
103
|
-
</button>
|
|
104
|
-
)}
|
|
105
|
-
</div>
|
|
117
|
+
}
|
|
118
|
+
title="No chats yet"
|
|
119
|
+
subtitle="Create one to start chatting"
|
|
120
|
+
action={!inSidebar ? { label: '+ New Chat', onClick: () => setNewSessionOpen(true) } : undefined}
|
|
121
|
+
/>
|
|
106
122
|
)
|
|
107
123
|
}
|
|
108
124
|
|
|
@@ -126,6 +142,7 @@ export function SessionList({ inSidebar, onSelect }: Props) {
|
|
|
126
142
|
onClick={async () => {
|
|
127
143
|
if (!window.confirm(`Delete ${filtered.length} chat${filtered.length === 1 ? '' : 's'}?`)) return
|
|
128
144
|
await clearSessions(filtered.map((s) => s.id))
|
|
145
|
+
toast.success(`${filtered.length} chat${filtered.length === 1 ? '' : 's'} deleted`)
|
|
129
146
|
}}
|
|
130
147
|
className="ml-auto p-1.5 rounded-[8px] text-text-3/70 hover:text-red-400 hover:bg-red-400/[0.06]
|
|
131
148
|
cursor-pointer transition-all bg-transparent border-none"
|
|
@@ -175,7 +192,7 @@ export function SessionList({ inSidebar, onSelect }: Props) {
|
|
|
175
192
|
onClick={() => handleSelect(s.id)}
|
|
176
193
|
/>
|
|
177
194
|
<button
|
|
178
|
-
onClick={(e) => { e.stopPropagation(); togglePinSession(s.id) }}
|
|
195
|
+
onClick={(e) => { e.stopPropagation(); togglePinSession(s.id); toast.success(s.pinned ? 'Chat unpinned' : 'Chat pinned') }}
|
|
179
196
|
aria-label={s.pinned ? 'Unpin chat' : 'Pin chat'}
|
|
180
197
|
className={`absolute top-2 right-2 p-1 rounded-[6px] border-none cursor-pointer transition-all
|
|
181
198
|
${s.pinned
|