@swarmclawai/swarmclaw 0.7.1 → 0.7.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 +85 -139
- package/package.json +1 -1
- package/src/app/api/agents/[id]/thread/route.ts +1 -2
- package/src/app/api/agents/route.ts +1 -1
- package/src/app/api/{sessions → chats}/[id]/checkpoints/route.ts +1 -1
- package/src/app/api/{sessions → chats}/[id]/main-loop/route.ts +2 -2
- package/src/app/api/{sessions → chats}/[id]/restore/route.ts +1 -1
- package/src/app/api/{sessions → chats}/[id]/route.ts +4 -52
- package/src/app/api/{sessions → chats}/route.ts +5 -7
- package/src/app/api/plugins/route.ts +3 -0
- package/src/app/api/plugins/settings/route.ts +35 -0
- package/src/app/api/usage/route.ts +30 -0
- package/src/cli/index.js +35 -33
- package/src/cli/index.ts +40 -39
- package/src/cli/spec.js +29 -27
- package/src/components/agents/agent-card.tsx +1 -1
- package/src/components/agents/agent-chat-list.tsx +3 -3
- package/src/components/agents/agent-list.tsx +8 -13
- package/src/components/agents/agent-sheet.tsx +2 -2
- package/src/components/agents/cron-job-form.tsx +3 -3
- package/src/components/agents/inspector-panel.tsx +2 -2
- package/src/components/auth/setup-wizard.tsx +5 -38
- package/src/components/chat/chat-area.tsx +10 -14
- package/src/components/{sessions/session-card.tsx → chat/chat-card.tsx} +3 -3
- package/src/components/chat/chat-header.tsx +156 -73
- package/src/components/{sessions/session-list.tsx → chat/chat-list.tsx} +4 -5
- package/src/components/chat/chat-tool-toggles.tsx +26 -17
- package/src/components/chat/checkpoint-timeline.tsx +4 -4
- package/src/components/chat/message-bubble.tsx +4 -1
- package/src/components/chat/message-list.tsx +2 -2
- package/src/components/{sessions/new-session-sheet.tsx → chat/new-chat-sheet.tsx} +6 -6
- package/src/components/chat/session-debug-panel.tsx +1 -1
- package/src/components/chat/tool-request-banner.tsx +3 -3
- package/src/components/chatrooms/agent-hover-card.tsx +3 -3
- package/src/components/chatrooms/chatroom-tool-request-banner.tsx +2 -2
- package/src/components/connectors/connector-sheet.tsx +1 -1
- package/src/components/home/home-view.tsx +1 -1
- package/src/components/layout/app-layout.tsx +23 -2
- package/src/components/plugins/plugin-list.tsx +475 -254
- package/src/components/plugins/plugin-sheet.tsx +124 -10
- package/src/components/settings/gateway-connection-panel.tsx +1 -1
- package/src/components/shared/command-palette.tsx +0 -1
- package/src/components/shared/settings/section-heartbeat.tsx +1 -1
- package/src/components/shared/settings/section-providers.tsx +1 -1
- package/src/components/shared/settings/settings-page.tsx +1 -12
- package/src/components/usage/metrics-dashboard.tsx +73 -0
- package/src/components/webhooks/webhook-sheet.tsx +1 -1
- package/src/lib/chat.ts +1 -1
- package/src/lib/{sessions.ts → chats.ts} +28 -18
- package/src/lib/providers/claude-cli.ts +1 -1
- package/src/lib/server/approvals.ts +4 -4
- package/src/lib/server/capability-router.ts +10 -8
- package/src/lib/server/chat-execution.ts +36 -105
- package/src/lib/server/chatroom-helpers.ts +3 -3
- package/src/lib/server/connectors/manager.ts +4 -4
- package/src/lib/server/cost.ts +34 -1
- package/src/lib/server/daemon-state.ts +2 -2
- package/src/lib/server/heartbeat-service.ts +1 -1
- package/src/lib/server/main-agent-loop.ts +25 -160
- package/src/lib/server/main-session.ts +6 -13
- package/src/lib/server/orchestrator-lg.ts +3 -3
- package/src/lib/server/orchestrator.ts +5 -5
- package/src/lib/server/plugins.ts +112 -4
- package/src/lib/server/provider-health.ts +5 -3
- package/src/lib/server/queue.ts +12 -10
- package/src/lib/server/session-run-manager.test.ts +9 -6
- package/src/lib/server/session-run-manager.ts +1 -3
- package/src/lib/server/session-tools/calendar.ts +376 -0
- package/src/lib/server/session-tools/canvas.ts +1 -1
- package/src/lib/server/session-tools/chatroom.ts +4 -2
- package/src/lib/server/session-tools/connector.ts +5 -2
- package/src/lib/server/session-tools/context.ts +7 -3
- package/src/lib/server/session-tools/crud.ts +14 -6
- package/src/lib/server/session-tools/delegate.ts +95 -8
- package/src/lib/server/session-tools/discovery.ts +2 -2
- package/src/lib/server/session-tools/edit_file.ts +4 -2
- package/src/lib/server/session-tools/email.ts +322 -0
- package/src/lib/server/session-tools/file.ts +5 -2
- package/src/lib/server/session-tools/git.ts +1 -1
- package/src/lib/server/session-tools/http.ts +1 -1
- package/src/lib/server/session-tools/image-gen.ts +382 -0
- package/src/lib/server/session-tools/index.ts +74 -49
- package/src/lib/server/session-tools/memory.ts +139 -2
- package/src/lib/server/session-tools/monitor.ts +1 -1
- package/src/lib/server/session-tools/openclaw-nodes.ts +1 -1
- package/src/lib/server/session-tools/openclaw-workspace.ts +1 -1
- package/src/lib/server/session-tools/platform.ts +6 -3
- package/src/lib/server/session-tools/plugin-creator.ts +3 -3
- package/src/lib/server/session-tools/replicate.ts +303 -0
- package/src/lib/server/session-tools/sample-ui.ts +1 -1
- package/src/lib/server/session-tools/sandbox.ts +4 -2
- package/src/lib/server/session-tools/schedule.ts +4 -2
- package/src/lib/server/session-tools/session-info.ts +7 -4
- package/src/lib/server/session-tools/shell.ts +5 -2
- package/src/lib/server/session-tools/subagent.ts +2 -2
- package/src/lib/server/session-tools/wallet.ts +29 -2
- package/src/lib/server/session-tools/web.ts +44 -5
- package/src/lib/server/storage.ts +29 -9
- package/src/lib/server/stream-agent-chat.ts +72 -249
- package/src/lib/server/tool-aliases.ts +26 -15
- package/src/lib/server/tool-capability-policy.test.ts +9 -9
- package/src/lib/server/tool-capability-policy.ts +32 -27
- package/src/lib/tool-definitions.ts +4 -0
- package/src/lib/validation/schemas.ts +3 -1
- package/src/stores/use-app-store.ts +5 -5
- package/src/stores/use-chat-store.ts +7 -7
- package/src/types/index.ts +65 -3
- /package/src/app/api/{sessions → chats}/[id]/browser/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/chat/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/clear/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/deploy/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/devserver/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/edit-resend/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/fork/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/mailbox/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/messages/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/retry/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/stop/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/heartbeat/route.ts +0 -0
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useEffect, useState, useCallback } from 'react'
|
|
3
|
+
import { useEffect, useState, useCallback, useMemo } from 'react'
|
|
4
4
|
import { useAppStore } from '@/stores/use-app-store'
|
|
5
5
|
import { api } from '@/lib/api-client'
|
|
6
6
|
import { toast } from 'sonner'
|
|
7
|
-
import type { MarketplacePlugin, PluginMeta } from '@/types'
|
|
7
|
+
import type { Agent, MarketplacePlugin, PluginMeta } from '@/types'
|
|
8
8
|
import { AgentAvatar } from '@/components/agents/agent-avatar'
|
|
9
9
|
import { ConfirmDialog } from '@/components/shared/confirm-dialog'
|
|
10
10
|
|
|
11
|
+
type InstalledTab = 'core' | 'extensions'
|
|
12
|
+
type TopTab = InstalledTab | 'swarmforge'
|
|
13
|
+
|
|
11
14
|
export function PluginList({ inSidebar }: { inSidebar?: boolean }) {
|
|
12
15
|
const plugins = useAppStore((s) => s.plugins)
|
|
13
16
|
const loadPlugins = useAppStore((s) => s.loadPlugins)
|
|
@@ -19,7 +22,6 @@ export function PluginList({ inSidebar }: { inSidebar?: boolean }) {
|
|
|
19
22
|
const setActiveView = useAppStore((s) => s.setActiveView)
|
|
20
23
|
|
|
21
24
|
const navigateToAgentChat = useCallback((agentId: string) => {
|
|
22
|
-
// Find the most recent chat for this agent
|
|
23
25
|
const agentSession = Object.values(sessions)
|
|
24
26
|
.filter((s) => s.agentId === agentId)
|
|
25
27
|
.sort((a, b) => (b.lastActiveAt ?? 0) - (a.lastActiveAt ?? 0))[0]
|
|
@@ -30,7 +32,7 @@ export function PluginList({ inSidebar }: { inSidebar?: boolean }) {
|
|
|
30
32
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
31
33
|
}, [sessions])
|
|
32
34
|
|
|
33
|
-
const [tab, setTab] = useState<
|
|
35
|
+
const [tab, setTab] = useState<TopTab>('core')
|
|
34
36
|
const [marketplace, setMarketplace] = useState<MarketplacePlugin[]>([])
|
|
35
37
|
const [mpLoading, setMpLoading] = useState(false)
|
|
36
38
|
const [installing, setInstalling] = useState<string | null>(null)
|
|
@@ -54,16 +56,28 @@ export function PluginList({ inSidebar }: { inSidebar?: boolean }) {
|
|
|
54
56
|
}, [])
|
|
55
57
|
|
|
56
58
|
useEffect(() => {
|
|
57
|
-
if (inSidebar || tab !== '
|
|
58
|
-
const timer = setTimeout(() => {
|
|
59
|
-
void loadMarketplace()
|
|
60
|
-
}, 0)
|
|
59
|
+
if (inSidebar || tab !== 'swarmforge') return
|
|
60
|
+
const timer = setTimeout(() => { void loadMarketplace() }, 0)
|
|
61
61
|
return () => clearTimeout(timer)
|
|
62
62
|
}, [tab, inSidebar, loadMarketplace])
|
|
63
63
|
|
|
64
64
|
const pluginList = Object.values(plugins)
|
|
65
|
-
const corePlugins = pluginList.filter((
|
|
66
|
-
const extensionPlugins = pluginList.filter((
|
|
65
|
+
const corePlugins = useMemo(() => pluginList.filter((p) => p.source === 'local'), [pluginList])
|
|
66
|
+
const extensionPlugins = useMemo(() => pluginList.filter((p) => p.source !== 'local'), [pluginList])
|
|
67
|
+
|
|
68
|
+
// Search filtering for installed plugins
|
|
69
|
+
const filterInstalled = useCallback((list: PluginMeta[]) => {
|
|
70
|
+
if (!search.trim()) return list
|
|
71
|
+
const q = search.toLowerCase()
|
|
72
|
+
return list.filter((p) =>
|
|
73
|
+
p.name.toLowerCase().includes(q) ||
|
|
74
|
+
(p.description || '').toLowerCase().includes(q) ||
|
|
75
|
+
p.filename.toLowerCase().includes(q)
|
|
76
|
+
)
|
|
77
|
+
}, [search])
|
|
78
|
+
|
|
79
|
+
const filteredCore = useMemo(() => filterInstalled(corePlugins), [filterInstalled, corePlugins])
|
|
80
|
+
const filteredExtensions = useMemo(() => filterInstalled(extensionPlugins), [filterInstalled, extensionPlugins])
|
|
67
81
|
|
|
68
82
|
const handleEdit = (filename: string) => {
|
|
69
83
|
setEditingPluginFilename(filename)
|
|
@@ -117,111 +131,331 @@ export function PluginList({ inSidebar }: { inSidebar?: boolean }) {
|
|
|
117
131
|
|
|
118
132
|
const installedFilenames = new Set(Object.keys(plugins))
|
|
119
133
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
134
|
+
// --- Sidebar mode ---
|
|
135
|
+
if (inSidebar) {
|
|
136
|
+
return (
|
|
137
|
+
<div className="px-3 pb-4 flex-1 overflow-y-auto">
|
|
138
|
+
<div className="space-y-2">
|
|
139
|
+
{pluginList.map((plugin) => (
|
|
140
|
+
<SidebarPluginCard key={plugin.filename} plugin={plugin} onEdit={handleEdit} />
|
|
141
|
+
))}
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
)
|
|
131
145
|
}
|
|
132
146
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
147
|
+
// --- Full page mode ---
|
|
148
|
+
const enabledCount = pluginList.filter((p) => p.enabled).length
|
|
149
|
+
const totalTools = pluginList.reduce((acc, p) => acc + (p.toolCount ?? 0), 0)
|
|
150
|
+
const totalHooks = pluginList.reduce((acc, p) => acc + (p.hookCount ?? 0), 0)
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<div className="flex-1 overflow-y-auto px-5 pb-6">
|
|
154
|
+
{/* Stats bar */}
|
|
155
|
+
<div className="flex items-center gap-3 mb-4">
|
|
156
|
+
<Stat label="Installed" value={pluginList.length} />
|
|
157
|
+
<Stat label="Enabled" value={enabledCount} accent />
|
|
158
|
+
<Stat label="Tools" value={totalTools} />
|
|
159
|
+
<Stat label="Hooks" value={totalHooks} />
|
|
160
|
+
<div className="flex-1" />
|
|
161
|
+
{/* Search */}
|
|
162
|
+
<div className="relative w-[260px]">
|
|
163
|
+
<svg className="absolute left-2.5 top-1/2 -translate-y-1/2 text-text-3/40" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
|
|
164
|
+
<circle cx="11" cy="11" r="8" /><line x1="21" y1="21" x2="16.65" y2="16.65" />
|
|
165
|
+
</svg>
|
|
166
|
+
<input
|
|
167
|
+
value={search}
|
|
168
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
169
|
+
placeholder="Search plugins..."
|
|
170
|
+
className="w-full pl-8 pr-3 py-2 rounded-[10px] bg-surface border border-white/[0.06] text-[12px] text-text placeholder:text-text-3/40 outline-none focus:border-accent-bright/30 transition-colors"
|
|
171
|
+
style={{ fontFamily: 'inherit' }}
|
|
172
|
+
/>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
|
|
176
|
+
{/* Tabs */}
|
|
177
|
+
<div className="flex items-center gap-1 mb-5 border-b border-white/[0.06] pb-px">
|
|
178
|
+
<TabButton active={tab === 'core'} onClick={() => setTab('core')} count={corePlugins.length}>
|
|
179
|
+
Core
|
|
180
|
+
</TabButton>
|
|
181
|
+
<TabButton active={tab === 'extensions'} onClick={() => setTab('extensions')} count={extensionPlugins.length}>
|
|
182
|
+
Extensions
|
|
183
|
+
</TabButton>
|
|
184
|
+
<TabButton active={tab === 'swarmforge'} onClick={() => setTab('swarmforge')}>
|
|
185
|
+
SwarmForge
|
|
186
|
+
</TabButton>
|
|
187
|
+
</div>
|
|
188
|
+
|
|
189
|
+
{/* Tab content */}
|
|
190
|
+
{tab === 'core' && (
|
|
191
|
+
<InstalledGrid
|
|
192
|
+
plugins={filteredCore}
|
|
193
|
+
allowDelete={false}
|
|
194
|
+
search={search}
|
|
195
|
+
agents={agents}
|
|
196
|
+
onEdit={handleEdit}
|
|
197
|
+
onToggle={handleToggle}
|
|
198
|
+
onDelete={handleDeleteClick}
|
|
199
|
+
onNavigateToAgent={navigateToAgentChat}
|
|
200
|
+
emptyMessage="No core plugins found"
|
|
201
|
+
/>
|
|
202
|
+
)}
|
|
203
|
+
|
|
204
|
+
{tab === 'extensions' && (
|
|
205
|
+
<InstalledGrid
|
|
206
|
+
plugins={filteredExtensions}
|
|
207
|
+
allowDelete
|
|
208
|
+
search={search}
|
|
209
|
+
agents={agents}
|
|
210
|
+
onEdit={handleEdit}
|
|
211
|
+
onToggle={handleToggle}
|
|
212
|
+
onDelete={handleDeleteClick}
|
|
213
|
+
onNavigateToAgent={navigateToAgentChat}
|
|
214
|
+
emptyMessage={search ? 'No extensions match your search' : 'No extensions installed'}
|
|
215
|
+
emptyAction={!search ? (
|
|
216
|
+
<button
|
|
217
|
+
onClick={() => setTab('swarmforge')}
|
|
218
|
+
className="mt-3 px-4 py-2 rounded-[10px] bg-transparent text-accent-bright text-[12px] font-600 cursor-pointer border border-accent-bright/20 hover:bg-accent-soft transition-all"
|
|
219
|
+
style={{ fontFamily: 'inherit' }}
|
|
220
|
+
>
|
|
221
|
+
Browse SwarmForge
|
|
222
|
+
</button>
|
|
223
|
+
) : undefined}
|
|
224
|
+
/>
|
|
225
|
+
)}
|
|
226
|
+
|
|
227
|
+
{tab === 'swarmforge' && (
|
|
228
|
+
<MarketplaceTab
|
|
229
|
+
marketplace={marketplace}
|
|
230
|
+
loading={mpLoading}
|
|
231
|
+
installing={installing}
|
|
232
|
+
installedFilenames={installedFilenames}
|
|
233
|
+
search={search}
|
|
234
|
+
activeTag={activeTag}
|
|
235
|
+
setActiveTag={setActiveTag}
|
|
236
|
+
sort={sort}
|
|
237
|
+
setSort={setSort}
|
|
238
|
+
onInstall={installFromMarketplace}
|
|
239
|
+
/>
|
|
240
|
+
)}
|
|
241
|
+
|
|
242
|
+
<ConfirmDialog
|
|
243
|
+
open={!!confirmDelete}
|
|
244
|
+
title="Delete Plugin"
|
|
245
|
+
message={confirmDelete ? `Delete "${confirmDelete.name}"? This cannot be undone.` : ''}
|
|
246
|
+
confirmLabel={deleting ? 'Deleting...' : 'Delete'}
|
|
247
|
+
danger
|
|
248
|
+
onConfirm={() => { void handleDeleteConfirm() }}
|
|
249
|
+
onCancel={() => { if (!deleting) setConfirmDelete(null) }}
|
|
250
|
+
/>
|
|
251
|
+
</div>
|
|
252
|
+
)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// --- Sub-components ---
|
|
256
|
+
|
|
257
|
+
function Stat({ label, value, accent }: { label: string; value: number; accent?: boolean }) {
|
|
258
|
+
return (
|
|
259
|
+
<div className="flex items-center gap-1.5">
|
|
260
|
+
<span className={`text-[18px] font-700 tabular-nums ${accent ? 'text-accent-bright' : 'text-text'}`}>
|
|
261
|
+
{value}
|
|
262
|
+
</span>
|
|
263
|
+
<span className="text-[11px] text-text-3/60 font-500">{label}</span>
|
|
264
|
+
</div>
|
|
265
|
+
)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function TabButton({ active, onClick, count, children }: {
|
|
269
|
+
active: boolean; onClick: () => void; count?: number; children: React.ReactNode
|
|
270
|
+
}) {
|
|
271
|
+
return (
|
|
272
|
+
<button
|
|
273
|
+
onClick={onClick}
|
|
274
|
+
className={`relative px-3 py-2 text-[12px] font-600 cursor-pointer transition-all border-none bg-transparent
|
|
275
|
+
${active ? 'text-accent-bright' : 'text-text-3/60 hover:text-text-2'}`}
|
|
276
|
+
style={{ fontFamily: 'inherit' }}
|
|
277
|
+
>
|
|
278
|
+
<span className="flex items-center gap-1.5">
|
|
279
|
+
{children}
|
|
280
|
+
{count !== undefined && (
|
|
281
|
+
<span className={`text-[10px] tabular-nums px-1.5 py-px rounded-full ${
|
|
282
|
+
active ? 'bg-accent-soft text-accent-bright' : 'bg-white/[0.04] text-text-3/50'
|
|
283
|
+
}`}>
|
|
284
|
+
{count}
|
|
285
|
+
</span>
|
|
286
|
+
)}
|
|
287
|
+
</span>
|
|
288
|
+
{active && <div className="absolute bottom-0 left-2 right-2 h-[2px] rounded-full bg-accent-bright" />}
|
|
289
|
+
</button>
|
|
290
|
+
)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function pluginDescription(plugin: PluginMeta): string {
|
|
294
|
+
const raw = (plugin.description || '').trim()
|
|
295
|
+
if (raw) return raw
|
|
296
|
+
const sourceLabel = plugin.source === 'local' ? 'core plugin' : 'installed plugin'
|
|
297
|
+
return `No description provided. Click to view metadata and controls for this ${sourceLabel}.`
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function pluginCapabilityBadges(plugin: PluginMeta): string[] {
|
|
301
|
+
const badges: string[] = []
|
|
302
|
+
if (plugin.toolCount && plugin.toolCount > 0) badges.push(`${plugin.toolCount} tool${plugin.toolCount === 1 ? '' : 's'}`)
|
|
303
|
+
if (plugin.hookCount && plugin.hookCount > 0) badges.push(`${plugin.hookCount} hook${plugin.hookCount === 1 ? '' : 's'}`)
|
|
304
|
+
if (plugin.hasUI) badges.push('UI')
|
|
305
|
+
if (plugin.providerCount && plugin.providerCount > 0) badges.push(`${plugin.providerCount} provider${plugin.providerCount === 1 ? '' : 's'}`)
|
|
306
|
+
if (plugin.connectorCount && plugin.connectorCount > 0) badges.push(`${plugin.connectorCount} connector${plugin.connectorCount === 1 ? '' : 's'}`)
|
|
307
|
+
return badges
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// --- Installed plugins grid ---
|
|
311
|
+
|
|
312
|
+
function InstalledGrid({ plugins, allowDelete, search, agents, onEdit, onToggle, onDelete, onNavigateToAgent, emptyMessage, emptyAction }: {
|
|
313
|
+
plugins: PluginMeta[]
|
|
314
|
+
allowDelete: boolean
|
|
315
|
+
search: string
|
|
316
|
+
agents: Record<string, Agent>
|
|
317
|
+
onEdit: (filename: string) => void
|
|
318
|
+
onToggle: (e: React.MouseEvent, filename: string, enabled: boolean) => void
|
|
319
|
+
onDelete: (e: React.MouseEvent, filename: string, name: string) => void
|
|
320
|
+
onNavigateToAgent: (agentId: string) => void
|
|
321
|
+
emptyMessage: string
|
|
322
|
+
emptyAction?: React.ReactNode
|
|
323
|
+
}) {
|
|
324
|
+
if (plugins.length === 0) {
|
|
325
|
+
return (
|
|
326
|
+
<div className="text-center py-16">
|
|
327
|
+
<div className="inline-flex items-center justify-center w-12 h-12 rounded-full bg-white/[0.03] mb-3">
|
|
328
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="text-text-3/30">
|
|
329
|
+
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" strokeLinecap="round" strokeLinejoin="round" />
|
|
330
|
+
</svg>
|
|
331
|
+
</div>
|
|
332
|
+
<p className="text-[13px] text-text-3/50">{emptyMessage}</p>
|
|
333
|
+
{emptyAction}
|
|
334
|
+
</div>
|
|
335
|
+
)
|
|
141
336
|
}
|
|
142
337
|
|
|
143
|
-
|
|
338
|
+
// Group enabled first, then disabled
|
|
339
|
+
const enabled = plugins.filter((p) => p.enabled)
|
|
340
|
+
const disabled = plugins.filter((p) => !p.enabled)
|
|
341
|
+
const sorted = [...enabled, ...disabled]
|
|
342
|
+
|
|
343
|
+
return (
|
|
344
|
+
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3">
|
|
345
|
+
{sorted.map((plugin) => (
|
|
346
|
+
<PluginCard
|
|
347
|
+
key={plugin.filename}
|
|
348
|
+
plugin={plugin}
|
|
349
|
+
allowDelete={allowDelete}
|
|
350
|
+
agents={agents}
|
|
351
|
+
onEdit={onEdit}
|
|
352
|
+
onToggle={onToggle}
|
|
353
|
+
onDelete={onDelete}
|
|
354
|
+
onNavigateToAgent={onNavigateToAgent}
|
|
355
|
+
highlight={search}
|
|
356
|
+
/>
|
|
357
|
+
))}
|
|
358
|
+
</div>
|
|
359
|
+
)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// --- Plugin card ---
|
|
363
|
+
|
|
364
|
+
function PluginCard({ plugin, allowDelete, agents, onEdit, onToggle, onDelete, onNavigateToAgent, highlight }: {
|
|
365
|
+
plugin: PluginMeta
|
|
366
|
+
allowDelete: boolean
|
|
367
|
+
agents: Record<string, Agent>
|
|
368
|
+
onEdit: (filename: string) => void
|
|
369
|
+
onToggle: (e: React.MouseEvent, filename: string, enabled: boolean) => void
|
|
370
|
+
onDelete: (e: React.MouseEvent, filename: string, name: string) => void
|
|
371
|
+
onNavigateToAgent: (agentId: string) => void
|
|
372
|
+
highlight: string
|
|
373
|
+
}) {
|
|
374
|
+
const badges = pluginCapabilityBadges(plugin)
|
|
375
|
+
const agent = plugin.createdByAgentId ? agents[plugin.createdByAgentId] : null
|
|
376
|
+
|
|
377
|
+
return (
|
|
144
378
|
<div
|
|
145
|
-
key={plugin.filename}
|
|
146
379
|
role="button"
|
|
147
380
|
tabIndex={0}
|
|
148
|
-
onClick={() =>
|
|
149
|
-
onKeyDown={(e) => {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
className="w-full text-left p-4 rounded-[14px] border border-white/[0.06] bg-surface hover:bg-surface-2 transition-all cursor-pointer"
|
|
381
|
+
onClick={() => onEdit(plugin.filename)}
|
|
382
|
+
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onEdit(plugin.filename) } }}
|
|
383
|
+
className={`group relative text-left p-4 rounded-[14px] border transition-all cursor-pointer
|
|
384
|
+
${plugin.enabled
|
|
385
|
+
? 'border-white/[0.06] bg-surface hover:bg-surface-2 hover:border-white/[0.1]'
|
|
386
|
+
: 'border-white/[0.03] bg-surface/50 hover:bg-surface hover:border-white/[0.06] opacity-70 hover:opacity-100'
|
|
387
|
+
}`}
|
|
156
388
|
>
|
|
157
|
-
|
|
389
|
+
{/* Top row: name + toggle */}
|
|
390
|
+
<div className="flex items-center justify-between gap-2 mb-1.5">
|
|
158
391
|
<div className="flex items-center gap-2 min-w-0">
|
|
159
|
-
{
|
|
392
|
+
{agent && (
|
|
160
393
|
<button
|
|
161
394
|
type="button"
|
|
162
|
-
title={`Created by ${
|
|
163
|
-
onClick={(e) => { e.stopPropagation();
|
|
395
|
+
title={`Created by ${agent.name}`}
|
|
396
|
+
onClick={(e) => { e.stopPropagation(); onNavigateToAgent(plugin.createdByAgentId!) }}
|
|
164
397
|
className="shrink-0 rounded-full hover:ring-2 hover:ring-accent-bright/40 transition-all cursor-pointer bg-transparent border-none p-0"
|
|
165
398
|
>
|
|
166
399
|
<AgentAvatar
|
|
167
|
-
seed={
|
|
168
|
-
avatarUrl={
|
|
169
|
-
name={
|
|
400
|
+
seed={agent.avatarSeed || null}
|
|
401
|
+
avatarUrl={agent.avatarUrl}
|
|
402
|
+
name={agent.name || 'Agent'}
|
|
170
403
|
size={20}
|
|
171
404
|
/>
|
|
172
405
|
</button>
|
|
173
406
|
)}
|
|
174
|
-
<span className="font-display text-[14px] font-600 text-text truncate">
|
|
407
|
+
<span className="font-display text-[14px] font-600 text-text truncate">
|
|
408
|
+
<HighlightText text={plugin.name} highlight={highlight} />
|
|
409
|
+
</span>
|
|
410
|
+
{plugin.version && (
|
|
411
|
+
<span className="text-[10px] font-mono text-text-3/40 shrink-0">v{plugin.version}</span>
|
|
412
|
+
)}
|
|
175
413
|
</div>
|
|
176
|
-
<div className="flex items-center gap-2 shrink-0
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
{
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
</svg>
|
|
196
|
-
</button>
|
|
197
|
-
)}
|
|
198
|
-
</>
|
|
199
|
-
) : (
|
|
200
|
-
<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]'}`}>
|
|
201
|
-
{plugin.enabled ? 'Enabled' : 'Disabled'}
|
|
202
|
-
</span>
|
|
414
|
+
<div className="flex items-center gap-2 shrink-0">
|
|
415
|
+
<div
|
|
416
|
+
onClick={(e) => onToggle(e, plugin.filename, plugin.enabled)}
|
|
417
|
+
className={`w-9 h-5 rounded-full transition-all relative cursor-pointer shrink-0
|
|
418
|
+
${plugin.enabled ? 'bg-accent-bright' : 'bg-white/[0.08]'}`}
|
|
419
|
+
>
|
|
420
|
+
<div className={`absolute top-0.5 w-4 h-4 rounded-full bg-white transition-all
|
|
421
|
+
${plugin.enabled ? 'left-[18px]' : 'left-0.5'}`} />
|
|
422
|
+
</div>
|
|
423
|
+
{allowDelete && (
|
|
424
|
+
<button
|
|
425
|
+
onClick={(e) => onDelete(e, plugin.filename, plugin.name)}
|
|
426
|
+
className="text-text-3/30 hover:text-red-400 transition-colors p-0.5 opacity-0 group-hover:opacity-100"
|
|
427
|
+
title="Delete"
|
|
428
|
+
>
|
|
429
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
430
|
+
<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" />
|
|
431
|
+
</svg>
|
|
432
|
+
</button>
|
|
203
433
|
)}
|
|
204
434
|
</div>
|
|
205
435
|
</div>
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
<
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
{
|
|
215
|
-
<span key={badge} className="text-[10px] font-600 px-1.5 py-0.5 rounded-full text-text-3/
|
|
436
|
+
|
|
437
|
+
{/* Description */}
|
|
438
|
+
<p className="text-[12px] text-text-3/60 leading-relaxed line-clamp-2 mb-2.5">
|
|
439
|
+
{pluginDescription(plugin)}
|
|
440
|
+
</p>
|
|
441
|
+
|
|
442
|
+
{/* Badges */}
|
|
443
|
+
<div className="flex items-center gap-1.5 flex-wrap">
|
|
444
|
+
{badges.map((badge) => (
|
|
445
|
+
<span key={badge} className="text-[10px] font-600 px-1.5 py-0.5 rounded-full text-text-3/70 bg-white/[0.04]">
|
|
216
446
|
{badge}
|
|
217
447
|
</span>
|
|
218
448
|
))}
|
|
449
|
+
{plugin.author && (
|
|
450
|
+
<span className="text-[10px] text-text-3/40 ml-auto">
|
|
451
|
+
{plugin.author}
|
|
452
|
+
</span>
|
|
453
|
+
)}
|
|
219
454
|
</div>
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
)}
|
|
455
|
+
|
|
456
|
+
{/* Failure warning */}
|
|
223
457
|
{plugin.autoDisabled && (
|
|
224
|
-
<p className="mt-
|
|
458
|
+
<p className="mt-2 text-[11px] text-amber-400/90 line-clamp-2">
|
|
225
459
|
Auto-disabled after {plugin.failureCount ?? 0} failures
|
|
226
460
|
{plugin.lastFailureStage ? ` (${plugin.lastFailureStage})` : ''}.
|
|
227
461
|
{plugin.lastFailureError ? ` ${plugin.lastFailureError}` : ''}
|
|
@@ -229,186 +463,173 @@ export function PluginList({ inSidebar }: { inSidebar?: boolean }) {
|
|
|
229
463
|
)}
|
|
230
464
|
</div>
|
|
231
465
|
)
|
|
466
|
+
}
|
|
232
467
|
|
|
233
|
-
|
|
234
|
-
const renderMarketplace = () => {
|
|
235
|
-
if (mpLoading) return <p className="text-[12px] text-text-3/70 py-8 text-center">Loading marketplace...</p>
|
|
236
|
-
if (marketplace.length === 0) return <p className="text-[12px] text-text-3/70 py-8 text-center">No plugins available</p>
|
|
468
|
+
// --- Sidebar card (compact) ---
|
|
237
469
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
470
|
+
function SidebarPluginCard({ plugin, onEdit }: { plugin: PluginMeta; onEdit: (filename: string) => void }) {
|
|
471
|
+
return (
|
|
472
|
+
<div
|
|
473
|
+
role="button"
|
|
474
|
+
tabIndex={0}
|
|
475
|
+
onClick={() => onEdit(plugin.filename)}
|
|
476
|
+
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onEdit(plugin.filename) } }}
|
|
477
|
+
className="w-full text-left p-3 rounded-[12px] border border-white/[0.06] bg-surface hover:bg-surface-2 transition-all cursor-pointer"
|
|
478
|
+
>
|
|
479
|
+
<div className="flex items-center justify-between mb-0.5">
|
|
480
|
+
<span className="font-display text-[13px] font-600 text-text truncate">{plugin.name}</span>
|
|
481
|
+
<span className={`text-[10px] font-600 px-1.5 py-0.5 rounded-full ${
|
|
482
|
+
plugin.enabled ? 'text-emerald-400 bg-emerald-400/10' : 'text-text-3/50 bg-white/[0.04]'
|
|
483
|
+
}`}>
|
|
484
|
+
{plugin.enabled ? 'On' : 'Off'}
|
|
485
|
+
</span>
|
|
486
|
+
</div>
|
|
487
|
+
<p className="text-[11px] text-text-3/50 line-clamp-1">{pluginDescription(plugin)}</p>
|
|
488
|
+
</div>
|
|
489
|
+
)
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// --- Highlight text helper ---
|
|
493
|
+
|
|
494
|
+
function HighlightText({ text, highlight }: { text: string; highlight: string }) {
|
|
495
|
+
if (!highlight.trim()) return <>{text}</>
|
|
496
|
+
const idx = text.toLowerCase().indexOf(highlight.toLowerCase())
|
|
497
|
+
if (idx === -1) return <>{text}</>
|
|
498
|
+
return (
|
|
499
|
+
<>
|
|
500
|
+
{text.slice(0, idx)}
|
|
501
|
+
<span className="text-accent-bright">{text.slice(idx, idx + highlight.length)}</span>
|
|
502
|
+
{text.slice(idx + highlight.length)}
|
|
503
|
+
</>
|
|
504
|
+
)
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// --- Marketplace tab ---
|
|
508
|
+
|
|
509
|
+
function MarketplaceTab({ marketplace, loading, installing, installedFilenames, search, activeTag, setActiveTag, sort, setSort, onInstall }: {
|
|
510
|
+
marketplace: MarketplacePlugin[]
|
|
511
|
+
loading: boolean
|
|
512
|
+
installing: string | null
|
|
513
|
+
installedFilenames: Set<string>
|
|
514
|
+
search: string
|
|
515
|
+
activeTag: string | null
|
|
516
|
+
setActiveTag: (v: string | null) => void
|
|
517
|
+
sort: 'name' | 'downloads'
|
|
518
|
+
setSort: (v: 'name' | 'downloads') => void
|
|
519
|
+
onInstall: (p: MarketplacePlugin) => void
|
|
520
|
+
}) {
|
|
521
|
+
if (loading) return <p className="text-[12px] text-text-3/70 py-8 text-center">Loading marketplace...</p>
|
|
247
522
|
|
|
523
|
+
if (marketplace.length === 0) {
|
|
248
524
|
return (
|
|
249
|
-
<div className="
|
|
250
|
-
<
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
style={{ fontFamily: 'inherit' }}
|
|
256
|
-
/>
|
|
257
|
-
<div className="flex items-center gap-1.5 flex-wrap">
|
|
258
|
-
<button
|
|
259
|
-
onClick={() => setActiveTag(null)}
|
|
260
|
-
className={`px-2 py-1 rounded-[6px] text-[10px] font-600 cursor-pointer transition-all border-none ${
|
|
261
|
-
!activeTag ? 'bg-accent-soft text-accent-bright' : 'bg-white/[0.03] text-text-3/60 hover:text-text-3'
|
|
262
|
-
}`}
|
|
263
|
-
>
|
|
264
|
-
All
|
|
265
|
-
</button>
|
|
266
|
-
{allTags.map((t) => (
|
|
267
|
-
<button
|
|
268
|
-
key={t}
|
|
269
|
-
onClick={() => setActiveTag(activeTag === t ? null : t)}
|
|
270
|
-
className={`px-2 py-1 rounded-[6px] text-[10px] font-600 cursor-pointer transition-all border-none ${
|
|
271
|
-
activeTag === t ? 'bg-accent-soft text-accent-bright' : 'bg-white/[0.03] text-text-3/60 hover:text-text-3'
|
|
272
|
-
}`}
|
|
273
|
-
>
|
|
274
|
-
{t}
|
|
275
|
-
</button>
|
|
276
|
-
))}
|
|
277
|
-
<div className="flex-1" />
|
|
278
|
-
<select
|
|
279
|
-
value={sort}
|
|
280
|
-
onChange={(e) => setSort(e.target.value as 'name' | 'downloads')}
|
|
281
|
-
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"
|
|
282
|
-
style={{ fontFamily: 'inherit' }}
|
|
283
|
-
>
|
|
284
|
-
<option value="downloads">Popular</option>
|
|
285
|
-
<option value="name">A-Z</option>
|
|
286
|
-
</select>
|
|
525
|
+
<div className="text-center py-16">
|
|
526
|
+
<div className="inline-flex items-center justify-center w-12 h-12 rounded-full bg-white/[0.03] mb-3">
|
|
527
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="text-text-3/30">
|
|
528
|
+
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" strokeLinecap="round" strokeLinejoin="round" />
|
|
529
|
+
<polyline points="9,22 9,12 15,12 15,22" strokeLinecap="round" strokeLinejoin="round" />
|
|
530
|
+
</svg>
|
|
287
531
|
</div>
|
|
288
|
-
|
|
289
|
-
<p className="text-[12px] text-text-3/50 text-center py-4">No plugins match your search</p>
|
|
290
|
-
) : (
|
|
291
|
-
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3">
|
|
292
|
-
{filtered.map((p) => {
|
|
293
|
-
const isInstalled = installedFilenames.has(`${p.id}.js`)
|
|
294
|
-
return (
|
|
295
|
-
<div key={p.id} className="py-3.5 px-4 rounded-[14px] bg-surface border border-white/[0.06]">
|
|
296
|
-
<div className="flex items-start gap-3">
|
|
297
|
-
<div className="flex-1 min-w-0">
|
|
298
|
-
<div className="flex items-center gap-2">
|
|
299
|
-
<span className="text-[14px] font-600 text-text">{p.name}</span>
|
|
300
|
-
<span className="text-[10px] font-mono text-text-3/70">v{p.version}</span>
|
|
301
|
-
{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>}
|
|
302
|
-
</div>
|
|
303
|
-
<div className="text-[11px] text-text-3/60 mt-1 line-clamp-2">{p.description}</div>
|
|
304
|
-
<div className="flex items-center gap-2 mt-2">
|
|
305
|
-
<span className="text-[10px] text-text-3/70">by {p.author}</span>
|
|
306
|
-
<span className="text-[10px] text-text-3/50">·</span>
|
|
307
|
-
{(p.tags ?? []).slice(0, 3).map((t) => (
|
|
308
|
-
<button
|
|
309
|
-
key={t}
|
|
310
|
-
onClick={() => setActiveTag(activeTag === t ? null : t)}
|
|
311
|
-
className={`text-[9px] font-600 px-1.5 py-0.5 rounded-full cursor-pointer transition-all border-none ${
|
|
312
|
-
activeTag === t ? 'text-accent-bright bg-accent-soft' : 'text-text-3/50 bg-white/[0.04] hover:text-text-3'
|
|
313
|
-
}`}
|
|
314
|
-
>
|
|
315
|
-
{t}
|
|
316
|
-
</button>
|
|
317
|
-
))}
|
|
318
|
-
</div>
|
|
319
|
-
</div>
|
|
320
|
-
<button
|
|
321
|
-
onClick={() => !isInstalled && installFromMarketplace(p)}
|
|
322
|
-
disabled={isInstalled || installing === p.id}
|
|
323
|
-
className={`shrink-0 py-2 px-4 rounded-[10px] text-[12px] font-600 transition-all cursor-pointer
|
|
324
|
-
${isInstalled
|
|
325
|
-
? 'bg-white/[0.04] text-text-3/70 cursor-default'
|
|
326
|
-
: installing === p.id
|
|
327
|
-
? 'bg-accent-soft text-accent-bright animate-pulse'
|
|
328
|
-
: 'bg-accent-soft text-accent-bright hover:bg-accent-soft/80 border border-accent-bright/20'}`}
|
|
329
|
-
style={{ fontFamily: 'inherit' }}
|
|
330
|
-
>
|
|
331
|
-
{isInstalled ? 'Installed' : installing === p.id ? 'Installing...' : 'Install'}
|
|
332
|
-
</button>
|
|
333
|
-
</div>
|
|
334
|
-
</div>
|
|
335
|
-
)
|
|
336
|
-
})}
|
|
337
|
-
</div>
|
|
338
|
-
)}
|
|
532
|
+
<p className="text-[13px] text-text-3/50">No plugins available in the marketplace</p>
|
|
339
533
|
</div>
|
|
340
534
|
)
|
|
341
535
|
}
|
|
342
536
|
|
|
537
|
+
const allTags = Array.from(new Set(marketplace.flatMap((p) => p.tags ?? []))).sort()
|
|
538
|
+
const q = search.toLowerCase()
|
|
539
|
+
const filtered = marketplace
|
|
540
|
+
.filter((p) => {
|
|
541
|
+
if (q && !p.name.toLowerCase().includes(q) && !p.description.toLowerCase().includes(q) && !(p.tags ?? []).some((t) => t.toLowerCase().includes(q))) return false
|
|
542
|
+
if (activeTag && !(p.tags ?? []).includes(activeTag)) return false
|
|
543
|
+
return true
|
|
544
|
+
})
|
|
545
|
+
.sort((a, b) => sort === 'downloads' ? (b.downloads ?? 0) - (a.downloads ?? 0) : a.name.localeCompare(b.name))
|
|
546
|
+
|
|
343
547
|
return (
|
|
344
|
-
<div className=
|
|
345
|
-
{/*
|
|
346
|
-
|
|
347
|
-
<
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
548
|
+
<div className="space-y-3">
|
|
549
|
+
{/* Tags + Sort */}
|
|
550
|
+
<div className="flex items-center gap-1.5 flex-wrap">
|
|
551
|
+
<button
|
|
552
|
+
onClick={() => setActiveTag(null)}
|
|
553
|
+
className={`px-2 py-1 rounded-[6px] text-[10px] font-600 cursor-pointer transition-all border-none ${
|
|
554
|
+
!activeTag ? 'bg-accent-soft text-accent-bright' : 'bg-white/[0.03] text-text-3/60 hover:text-text-3'
|
|
555
|
+
}`}
|
|
556
|
+
>
|
|
557
|
+
All
|
|
558
|
+
</button>
|
|
559
|
+
{allTags.map((t) => (
|
|
560
|
+
<button
|
|
561
|
+
key={t}
|
|
562
|
+
onClick={() => setActiveTag(activeTag === t ? null : t)}
|
|
563
|
+
className={`px-2 py-1 rounded-[6px] text-[10px] font-600 cursor-pointer transition-all border-none ${
|
|
564
|
+
activeTag === t ? 'bg-accent-soft text-accent-bright' : 'bg-white/[0.03] text-text-3/60 hover:text-text-3'
|
|
565
|
+
}`}
|
|
566
|
+
>
|
|
567
|
+
{t}
|
|
353
568
|
</button>
|
|
354
|
-
|
|
355
|
-
|
|
569
|
+
))}
|
|
570
|
+
<div className="flex-1" />
|
|
571
|
+
<select
|
|
572
|
+
value={sort}
|
|
573
|
+
onChange={(e) => setSort(e.target.value as 'name' | 'downloads')}
|
|
574
|
+
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"
|
|
575
|
+
style={{ fontFamily: 'inherit' }}
|
|
576
|
+
>
|
|
577
|
+
<option value="downloads">Popular</option>
|
|
578
|
+
<option value="name">A-Z</option>
|
|
579
|
+
</select>
|
|
580
|
+
</div>
|
|
356
581
|
|
|
357
|
-
{
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
{extensionPlugins.length > 0 && (
|
|
389
|
-
<section>
|
|
390
|
-
<div className="mb-3 px-1">
|
|
391
|
-
<h3 className="text-[13px] font-700 text-text-2">Extensions</h3>
|
|
392
|
-
<p className="text-[12px] text-text-3/60 mt-0.5">Marketplace and custom plugins installed by your team.</p>
|
|
393
|
-
</div>
|
|
394
|
-
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3">
|
|
395
|
-
{extensionPlugins.map((plugin) => renderInstalledPlugin(plugin, true))}
|
|
582
|
+
{filtered.length === 0 ? (
|
|
583
|
+
<p className="text-[12px] text-text-3/50 text-center py-4">No plugins match your search</p>
|
|
584
|
+
) : (
|
|
585
|
+
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3">
|
|
586
|
+
{filtered.map((p) => {
|
|
587
|
+
const isInstalled = installedFilenames.has(`${p.id}.js`)
|
|
588
|
+
return (
|
|
589
|
+
<div key={p.id} className="py-3.5 px-4 rounded-[14px] bg-surface border border-white/[0.06]">
|
|
590
|
+
<div className="flex items-start gap-3">
|
|
591
|
+
<div className="flex-1 min-w-0">
|
|
592
|
+
<div className="flex items-center gap-2">
|
|
593
|
+
<span className="text-[14px] font-600 text-text">{p.name}</span>
|
|
594
|
+
<span className="text-[10px] font-mono text-text-3/70">v{p.version}</span>
|
|
595
|
+
{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>}
|
|
596
|
+
</div>
|
|
597
|
+
<div className="text-[11px] text-text-3/60 mt-1 line-clamp-2">{p.description}</div>
|
|
598
|
+
<div className="flex items-center gap-2 mt-2">
|
|
599
|
+
<span className="text-[10px] text-text-3/70">by {p.author}</span>
|
|
600
|
+
<span className="text-[10px] text-text-3/50">·</span>
|
|
601
|
+
{(p.tags ?? []).slice(0, 3).map((t) => (
|
|
602
|
+
<button
|
|
603
|
+
key={t}
|
|
604
|
+
onClick={() => setActiveTag(activeTag === t ? null : t)}
|
|
605
|
+
className={`text-[9px] font-600 px-1.5 py-0.5 rounded-full cursor-pointer transition-all border-none ${
|
|
606
|
+
activeTag === t ? 'text-accent-bright bg-accent-soft' : 'text-text-3/50 bg-white/[0.04] hover:text-text-3'
|
|
607
|
+
}`}
|
|
608
|
+
>
|
|
609
|
+
{t}
|
|
610
|
+
</button>
|
|
611
|
+
))}
|
|
612
|
+
</div>
|
|
396
613
|
</div>
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
614
|
+
<button
|
|
615
|
+
onClick={() => !isInstalled && onInstall(p)}
|
|
616
|
+
disabled={isInstalled || installing === p.id}
|
|
617
|
+
className={`shrink-0 py-2 px-4 rounded-[10px] text-[12px] font-600 transition-all cursor-pointer
|
|
618
|
+
${isInstalled
|
|
619
|
+
? 'bg-white/[0.04] text-text-3/70 cursor-default'
|
|
620
|
+
: installing === p.id
|
|
621
|
+
? 'bg-accent-soft text-accent-bright animate-pulse'
|
|
622
|
+
: 'bg-accent-soft text-accent-bright hover:bg-accent-soft/80 border border-accent-bright/20'}`}
|
|
623
|
+
style={{ fontFamily: 'inherit' }}
|
|
624
|
+
>
|
|
625
|
+
{isInstalled ? 'Installed' : installing === p.id ? 'Installing...' : 'Install'}
|
|
626
|
+
</button>
|
|
627
|
+
</div>
|
|
628
|
+
</div>
|
|
629
|
+
)
|
|
630
|
+
})}
|
|
631
|
+
</div>
|
|
402
632
|
)}
|
|
403
|
-
<ConfirmDialog
|
|
404
|
-
open={!!confirmDelete}
|
|
405
|
-
title="Delete Plugin"
|
|
406
|
-
message={confirmDelete ? `Delete "${confirmDelete.name}"? This cannot be undone.` : ''}
|
|
407
|
-
confirmLabel={deleting ? 'Deleting...' : 'Delete'}
|
|
408
|
-
danger
|
|
409
|
-
onConfirm={() => { void handleDeleteConfirm() }}
|
|
410
|
-
onCancel={() => { if (!deleting) setConfirmDelete(null) }}
|
|
411
|
-
/>
|
|
412
633
|
</div>
|
|
413
634
|
)
|
|
414
635
|
}
|