@swarmclawai/swarmclaw 0.3.0 → 0.4.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 +20 -11
- package/bin/server-cmd.js +14 -7
- package/bin/swarmclaw.js +3 -1
- package/bin/update-cmd.js +120 -0
- package/next.config.ts +2 -0
- package/package.json +3 -1
- package/src/app/api/agents/[id]/route.ts +3 -0
- package/src/app/api/agents/[id]/thread/route.ts +2 -1
- package/src/app/api/agents/route.ts +5 -1
- package/src/app/api/auth/route.ts +3 -1
- package/src/app/api/claude-skills/route.ts +3 -1
- package/src/app/api/connectors/[id]/route.ts +4 -0
- package/src/app/api/connectors/route.ts +6 -1
- package/src/app/api/credentials/route.ts +3 -1
- package/src/app/api/daemon/route.ts +6 -1
- package/src/app/api/ip/route.ts +3 -1
- package/src/app/api/mcp-servers/route.ts +3 -1
- package/src/app/api/orchestrator/graph/route.ts +25 -0
- package/src/app/api/plugins/marketplace/route.ts +3 -1
- package/src/app/api/plugins/route.ts +3 -1
- package/src/app/api/providers/[id]/route.ts +3 -0
- package/src/app/api/providers/configs/route.ts +3 -1
- package/src/app/api/providers/route.ts +5 -1
- package/src/app/api/schedules/[id]/route.ts +3 -0
- package/src/app/api/schedules/route.ts +6 -1
- package/src/app/api/secrets/route.ts +3 -1
- package/src/app/api/sessions/[id]/chat/route.ts +5 -2
- package/src/app/api/sessions/route.ts +9 -2
- package/src/app/api/settings/route.ts +3 -1
- package/src/app/api/setup/doctor/route.ts +1 -0
- package/src/app/api/setup/openclaw-device/route.ts +3 -1
- package/src/app/api/skills/route.ts +3 -1
- package/src/app/api/tasks/[id]/approve/route.ts +73 -0
- package/src/app/api/tasks/[id]/route.ts +3 -0
- package/src/app/api/tasks/route.ts +3 -0
- package/src/app/api/usage/route.ts +3 -1
- package/src/app/api/version/route.ts +3 -1
- package/src/app/api/webhooks/[id]/route.ts +2 -1
- package/src/app/api/webhooks/route.ts +3 -1
- package/src/app/icon.svg +58 -0
- package/src/app/page.tsx +8 -2
- package/src/cli/index.js +1 -9
- package/src/cli/index.ts +51 -1
- package/src/cli/spec.js +0 -8
- package/src/components/agents/agent-card.tsx +1 -1
- package/src/components/agents/agent-sheet.tsx +63 -80
- package/src/components/chat/chat-area.tsx +44 -30
- package/src/components/chat/chat-tool-toggles.tsx +12 -53
- package/src/components/chat/message-bubble.tsx +110 -42
- package/src/components/chat/tool-call-bubble.tsx +41 -3
- package/src/components/chat/tool-request-banner.tsx +1 -9
- package/src/components/connectors/connector-list.tsx +3 -8
- package/src/components/connectors/connector-sheet.tsx +24 -29
- package/src/components/input/chat-input.tsx +72 -56
- package/src/components/knowledge/knowledge-list.tsx +27 -31
- package/src/components/layout/app-layout.tsx +92 -71
- package/src/components/layout/daemon-indicator.tsx +3 -5
- package/src/components/logs/log-list.tsx +5 -9
- package/src/components/mcp-servers/mcp-server-list.tsx +24 -2
- package/src/components/memory/memory-detail.tsx +1 -1
- package/src/components/plugins/plugin-list.tsx +227 -27
- package/src/components/providers/provider-list.tsx +46 -13
- package/src/components/providers/provider-sheet.tsx +0 -45
- package/src/components/runs/run-list.tsx +6 -15
- package/src/components/schedules/schedule-card.tsx +54 -4
- package/src/components/schedules/schedule-list.tsx +6 -3
- package/src/components/schedules/schedule-sheet.tsx +0 -47
- package/src/components/secrets/secrets-list.tsx +20 -2
- package/src/components/sessions/new-session-sheet.tsx +8 -9
- package/src/components/shared/connector-platform-icon.tsx +22 -20
- package/src/components/shared/model-combobox.tsx +148 -0
- package/src/components/shared/settings/section-heartbeat.tsx +7 -39
- package/src/components/shared/settings/section-orchestrator.tsx +8 -9
- package/src/components/skills/skill-list.tsx +260 -34
- package/src/components/skills/skill-sheet.tsx +0 -45
- package/src/components/tasks/task-board.tsx +3 -6
- package/src/components/tasks/task-card.tsx +43 -1
- package/src/components/tasks/task-list.tsx +3 -5
- package/src/components/tasks/task-sheet.tsx +0 -44
- package/src/components/usage/usage-list.tsx +12 -4
- package/src/hooks/use-ws.ts +66 -0
- package/src/instrumentation.ts +2 -0
- package/src/lib/chat.ts +14 -2
- package/src/lib/providers/anthropic.ts +1 -1
- package/src/lib/providers/index.ts +2 -0
- package/src/lib/providers/ollama.ts +1 -1
- package/src/lib/providers/openai.ts +33 -12
- package/src/lib/server/chat-execution.ts +19 -4
- package/src/lib/server/connectors/manager.ts +9 -3
- package/src/lib/server/context-manager.ts +1 -1
- package/src/lib/server/daemon-state.ts +3 -0
- package/src/lib/server/data-dir.ts +1 -0
- package/src/lib/server/heartbeat-service.ts +67 -3
- package/src/lib/server/langgraph-checkpoint.ts +274 -0
- package/src/lib/server/main-agent-loop.ts +61 -2
- package/src/lib/server/orchestrator-lg.ts +394 -13
- package/src/lib/server/orchestrator.ts +25 -5
- package/src/lib/server/queue.ts +17 -3
- package/src/lib/server/session-run-manager.ts +6 -1
- package/src/lib/server/session-tools/delegate.ts +2 -2
- package/src/lib/server/session-tools/index.ts +2 -0
- package/src/lib/server/session-tools/sandbox.ts +164 -0
- package/src/lib/server/storage-mcp.test.ts +25 -2
- package/src/lib/server/storage.ts +24 -7
- package/src/lib/server/stream-agent-chat.ts +77 -22
- package/src/lib/server/task-validation.test.ts +23 -0
- package/src/lib/server/task-validation.ts +5 -3
- package/src/lib/server/ws-hub.ts +85 -0
- package/src/lib/tool-definitions.ts +42 -0
- package/src/lib/upload.ts +7 -1
- package/src/lib/ws-client.ts +124 -0
- package/src/stores/use-chat-store.ts +33 -13
- package/src/types/index.ts +8 -1
- package/src/app/api/agents/generate/route.ts +0 -42
- package/src/app/api/generate/info/route.ts +0 -12
- package/src/app/api/generate/route.ts +0 -106
- package/src/app/favicon.ico +0 -0
- package/src/components/shared/ai-gen-block.tsx +0 -77
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useEffect } from 'react'
|
|
3
|
+
import { useEffect, useState, useCallback } from 'react'
|
|
4
4
|
import { useAppStore } from '@/stores/use-app-store'
|
|
5
|
+
import { api } from '@/lib/api-client'
|
|
6
|
+
import type { MarketplacePlugin } from '@/types'
|
|
5
7
|
|
|
6
8
|
export function PluginList({ inSidebar }: { inSidebar?: boolean }) {
|
|
7
9
|
const plugins = useAppStore((s) => s.plugins)
|
|
@@ -9,10 +11,31 @@ export function PluginList({ inSidebar }: { inSidebar?: boolean }) {
|
|
|
9
11
|
const setPluginSheetOpen = useAppStore((s) => s.setPluginSheetOpen)
|
|
10
12
|
const setEditingPluginFilename = useAppStore((s) => s.setEditingPluginFilename)
|
|
11
13
|
|
|
14
|
+
const [tab, setTab] = useState<'installed' | 'marketplace'>('installed')
|
|
15
|
+
const [marketplace, setMarketplace] = useState<MarketplacePlugin[]>([])
|
|
16
|
+
const [mpLoading, setMpLoading] = useState(false)
|
|
17
|
+
const [installing, setInstalling] = useState<string | null>(null)
|
|
18
|
+
const [search, setSearch] = useState('')
|
|
19
|
+
const [activeTag, setActiveTag] = useState<string | null>(null)
|
|
20
|
+
const [sort, setSort] = useState<'name' | 'downloads'>('downloads')
|
|
21
|
+
|
|
12
22
|
useEffect(() => {
|
|
13
23
|
loadPlugins()
|
|
14
24
|
}, [])
|
|
15
25
|
|
|
26
|
+
const loadMarketplace = useCallback(async () => {
|
|
27
|
+
setMpLoading(true)
|
|
28
|
+
try {
|
|
29
|
+
const data = await api<MarketplacePlugin[]>('GET', '/plugins/marketplace')
|
|
30
|
+
if (Array.isArray(data)) setMarketplace(data)
|
|
31
|
+
} catch { /* ignore */ }
|
|
32
|
+
setMpLoading(false)
|
|
33
|
+
}, [])
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (!inSidebar && tab === 'marketplace') loadMarketplace()
|
|
37
|
+
}, [tab, inSidebar, loadMarketplace])
|
|
38
|
+
|
|
16
39
|
const pluginList = Object.values(plugins)
|
|
17
40
|
|
|
18
41
|
const handleEdit = (filename: string) => {
|
|
@@ -20,41 +43,218 @@ export function PluginList({ inSidebar }: { inSidebar?: boolean }) {
|
|
|
20
43
|
setPluginSheetOpen(true)
|
|
21
44
|
}
|
|
22
45
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
46
|
+
const handleToggle = async (e: React.MouseEvent, filename: string, enabled: boolean) => {
|
|
47
|
+
e.stopPropagation()
|
|
48
|
+
await api('POST', '/plugins', { filename, enabled: !enabled })
|
|
49
|
+
loadPlugins()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const handleDelete = async (e: React.MouseEvent, filename: string) => {
|
|
53
|
+
e.stopPropagation()
|
|
54
|
+
await api('DELETE', `/plugins/${encodeURIComponent(filename)}`)
|
|
55
|
+
loadPlugins()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const installFromMarketplace = async (p: MarketplacePlugin) => {
|
|
59
|
+
setInstalling(p.id)
|
|
60
|
+
try {
|
|
61
|
+
await api('POST', '/plugins/install', { url: p.url, filename: `${p.id}.js` })
|
|
62
|
+
await loadPlugins()
|
|
63
|
+
} catch { /* ignore */ }
|
|
64
|
+
setInstalling(null)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const installedFilenames = new Set(Object.keys(plugins))
|
|
68
|
+
|
|
69
|
+
const tabClass = (t: string) =>
|
|
70
|
+
`py-1.5 px-3.5 rounded-[8px] text-[12px] font-600 cursor-pointer transition-all border
|
|
71
|
+
${tab === t
|
|
72
|
+
? 'bg-accent-soft border-accent-bright/25 text-accent-bright'
|
|
73
|
+
: 'bg-transparent border-transparent text-text-3 hover:text-text-2'}`
|
|
74
|
+
|
|
75
|
+
// Marketplace tab content (full-width only)
|
|
76
|
+
const renderMarketplace = () => {
|
|
77
|
+
if (mpLoading) return <p className="text-[12px] text-text-3/70 py-8 text-center">Loading marketplace...</p>
|
|
78
|
+
if (marketplace.length === 0) return <p className="text-[12px] text-text-3/70 py-8 text-center">No plugins available</p>
|
|
79
|
+
|
|
80
|
+
const allTags = Array.from(new Set(marketplace.flatMap((p) => p.tags))).sort()
|
|
81
|
+
const q = search.toLowerCase()
|
|
82
|
+
const filtered = marketplace
|
|
83
|
+
.filter((p) => {
|
|
84
|
+
if (q && !p.name.toLowerCase().includes(q) && !p.description.toLowerCase().includes(q) && !p.tags.some((t) => t.toLowerCase().includes(q))) return false
|
|
85
|
+
if (activeTag && !p.tags.includes(activeTag)) return false
|
|
86
|
+
return true
|
|
87
|
+
})
|
|
88
|
+
.sort((a, b) => sort === 'downloads' ? b.downloads - a.downloads : a.name.localeCompare(b.name))
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<div className="space-y-3">
|
|
92
|
+
<input
|
|
93
|
+
value={search}
|
|
94
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
95
|
+
placeholder="Search plugins..."
|
|
96
|
+
className="w-full px-3 py-2.5 rounded-[10px] bg-surface border border-white/[0.06] text-[12px] text-text placeholder:text-text-3/50 outline-none focus:border-accent-bright/30"
|
|
97
|
+
style={{ fontFamily: 'inherit' }}
|
|
98
|
+
/>
|
|
99
|
+
<div className="flex items-center gap-1.5 flex-wrap">
|
|
28
100
|
<button
|
|
29
|
-
onClick={() =>
|
|
30
|
-
className=
|
|
31
|
-
|
|
101
|
+
onClick={() => setActiveTag(null)}
|
|
102
|
+
className={`px-2 py-1 rounded-[6px] text-[10px] font-600 cursor-pointer transition-all border-none ${
|
|
103
|
+
!activeTag ? 'bg-accent-soft text-accent-bright' : 'bg-white/[0.03] text-text-3/60 hover:text-text-3'
|
|
104
|
+
}`}
|
|
32
105
|
>
|
|
33
|
-
|
|
106
|
+
All
|
|
34
107
|
</button>
|
|
35
|
-
|
|
36
|
-
) : (
|
|
37
|
-
<div className="space-y-2">
|
|
38
|
-
{pluginList.map((plugin) => (
|
|
108
|
+
{allTags.map((t) => (
|
|
39
109
|
<button
|
|
40
|
-
key={
|
|
41
|
-
onClick={() =>
|
|
42
|
-
className=
|
|
110
|
+
key={t}
|
|
111
|
+
onClick={() => setActiveTag(activeTag === t ? null : t)}
|
|
112
|
+
className={`px-2 py-1 rounded-[6px] text-[10px] font-600 cursor-pointer transition-all border-none ${
|
|
113
|
+
activeTag === t ? 'bg-accent-soft text-accent-bright' : 'bg-white/[0.03] text-text-3/60 hover:text-text-3'
|
|
114
|
+
}`}
|
|
43
115
|
>
|
|
44
|
-
|
|
45
|
-
<span className="font-display text-[14px] font-600 text-text truncate">{plugin.name}</span>
|
|
46
|
-
<span className={`text-[10px] font-600 px-1.5 py-0.5 rounded-full shrink-0 ml-2 ${plugin.enabled ? 'text-emerald-400 bg-emerald-400/10' : 'text-text-3/50 bg-white/[0.04]'}`}>
|
|
47
|
-
{plugin.enabled ? 'Enabled' : 'Disabled'}
|
|
48
|
-
</span>
|
|
49
|
-
</div>
|
|
50
|
-
<div className="text-[11px] font-mono text-text-3/50 mb-1">{plugin.filename}</div>
|
|
51
|
-
{plugin.description && (
|
|
52
|
-
<p className="text-[12px] text-text-3/60 line-clamp-2">{plugin.description}</p>
|
|
53
|
-
)}
|
|
116
|
+
{t}
|
|
54
117
|
</button>
|
|
55
118
|
))}
|
|
119
|
+
<div className="flex-1" />
|
|
120
|
+
<select
|
|
121
|
+
value={sort}
|
|
122
|
+
onChange={(e) => setSort(e.target.value as 'name' | 'downloads')}
|
|
123
|
+
className="px-2 py-1 rounded-[6px] bg-surface border border-white/[0.06] text-[10px] text-text-3 outline-none cursor-pointer appearance-none"
|
|
124
|
+
style={{ fontFamily: 'inherit' }}
|
|
125
|
+
>
|
|
126
|
+
<option value="downloads">Popular</option>
|
|
127
|
+
<option value="name">A-Z</option>
|
|
128
|
+
</select>
|
|
129
|
+
</div>
|
|
130
|
+
{filtered.length === 0 ? (
|
|
131
|
+
<p className="text-[12px] text-text-3/50 text-center py-4">No plugins match your search</p>
|
|
132
|
+
) : (
|
|
133
|
+
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3">
|
|
134
|
+
{filtered.map((p) => {
|
|
135
|
+
const isInstalled = installedFilenames.has(`${p.id}.js`)
|
|
136
|
+
return (
|
|
137
|
+
<div key={p.id} className="py-3.5 px-4 rounded-[14px] bg-surface border border-white/[0.06]">
|
|
138
|
+
<div className="flex items-start gap-3">
|
|
139
|
+
<div className="flex-1 min-w-0">
|
|
140
|
+
<div className="flex items-center gap-2">
|
|
141
|
+
<span className="text-[14px] font-600 text-text">{p.name}</span>
|
|
142
|
+
<span className="text-[10px] font-mono text-text-3/70">v{p.version}</span>
|
|
143
|
+
{p.openclaw && <span className="text-[9px] font-600 text-emerald-400 bg-emerald-400/10 px-1.5 py-0.5 rounded-full">OpenClaw</span>}
|
|
144
|
+
</div>
|
|
145
|
+
<div className="text-[11px] text-text-3/60 mt-1 line-clamp-2">{p.description}</div>
|
|
146
|
+
<div className="flex items-center gap-2 mt-2">
|
|
147
|
+
<span className="text-[10px] text-text-3/70">by {p.author}</span>
|
|
148
|
+
<span className="text-[10px] text-text-3/50">·</span>
|
|
149
|
+
{p.tags.slice(0, 3).map((t) => (
|
|
150
|
+
<button
|
|
151
|
+
key={t}
|
|
152
|
+
onClick={() => setActiveTag(activeTag === t ? null : t)}
|
|
153
|
+
className={`text-[9px] font-600 px-1.5 py-0.5 rounded-full cursor-pointer transition-all border-none ${
|
|
154
|
+
activeTag === t ? 'text-accent-bright bg-accent-soft' : 'text-text-3/50 bg-white/[0.04] hover:text-text-3'
|
|
155
|
+
}`}
|
|
156
|
+
>
|
|
157
|
+
{t}
|
|
158
|
+
</button>
|
|
159
|
+
))}
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
<button
|
|
163
|
+
onClick={() => !isInstalled && installFromMarketplace(p)}
|
|
164
|
+
disabled={isInstalled || installing === p.id}
|
|
165
|
+
className={`shrink-0 py-2 px-4 rounded-[10px] text-[12px] font-600 transition-all cursor-pointer
|
|
166
|
+
${isInstalled
|
|
167
|
+
? 'bg-white/[0.04] text-text-3/70 cursor-default'
|
|
168
|
+
: installing === p.id
|
|
169
|
+
? 'bg-accent-soft text-accent-bright animate-pulse'
|
|
170
|
+
: 'bg-accent-soft text-accent-bright hover:bg-accent-soft/80 border border-accent-bright/20'}`}
|
|
171
|
+
style={{ fontFamily: 'inherit' }}
|
|
172
|
+
>
|
|
173
|
+
{isInstalled ? 'Installed' : installing === p.id ? 'Installing...' : 'Install'}
|
|
174
|
+
</button>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
)
|
|
178
|
+
})}
|
|
179
|
+
</div>
|
|
180
|
+
)}
|
|
181
|
+
</div>
|
|
182
|
+
)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return (
|
|
186
|
+
<div className={`flex-1 overflow-y-auto ${inSidebar ? 'px-3 pb-4' : 'px-5 pb-6'}`}>
|
|
187
|
+
{/* Tabs — full-width only */}
|
|
188
|
+
{!inSidebar && (
|
|
189
|
+
<div className="flex gap-1 mb-4">
|
|
190
|
+
<button onClick={() => setTab('installed')} className={tabClass('installed')} style={{ fontFamily: 'inherit' }}>
|
|
191
|
+
Installed
|
|
192
|
+
</button>
|
|
193
|
+
<button onClick={() => setTab('marketplace')} className={tabClass('marketplace')} style={{ fontFamily: 'inherit' }}>
|
|
194
|
+
Marketplace
|
|
195
|
+
</button>
|
|
56
196
|
</div>
|
|
57
197
|
)}
|
|
198
|
+
|
|
199
|
+
{(!inSidebar && tab === 'marketplace') ? renderMarketplace() : (
|
|
200
|
+
pluginList.length === 0 ? (
|
|
201
|
+
<div className="text-center py-12">
|
|
202
|
+
<p className="text-[13px] text-text-3/60">No plugins installed</p>
|
|
203
|
+
<button
|
|
204
|
+
onClick={() => { setEditingPluginFilename(null); setPluginSheetOpen(true) }}
|
|
205
|
+
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"
|
|
206
|
+
style={{ fontFamily: 'inherit' }}
|
|
207
|
+
>
|
|
208
|
+
+ Add Plugin
|
|
209
|
+
</button>
|
|
210
|
+
</div>
|
|
211
|
+
) : (
|
|
212
|
+
<div className={inSidebar ? 'space-y-2' : 'grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3'}>
|
|
213
|
+
{pluginList.map((plugin) => (
|
|
214
|
+
<button
|
|
215
|
+
key={plugin.filename}
|
|
216
|
+
onClick={() => handleEdit(plugin.filename)}
|
|
217
|
+
className="w-full text-left p-4 rounded-[14px] border border-white/[0.06] bg-surface hover:bg-surface-2 transition-all cursor-pointer"
|
|
218
|
+
>
|
|
219
|
+
<div className="flex items-center justify-between mb-1">
|
|
220
|
+
<span className="font-display text-[14px] font-600 text-text truncate">{plugin.name}</span>
|
|
221
|
+
<div className="flex items-center gap-2 shrink-0 ml-2">
|
|
222
|
+
{!inSidebar ? (
|
|
223
|
+
<>
|
|
224
|
+
<div
|
|
225
|
+
onClick={(e) => handleToggle(e, plugin.filename, plugin.enabled)}
|
|
226
|
+
className={`w-9 h-5 rounded-full transition-all relative cursor-pointer shrink-0
|
|
227
|
+
${plugin.enabled ? 'bg-[#6366F1]' : 'bg-white/[0.08]'}`}
|
|
228
|
+
>
|
|
229
|
+
<div className={`absolute top-0.5 w-4 h-4 rounded-full bg-white transition-all
|
|
230
|
+
${plugin.enabled ? 'left-[18px]' : 'left-0.5'}`} />
|
|
231
|
+
</div>
|
|
232
|
+
<button
|
|
233
|
+
onClick={(e) => handleDelete(e, plugin.filename)}
|
|
234
|
+
className="text-text-3/40 hover:text-red-400 transition-colors p-0.5"
|
|
235
|
+
title="Delete"
|
|
236
|
+
>
|
|
237
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
238
|
+
<path d="M3 6h18M19 6v14a2 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" />
|
|
239
|
+
</svg>
|
|
240
|
+
</button>
|
|
241
|
+
</>
|
|
242
|
+
) : (
|
|
243
|
+
<span className={`text-[10px] font-600 px-1.5 py-0.5 rounded-full ${plugin.enabled ? 'text-emerald-400 bg-emerald-400/10' : 'text-text-3/50 bg-white/[0.04]'}`}>
|
|
244
|
+
{plugin.enabled ? 'Enabled' : 'Disabled'}
|
|
245
|
+
</span>
|
|
246
|
+
)}
|
|
247
|
+
</div>
|
|
248
|
+
</div>
|
|
249
|
+
<div className="text-[11px] font-mono text-text-3/50 mb-1">{plugin.filename}</div>
|
|
250
|
+
{plugin.description && (
|
|
251
|
+
<p className="text-[12px] text-text-3/60 line-clamp-2">{plugin.description}</p>
|
|
252
|
+
)}
|
|
253
|
+
</button>
|
|
254
|
+
))}
|
|
255
|
+
</div>
|
|
256
|
+
)
|
|
257
|
+
)}
|
|
58
258
|
</div>
|
|
59
259
|
)
|
|
60
260
|
}
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import { useCallback, useEffect, useState } from 'react'
|
|
4
4
|
import { useAppStore } from '@/stores/use-app-store'
|
|
5
|
+
import { useWs } from '@/hooks/use-ws'
|
|
6
|
+
import { api } from '@/lib/api-client'
|
|
5
7
|
|
|
6
8
|
export function ProviderList({ inSidebar }: { inSidebar?: boolean }) {
|
|
7
9
|
const providers = useAppStore((s) => s.providers)
|
|
@@ -19,20 +21,26 @@ export function ProviderList({ inSidebar }: { inSidebar?: boolean }) {
|
|
|
19
21
|
setLoaded(true)
|
|
20
22
|
}, [loadProviders, loadProviderConfigs, loadCredentials])
|
|
21
23
|
|
|
22
|
-
useEffect(() => {
|
|
23
|
-
|
|
24
|
-
const poll = setInterval(() => { void loadProviders() }, 20_000)
|
|
25
|
-
return () => {
|
|
26
|
-
clearTimeout(bootstrap)
|
|
27
|
-
clearInterval(poll)
|
|
28
|
-
}
|
|
29
|
-
}, [refresh, loadProviders])
|
|
24
|
+
useEffect(() => { void refresh() }, [refresh])
|
|
25
|
+
useWs('providers', loadProviders, 20_000)
|
|
30
26
|
|
|
31
27
|
const handleEdit = (id: string) => {
|
|
32
28
|
setEditingProviderId(id)
|
|
33
29
|
setProviderSheetOpen(true)
|
|
34
30
|
}
|
|
35
31
|
|
|
32
|
+
const handleToggle = async (e: React.MouseEvent, id: string, currentEnabled: boolean) => {
|
|
33
|
+
e.stopPropagation()
|
|
34
|
+
await api('PUT', `/providers/${id}`, { isEnabled: !currentEnabled })
|
|
35
|
+
await loadProviderConfigs()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const handleDelete = async (e: React.MouseEvent, id: string) => {
|
|
39
|
+
e.stopPropagation()
|
|
40
|
+
await api('DELETE', `/providers/${id}`)
|
|
41
|
+
await loadProviderConfigs()
|
|
42
|
+
}
|
|
43
|
+
|
|
36
44
|
// Merge built-in providers with custom configs
|
|
37
45
|
const builtinItems = providers.map((p) => ({
|
|
38
46
|
id: p.id,
|
|
@@ -58,15 +66,15 @@ export function ProviderList({ inSidebar }: { inSidebar?: boolean }) {
|
|
|
58
66
|
|
|
59
67
|
if (!loaded) {
|
|
60
68
|
return (
|
|
61
|
-
<div className={`flex-1 flex items-center justify-center ${inSidebar ? 'px-3 pb-4' : 'px-
|
|
69
|
+
<div className={`flex-1 flex items-center justify-center ${inSidebar ? 'px-3 pb-4' : 'px-5'}`}>
|
|
62
70
|
<p className="text-[13px] text-text-3">Loading providers...</p>
|
|
63
71
|
</div>
|
|
64
72
|
)
|
|
65
73
|
}
|
|
66
74
|
|
|
67
75
|
return (
|
|
68
|
-
<div className={`flex-1 overflow-y-auto ${inSidebar ? 'px-3 pb-4' : 'px-
|
|
69
|
-
<div className=
|
|
76
|
+
<div className={`flex-1 overflow-y-auto ${inSidebar ? 'px-3 pb-4' : 'px-5 pb-6'}`}>
|
|
77
|
+
<div className={inSidebar ? 'space-y-2' : 'grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3'}>
|
|
70
78
|
{allItems.map((item) => (
|
|
71
79
|
<button
|
|
72
80
|
key={item.id}
|
|
@@ -81,12 +89,37 @@ export function ProviderList({ inSidebar }: { inSidebar?: boolean }) {
|
|
|
81
89
|
${item.type === 'builtin' ? 'bg-white/[0.04] text-text-3' : 'bg-[#6366F1]/10 text-[#6366F1]'}`}>
|
|
82
90
|
{item.type === 'builtin' ? 'Built-in' : 'Custom'}
|
|
83
91
|
</span>
|
|
92
|
+
{!inSidebar && item.type === 'custom' && (
|
|
93
|
+
<>
|
|
94
|
+
<div
|
|
95
|
+
onClick={(e) => handleToggle(e, item.id, item.isEnabled)}
|
|
96
|
+
className={`w-9 h-5 rounded-full transition-all relative cursor-pointer shrink-0
|
|
97
|
+
${item.isEnabled ? 'bg-[#6366F1]' : 'bg-white/[0.08]'}`}
|
|
98
|
+
>
|
|
99
|
+
<div className={`absolute top-0.5 w-4 h-4 rounded-full bg-white transition-all
|
|
100
|
+
${item.isEnabled ? 'left-[18px]' : 'left-0.5'}`} />
|
|
101
|
+
</div>
|
|
102
|
+
<button
|
|
103
|
+
onClick={(e) => handleDelete(e, item.id)}
|
|
104
|
+
className="text-text-3/40 hover:text-red-400 transition-colors p-0.5"
|
|
105
|
+
title="Delete provider"
|
|
106
|
+
>
|
|
107
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
108
|
+
<path d="M3 6h18M19 6v14a2 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" />
|
|
109
|
+
</svg>
|
|
110
|
+
</button>
|
|
111
|
+
</>
|
|
112
|
+
)}
|
|
84
113
|
<span className={`w-2 h-2 rounded-full ${item.isConnected ? 'bg-emerald-400' : 'bg-white/10'}`} />
|
|
85
114
|
</div>
|
|
86
115
|
</div>
|
|
87
116
|
<div className="text-[12px] text-text-3/60 font-mono truncate">
|
|
88
|
-
{item.models.
|
|
89
|
-
|
|
117
|
+
{!inSidebar ? item.models.join(', ') : (
|
|
118
|
+
<>
|
|
119
|
+
{item.models.slice(0, 3).join(', ')}
|
|
120
|
+
{item.models.length > 3 && ` +${item.models.length - 3}`}
|
|
121
|
+
</>
|
|
122
|
+
)}
|
|
90
123
|
</div>
|
|
91
124
|
</button>
|
|
92
125
|
))}
|
|
@@ -5,7 +5,6 @@ import { useAppStore } from '@/stores/use-app-store'
|
|
|
5
5
|
import { createProviderConfig, updateProviderConfig, deleteProviderConfig } from '@/lib/provider-config'
|
|
6
6
|
import { api } from '@/lib/api-client'
|
|
7
7
|
import { BottomSheet } from '@/components/shared/bottom-sheet'
|
|
8
|
-
import { AiGenBlock } from '@/components/shared/ai-gen-block'
|
|
9
8
|
import { toast } from 'sonner'
|
|
10
9
|
|
|
11
10
|
export function ProviderSheet() {
|
|
@@ -41,51 +40,15 @@ export function ProviderSheet() {
|
|
|
41
40
|
const [localLoading, setLocalLoading] = useState(false)
|
|
42
41
|
const [localError, setLocalError] = useState('')
|
|
43
42
|
|
|
44
|
-
// AI generation state
|
|
45
|
-
const [aiPrompt, setAiPrompt] = useState('')
|
|
46
|
-
const [generating, setGenerating] = useState(false)
|
|
47
|
-
const [generated, setGenerated] = useState(false)
|
|
48
|
-
const [genError, setGenError] = useState('')
|
|
49
|
-
const appSettings = useAppStore((s) => s.appSettings)
|
|
50
|
-
const loadSettings = useAppStore((s) => s.loadSettings)
|
|
51
|
-
|
|
52
43
|
// Find editing provider in custom configs OR built-in list
|
|
53
44
|
const editingCustom = editingId ? providerConfigs.find((c) => c.id === editingId) : null
|
|
54
45
|
const editingBuiltin = editingId ? providers.find((p) => p.id === editingId) : null
|
|
55
46
|
const isBuiltin = !!editingBuiltin && !editingCustom
|
|
56
47
|
const editing = editingCustom || editingBuiltin
|
|
57
48
|
|
|
58
|
-
const handleGenerate = async () => {
|
|
59
|
-
if (!aiPrompt.trim()) return
|
|
60
|
-
setGenerating(true)
|
|
61
|
-
setGenError('')
|
|
62
|
-
try {
|
|
63
|
-
const result = await api<{ name?: string; baseUrl?: string; models?: string; requiresApiKey?: boolean; error?: string }>('POST', '/generate', { type: 'provider', prompt: aiPrompt })
|
|
64
|
-
if (result.error) {
|
|
65
|
-
setGenError(result.error)
|
|
66
|
-
} else if (result.name || result.baseUrl) {
|
|
67
|
-
if (result.name) setName(result.name)
|
|
68
|
-
if (result.baseUrl) setBaseUrl(result.baseUrl)
|
|
69
|
-
if (result.models) setModels(result.models)
|
|
70
|
-
if (result.requiresApiKey !== undefined) setRequiresApiKey(result.requiresApiKey)
|
|
71
|
-
setGenerated(true)
|
|
72
|
-
} else {
|
|
73
|
-
setGenError('AI returned empty response — try again')
|
|
74
|
-
}
|
|
75
|
-
} catch (err: unknown) {
|
|
76
|
-
setGenError(err instanceof Error ? err.message : 'Generation failed')
|
|
77
|
-
}
|
|
78
|
-
setGenerating(false)
|
|
79
|
-
}
|
|
80
|
-
|
|
81
49
|
useEffect(() => {
|
|
82
50
|
if (open) {
|
|
83
51
|
loadCredentials()
|
|
84
|
-
loadSettings()
|
|
85
|
-
setAiPrompt('')
|
|
86
|
-
setGenerating(false)
|
|
87
|
-
setGenerated(false)
|
|
88
|
-
setGenError('')
|
|
89
52
|
setNewModel('')
|
|
90
53
|
setLocalModels([])
|
|
91
54
|
setLocalError('')
|
|
@@ -248,14 +211,6 @@ export function ProviderSheet() {
|
|
|
248
211
|
</p>
|
|
249
212
|
</div>
|
|
250
213
|
|
|
251
|
-
{/* AI Generation — only for new custom providers */}
|
|
252
|
-
{isNew && <AiGenBlock
|
|
253
|
-
aiPrompt={aiPrompt} setAiPrompt={setAiPrompt}
|
|
254
|
-
generating={generating} generated={generated} genError={genError}
|
|
255
|
-
onGenerate={handleGenerate} appSettings={appSettings}
|
|
256
|
-
placeholder='Name a provider, e.g. "Groq", "Together AI", "z.ai", "DeepSeek"'
|
|
257
|
-
/>}
|
|
258
|
-
|
|
259
214
|
{/* Name */}
|
|
260
215
|
<div className="mb-8">
|
|
261
216
|
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Name</label>
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useEffect, useState,
|
|
3
|
+
import { useEffect, useState, useCallback } from 'react'
|
|
4
4
|
import { api } from '@/lib/api-client'
|
|
5
|
+
import { useWs } from '@/hooks/use-ws'
|
|
5
6
|
import { BottomSheet } from '@/components/shared/bottom-sheet'
|
|
6
7
|
import type { SessionRunRecord, SessionRunStatus } from '@/types'
|
|
7
8
|
|
|
@@ -37,7 +38,6 @@ export function RunList() {
|
|
|
37
38
|
const [autoRefresh, setAutoRefresh] = useState(false)
|
|
38
39
|
const [statusFilter, setStatusFilter] = useState<SessionRunStatus | null>(null)
|
|
39
40
|
const [selected, setSelected] = useState<SessionRunRecord | null>(null)
|
|
40
|
-
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
|
41
41
|
|
|
42
42
|
const fetchRuns = useCallback(async () => {
|
|
43
43
|
try {
|
|
@@ -54,16 +54,7 @@ export function RunList() {
|
|
|
54
54
|
fetchRuns()
|
|
55
55
|
}, [fetchRuns])
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
if (!autoRefresh) {
|
|
59
|
-
if (timerRef.current) clearInterval(timerRef.current)
|
|
60
|
-
return
|
|
61
|
-
}
|
|
62
|
-
timerRef.current = setInterval(fetchRuns, 3000)
|
|
63
|
-
return () => {
|
|
64
|
-
if (timerRef.current) clearInterval(timerRef.current)
|
|
65
|
-
}
|
|
66
|
-
}, [autoRefresh, fetchRuns])
|
|
57
|
+
useWs('runs', fetchRuns, autoRefresh ? 3000 : undefined)
|
|
67
58
|
|
|
68
59
|
const filtered = statusFilter ? runs.filter((r) => r.status === statusFilter) : runs
|
|
69
60
|
|
|
@@ -78,7 +69,7 @@ export function RunList() {
|
|
|
78
69
|
return (
|
|
79
70
|
<div className="flex-1 flex flex-col overflow-hidden">
|
|
80
71
|
{/* Controls */}
|
|
81
|
-
<div className="px-
|
|
72
|
+
<div className="px-5 py-2 space-y-2 shrink-0">
|
|
82
73
|
{/* Status filter + auto-refresh */}
|
|
83
74
|
<div className="flex items-center gap-1.5 flex-wrap">
|
|
84
75
|
<button
|
|
@@ -113,12 +104,12 @@ export function RunList() {
|
|
|
113
104
|
</div>
|
|
114
105
|
|
|
115
106
|
{/* Count */}
|
|
116
|
-
<div className="px-
|
|
107
|
+
<div className="px-5 py-1 text-[10px] text-text-3/60">
|
|
117
108
|
{filtered.length} run{filtered.length !== 1 ? 's' : ''}
|
|
118
109
|
</div>
|
|
119
110
|
|
|
120
111
|
{/* Run list */}
|
|
121
|
-
<div className="flex-1 overflow-y-auto px-
|
|
112
|
+
<div className="flex-1 overflow-y-auto px-4 pb-8">
|
|
122
113
|
{filtered.length === 0 ? (
|
|
123
114
|
<div className="flex items-center justify-center h-32 text-text-3 text-[12px]">
|
|
124
115
|
No runs found
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import type { Schedule } from '@/types'
|
|
4
4
|
import { useAppStore } from '@/stores/use-app-store'
|
|
5
|
+
import { api } from '@/lib/api-client'
|
|
5
6
|
|
|
6
7
|
const STATUS_COLORS: Record<string, string> = {
|
|
7
8
|
active: 'text-emerald-400 bg-emerald-400/[0.08]',
|
|
@@ -24,11 +25,13 @@ function formatNext(ts?: number): string {
|
|
|
24
25
|
|
|
25
26
|
interface Props {
|
|
26
27
|
schedule: Schedule
|
|
28
|
+
inSidebar?: boolean
|
|
27
29
|
}
|
|
28
30
|
|
|
29
|
-
export function ScheduleCard({ schedule }: Props) {
|
|
31
|
+
export function ScheduleCard({ schedule, inSidebar }: Props) {
|
|
30
32
|
const setEditingScheduleId = useAppStore((s) => s.setEditingScheduleId)
|
|
31
33
|
const setScheduleSheetOpen = useAppStore((s) => s.setScheduleSheetOpen)
|
|
34
|
+
const loadSchedules = useAppStore((s) => s.loadSchedules)
|
|
32
35
|
const agents = useAppStore((s) => s.agents)
|
|
33
36
|
|
|
34
37
|
const handleClick = () => {
|
|
@@ -36,8 +39,22 @@ export function ScheduleCard({ schedule }: Props) {
|
|
|
36
39
|
setScheduleSheetOpen(true)
|
|
37
40
|
}
|
|
38
41
|
|
|
42
|
+
const handleToggle = async (e: React.MouseEvent) => {
|
|
43
|
+
e.stopPropagation()
|
|
44
|
+
const newStatus = schedule.status === 'active' ? 'paused' : 'active'
|
|
45
|
+
await api('PUT', `/schedules/${schedule.id}`, { status: newStatus })
|
|
46
|
+
loadSchedules()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const handleDelete = async (e: React.MouseEvent) => {
|
|
50
|
+
e.stopPropagation()
|
|
51
|
+
await api('DELETE', `/schedules/${schedule.id}`)
|
|
52
|
+
loadSchedules()
|
|
53
|
+
}
|
|
54
|
+
|
|
39
55
|
const agent = agents[schedule.agentId]
|
|
40
56
|
const statusClass = STATUS_COLORS[schedule.status] || STATUS_COLORS.paused
|
|
57
|
+
const canToggle = schedule.status === 'active' || schedule.status === 'paused'
|
|
41
58
|
|
|
42
59
|
return (
|
|
43
60
|
<div
|
|
@@ -48,12 +65,45 @@ export function ScheduleCard({ schedule }: Props) {
|
|
|
48
65
|
>
|
|
49
66
|
<div className="flex items-center gap-2.5">
|
|
50
67
|
<span className="font-display text-[14px] font-600 truncate flex-1 tracking-[-0.01em]">{schedule.name}</span>
|
|
51
|
-
<
|
|
52
|
-
{
|
|
53
|
-
|
|
68
|
+
<div className="flex items-center gap-2 shrink-0">
|
|
69
|
+
{!inSidebar && canToggle && (
|
|
70
|
+
<div
|
|
71
|
+
onClick={handleToggle}
|
|
72
|
+
className={`w-9 h-5 rounded-full transition-all relative cursor-pointer shrink-0
|
|
73
|
+
${schedule.status === 'active' ? 'bg-[#6366F1]' : 'bg-white/[0.08]'}`}
|
|
74
|
+
>
|
|
75
|
+
<div className={`absolute top-0.5 w-4 h-4 rounded-full bg-white transition-all
|
|
76
|
+
${schedule.status === 'active' ? 'left-[18px]' : 'left-0.5'}`} />
|
|
77
|
+
</div>
|
|
78
|
+
)}
|
|
79
|
+
<span className={`text-[10px] font-600 uppercase tracking-wider px-2 py-0.5 rounded-[6px] ${statusClass}`}>
|
|
80
|
+
{schedule.status}
|
|
81
|
+
</span>
|
|
82
|
+
{!inSidebar && (
|
|
83
|
+
<button
|
|
84
|
+
onClick={handleDelete}
|
|
85
|
+
className="text-text-3/40 hover:text-red-400 transition-colors p-0.5"
|
|
86
|
+
title="Delete"
|
|
87
|
+
>
|
|
88
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
89
|
+
<path d="M3 6h18M19 6v14a2 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" />
|
|
90
|
+
</svg>
|
|
91
|
+
</button>
|
|
92
|
+
)}
|
|
93
|
+
</div>
|
|
54
94
|
</div>
|
|
55
95
|
<div className="text-[12px] text-text-3/70 mt-1.5 truncate">
|
|
56
96
|
{agent?.name || 'Unknown agent'} · {schedule.scheduleType}
|
|
97
|
+
{!inSidebar && schedule.scheduleType === 'cron' && schedule.cron && (
|
|
98
|
+
<span className="font-mono text-text-3/50 ml-1">({schedule.cron})</span>
|
|
99
|
+
)}
|
|
100
|
+
{!inSidebar && schedule.scheduleType === 'interval' && schedule.intervalMs && (
|
|
101
|
+
<span className="text-text-3/50 ml-1">
|
|
102
|
+
(every {schedule.intervalMs >= 3600000
|
|
103
|
+
? `${Math.round(schedule.intervalMs / 3600000)}h`
|
|
104
|
+
: `${Math.round(schedule.intervalMs / 60000)}m`})
|
|
105
|
+
</span>
|
|
106
|
+
)}
|
|
57
107
|
</div>
|
|
58
108
|
<div className="text-[11px] text-text-3/60 mt-1">
|
|
59
109
|
Next: {formatNext(schedule.nextRunAt)}
|
|
@@ -54,7 +54,7 @@ export function ScheduleList({ inSidebar }: Props) {
|
|
|
54
54
|
return (
|
|
55
55
|
<div className="flex-1 overflow-y-auto">
|
|
56
56
|
{(filtered.length > 3 || search) && (
|
|
57
|
-
<div className=
|
|
57
|
+
<div className={inSidebar ? 'px-4 py-2.5' : 'px-5 py-2.5'}>
|
|
58
58
|
<input
|
|
59
59
|
type="text"
|
|
60
60
|
value={search}
|
|
@@ -66,9 +66,12 @@ export function ScheduleList({ inSidebar }: Props) {
|
|
|
66
66
|
/>
|
|
67
67
|
</div>
|
|
68
68
|
)}
|
|
69
|
-
<div className=
|
|
69
|
+
<div className={inSidebar
|
|
70
|
+
? 'flex flex-col gap-1 px-2 pb-4'
|
|
71
|
+
: 'grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3 px-5 pb-6'
|
|
72
|
+
}>
|
|
70
73
|
{filtered.map((s) => (
|
|
71
|
-
<ScheduleCard key={s.id} schedule={s} />
|
|
74
|
+
<ScheduleCard key={s.id} schedule={s} inSidebar={inSidebar} />
|
|
72
75
|
))}
|
|
73
76
|
</div>
|
|
74
77
|
</div>
|