@swarmclawai/swarmclaw 0.6.8 → 0.7.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 +70 -45
- package/next.config.ts +31 -6
- package/package.json +3 -2
- package/src/app/api/agents/[id]/thread/route.ts +1 -0
- package/src/app/api/agents/route.ts +18 -5
- package/src/app/api/approvals/route.ts +22 -0
- package/src/app/api/clawhub/install/route.ts +2 -2
- package/src/app/api/mcp-servers/[id]/conformance/route.ts +26 -0
- package/src/app/api/mcp-servers/[id]/invoke/route.ts +81 -0
- package/src/app/api/memory/route.ts +36 -5
- package/src/app/api/notifications/route.ts +3 -0
- package/src/app/api/plugins/install/route.ts +57 -5
- package/src/app/api/plugins/marketplace/route.ts +73 -22
- package/src/app/api/plugins/route.ts +61 -1
- package/src/app/api/plugins/ui/route.ts +34 -0
- package/src/app/api/settings/route.ts +62 -0
- package/src/app/api/setup/doctor/route.ts +22 -5
- package/src/app/api/tasks/[id]/approve/route.ts +4 -3
- package/src/app/api/tasks/[id]/route.ts +11 -3
- package/src/app/api/tasks/route.ts +8 -2
- package/src/app/globals.css +27 -0
- package/src/app/page.tsx +10 -5
- package/src/cli/index.js +13 -0
- package/src/components/activity/activity-feed.tsx +9 -2
- package/src/components/agents/agent-avatar.tsx +5 -1
- package/src/components/agents/agent-card.tsx +55 -9
- package/src/components/agents/agent-sheet.tsx +86 -29
- package/src/components/agents/inspector-panel.tsx +1 -1
- package/src/components/auth/access-key-gate.tsx +63 -54
- package/src/components/auth/user-picker.tsx +37 -32
- package/src/components/chat/chat-area.tsx +11 -0
- package/src/components/chat/chat-header.tsx +69 -25
- package/src/components/chat/chat-tool-toggles.tsx +2 -2
- package/src/components/chat/code-block.tsx +3 -1
- package/src/components/chat/exec-approval-card.tsx +8 -1
- package/src/components/chat/message-bubble.tsx +164 -4
- package/src/components/chat/message-list.tsx +30 -4
- package/src/components/chat/session-approval-card.tsx +80 -0
- package/src/components/chat/streaming-bubble.tsx +6 -5
- package/src/components/chat/thinking-indicator.tsx +48 -12
- package/src/components/chat/tool-request-banner.tsx +39 -20
- package/src/components/chatrooms/chatroom-list.tsx +11 -4
- package/src/components/chatrooms/chatroom-sheet.tsx +7 -2
- package/src/components/connectors/connector-list.tsx +33 -11
- package/src/components/connectors/connector-sheet.tsx +29 -6
- package/src/components/home/home-view.tsx +20 -14
- package/src/components/input/chat-input.tsx +22 -1
- package/src/components/knowledge/knowledge-list.tsx +17 -18
- package/src/components/knowledge/knowledge-sheet.tsx +9 -5
- package/src/components/layout/app-layout.tsx +73 -21
- package/src/components/mcp-servers/mcp-server-list.tsx +352 -50
- package/src/components/mcp-servers/mcp-server-sheet.tsx +25 -9
- package/src/components/memory/memory-list.tsx +20 -13
- package/src/components/plugins/plugin-list.tsx +213 -59
- package/src/components/plugins/plugin-sheet.tsx +119 -24
- package/src/components/projects/project-list.tsx +17 -9
- package/src/components/providers/provider-list.tsx +21 -6
- package/src/components/providers/provider-sheet.tsx +42 -25
- package/src/components/runs/run-list.tsx +17 -13
- package/src/components/schedules/schedule-card.tsx +10 -3
- package/src/components/schedules/schedule-list.tsx +2 -2
- package/src/components/schedules/schedule-sheet.tsx +19 -7
- package/src/components/secrets/secret-sheet.tsx +7 -2
- package/src/components/secrets/secrets-list.tsx +18 -5
- package/src/components/sessions/new-session-sheet.tsx +183 -376
- package/src/components/sessions/session-card.tsx +10 -2
- package/src/components/settings/gateway-connection-panel.tsx +9 -8
- package/src/components/shared/command-palette.tsx +13 -5
- package/src/components/shared/empty-state.tsx +20 -8
- package/src/components/shared/notification-center.tsx +134 -86
- package/src/components/shared/profile-sheet.tsx +4 -0
- package/src/components/shared/settings/plugin-manager.tsx +360 -135
- package/src/components/shared/settings/section-capability-policy.tsx +3 -3
- package/src/components/shared/settings/section-runtime-loop.tsx +144 -0
- package/src/components/skills/clawhub-browser.tsx +1 -0
- package/src/components/skills/skill-list.tsx +31 -12
- package/src/components/skills/skill-sheet.tsx +20 -7
- package/src/components/tasks/approvals-panel.tsx +170 -66
- package/src/components/tasks/task-board.tsx +20 -12
- package/src/components/tasks/task-card.tsx +21 -7
- package/src/components/tasks/task-column.tsx +4 -3
- package/src/components/tasks/task-list.tsx +1 -1
- package/src/components/tasks/task-sheet.tsx +130 -1
- package/src/components/ui/dialog.tsx +1 -0
- package/src/components/ui/sheet.tsx +1 -0
- package/src/components/usage/metrics-dashboard.tsx +66 -64
- package/src/components/wallets/wallet-panel.tsx +65 -41
- package/src/components/wallets/wallet-section.tsx +9 -3
- package/src/components/webhooks/webhook-list.tsx +21 -12
- package/src/components/webhooks/webhook-sheet.tsx +13 -3
- package/src/lib/approval-display.test.ts +45 -0
- package/src/lib/approval-display.ts +62 -0
- package/src/lib/clipboard.ts +38 -0
- package/src/lib/memory.ts +8 -0
- package/src/lib/providers/claude-cli.ts +5 -3
- package/src/lib/providers/index.ts +67 -21
- package/src/lib/runtime-loop.ts +3 -2
- package/src/lib/server/approvals.ts +150 -0
- package/src/lib/server/chat-execution.ts +223 -62
- package/src/lib/server/clawhub-client.ts +82 -6
- package/src/lib/server/connectors/manager.ts +27 -1
- package/src/lib/server/cost.test.ts +73 -0
- package/src/lib/server/cost.ts +165 -34
- package/src/lib/server/daemon-state.ts +42 -0
- package/src/lib/server/data-dir.ts +18 -1
- package/src/lib/server/integrity-monitor.ts +208 -0
- package/src/lib/server/llm-response-cache.test.ts +102 -0
- package/src/lib/server/llm-response-cache.ts +227 -0
- package/src/lib/server/main-agent-loop.ts +1 -1
- package/src/lib/server/main-session.ts +6 -3
- package/src/lib/server/mcp-conformance.test.ts +18 -0
- package/src/lib/server/mcp-conformance.ts +233 -0
- package/src/lib/server/memory-db.ts +180 -17
- package/src/lib/server/memory-retrieval.test.ts +56 -0
- package/src/lib/server/orchestrator-lg.ts +4 -1
- package/src/lib/server/orchestrator.ts +4 -3
- package/src/lib/server/plugins.ts +650 -142
- package/src/lib/server/process-manager.ts +18 -0
- package/src/lib/server/queue.ts +253 -11
- package/src/lib/server/runtime-settings.ts +9 -0
- package/src/lib/server/session-run-manager.test.ts +23 -0
- package/src/lib/server/session-run-manager.ts +11 -1
- package/src/lib/server/session-tools/canvas.ts +85 -50
- package/src/lib/server/session-tools/chatroom.ts +130 -127
- package/src/lib/server/session-tools/connector.ts +233 -454
- package/src/lib/server/session-tools/context-mgmt.ts +87 -105
- package/src/lib/server/session-tools/crud.ts +84 -7
- package/src/lib/server/session-tools/delegate.ts +351 -752
- package/src/lib/server/session-tools/discovery.ts +198 -0
- package/src/lib/server/session-tools/edit_file.ts +82 -0
- package/src/lib/server/session-tools/file-send.test.ts +39 -0
- package/src/lib/server/session-tools/file.ts +257 -425
- package/src/lib/server/session-tools/git.ts +87 -47
- package/src/lib/server/session-tools/http.ts +85 -33
- package/src/lib/server/session-tools/index.ts +205 -160
- package/src/lib/server/session-tools/memory.ts +152 -265
- package/src/lib/server/session-tools/monitor.ts +126 -0
- package/src/lib/server/session-tools/normalize-tool-args.test.ts +61 -0
- package/src/lib/server/session-tools/normalize-tool-args.ts +48 -0
- package/src/lib/server/session-tools/openclaw-nodes.ts +82 -99
- package/src/lib/server/session-tools/openclaw-workspace.ts +103 -93
- package/src/lib/server/session-tools/platform.ts +86 -0
- package/src/lib/server/session-tools/plugin-creator.ts +239 -0
- package/src/lib/server/session-tools/sample-ui.ts +97 -0
- package/src/lib/server/session-tools/sandbox.ts +175 -148
- package/src/lib/server/session-tools/schedule.ts +66 -31
- package/src/lib/server/session-tools/session-info.ts +104 -410
- package/src/lib/server/session-tools/shell-normalize.test.ts +43 -0
- package/src/lib/server/session-tools/shell.ts +171 -143
- package/src/lib/server/session-tools/subagent.ts +77 -77
- package/src/lib/server/session-tools/wallet.ts +182 -106
- package/src/lib/server/session-tools/web.ts +179 -349
- package/src/lib/server/storage.ts +24 -0
- package/src/lib/server/stream-agent-chat.ts +301 -244
- package/src/lib/server/task-quality-gate.test.ts +44 -0
- package/src/lib/server/task-quality-gate.ts +67 -0
- package/src/lib/server/task-validation.test.ts +78 -0
- package/src/lib/server/task-validation.ts +67 -2
- package/src/lib/server/tool-aliases.ts +68 -0
- package/src/lib/server/tool-capability-policy.ts +23 -5
- package/src/lib/tasks.ts +7 -1
- package/src/lib/tool-definitions.ts +23 -23
- package/src/lib/validation/schemas.ts +12 -0
- package/src/lib/view-routes.ts +2 -24
- package/src/stores/use-app-store.ts +23 -1
- package/src/types/index.ts +121 -7
|
@@ -3,25 +3,46 @@
|
|
|
3
3
|
import { useEffect, useState, useCallback } from 'react'
|
|
4
4
|
import { useAppStore } from '@/stores/use-app-store'
|
|
5
5
|
import { api } from '@/lib/api-client'
|
|
6
|
-
import
|
|
6
|
+
import { toast } from 'sonner'
|
|
7
|
+
import type { MarketplacePlugin, PluginMeta } from '@/types'
|
|
8
|
+
import { AgentAvatar } from '@/components/agents/agent-avatar'
|
|
9
|
+
import { ConfirmDialog } from '@/components/shared/confirm-dialog'
|
|
7
10
|
|
|
8
11
|
export function PluginList({ inSidebar }: { inSidebar?: boolean }) {
|
|
9
12
|
const plugins = useAppStore((s) => s.plugins)
|
|
10
13
|
const loadPlugins = useAppStore((s) => s.loadPlugins)
|
|
11
14
|
const setPluginSheetOpen = useAppStore((s) => s.setPluginSheetOpen)
|
|
12
15
|
const setEditingPluginFilename = useAppStore((s) => s.setEditingPluginFilename)
|
|
16
|
+
const agents = useAppStore((s) => s.agents)
|
|
17
|
+
const sessions = useAppStore((s) => s.sessions)
|
|
18
|
+
const setCurrentSession = useAppStore((s) => s.setCurrentSession)
|
|
19
|
+
const setActiveView = useAppStore((s) => s.setActiveView)
|
|
20
|
+
|
|
21
|
+
const navigateToAgentChat = useCallback((agentId: string) => {
|
|
22
|
+
// Find the most recent chat for this agent
|
|
23
|
+
const agentSession = Object.values(sessions)
|
|
24
|
+
.filter((s) => s.agentId === agentId)
|
|
25
|
+
.sort((a, b) => (b.lastActiveAt ?? 0) - (a.lastActiveAt ?? 0))[0]
|
|
26
|
+
if (agentSession) {
|
|
27
|
+
setCurrentSession(agentSession.id)
|
|
28
|
+
setActiveView('agents')
|
|
29
|
+
}
|
|
30
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
31
|
+
}, [sessions])
|
|
13
32
|
|
|
14
33
|
const [tab, setTab] = useState<'installed' | 'marketplace'>('installed')
|
|
15
34
|
const [marketplace, setMarketplace] = useState<MarketplacePlugin[]>([])
|
|
16
35
|
const [mpLoading, setMpLoading] = useState(false)
|
|
17
36
|
const [installing, setInstalling] = useState<string | null>(null)
|
|
37
|
+
const [deleting, setDeleting] = useState(false)
|
|
38
|
+
const [confirmDelete, setConfirmDelete] = useState<{ filename: string; name: string } | null>(null)
|
|
18
39
|
const [search, setSearch] = useState('')
|
|
19
40
|
const [activeTag, setActiveTag] = useState<string | null>(null)
|
|
20
41
|
const [sort, setSort] = useState<'name' | 'downloads'>('downloads')
|
|
21
42
|
|
|
22
43
|
useEffect(() => {
|
|
23
|
-
loadPlugins()
|
|
24
|
-
}, [])
|
|
44
|
+
void loadPlugins()
|
|
45
|
+
}, [loadPlugins])
|
|
25
46
|
|
|
26
47
|
const loadMarketplace = useCallback(async () => {
|
|
27
48
|
setMpLoading(true)
|
|
@@ -33,10 +54,16 @@ export function PluginList({ inSidebar }: { inSidebar?: boolean }) {
|
|
|
33
54
|
}, [])
|
|
34
55
|
|
|
35
56
|
useEffect(() => {
|
|
36
|
-
if (
|
|
57
|
+
if (inSidebar || tab !== 'marketplace') return
|
|
58
|
+
const timer = setTimeout(() => {
|
|
59
|
+
void loadMarketplace()
|
|
60
|
+
}, 0)
|
|
61
|
+
return () => clearTimeout(timer)
|
|
37
62
|
}, [tab, inSidebar, loadMarketplace])
|
|
38
63
|
|
|
39
64
|
const pluginList = Object.values(plugins)
|
|
65
|
+
const corePlugins = pluginList.filter((plugin) => plugin.source === 'local')
|
|
66
|
+
const extensionPlugins = pluginList.filter((plugin) => plugin.source !== 'local')
|
|
40
67
|
|
|
41
68
|
const handleEdit = (filename: string) => {
|
|
42
69
|
setEditingPluginFilename(filename)
|
|
@@ -45,22 +72,46 @@ export function PluginList({ inSidebar }: { inSidebar?: boolean }) {
|
|
|
45
72
|
|
|
46
73
|
const handleToggle = async (e: React.MouseEvent, filename: string, enabled: boolean) => {
|
|
47
74
|
e.stopPropagation()
|
|
48
|
-
|
|
49
|
-
|
|
75
|
+
try {
|
|
76
|
+
await api('POST', '/plugins', { filename, enabled: !enabled })
|
|
77
|
+
toast.success(!enabled ? 'Plugin enabled' : 'Plugin disabled')
|
|
78
|
+
loadPlugins()
|
|
79
|
+
} catch (err: unknown) {
|
|
80
|
+
toast.error(err instanceof Error ? err.message : 'Failed to toggle plugin')
|
|
81
|
+
}
|
|
50
82
|
}
|
|
51
83
|
|
|
52
|
-
const
|
|
84
|
+
const handleDeleteClick = (e: React.MouseEvent, filename: string, name: string) => {
|
|
53
85
|
e.stopPropagation()
|
|
54
|
-
|
|
55
|
-
|
|
86
|
+
setConfirmDelete({ filename, name })
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const handleDeleteConfirm = async () => {
|
|
90
|
+
if (!confirmDelete) return
|
|
91
|
+
setDeleting(true)
|
|
92
|
+
try {
|
|
93
|
+
await api('DELETE', `/plugins?filename=${encodeURIComponent(confirmDelete.filename)}`)
|
|
94
|
+
toast.success('Plugin deleted')
|
|
95
|
+
await loadPlugins()
|
|
96
|
+
} catch (err: unknown) {
|
|
97
|
+
toast.error(err instanceof Error ? err.message : 'Delete failed')
|
|
98
|
+
} finally {
|
|
99
|
+
setDeleting(false)
|
|
100
|
+
setConfirmDelete(null)
|
|
101
|
+
}
|
|
56
102
|
}
|
|
57
103
|
|
|
58
104
|
const installFromMarketplace = async (p: MarketplacePlugin) => {
|
|
59
105
|
setInstalling(p.id)
|
|
106
|
+
const toastId = toast.loading(`Installing ${p.name}...`)
|
|
60
107
|
try {
|
|
61
|
-
|
|
108
|
+
const safeFilename = `${p.id.replace(/[^a-zA-Z0-9.-]/g, '_')}.js`
|
|
109
|
+
await api('POST', '/plugins/install', { url: p.url, filename: safeFilename })
|
|
62
110
|
await loadPlugins()
|
|
63
|
-
|
|
111
|
+
toast.success(`Installed ${p.name}`, { id: toastId })
|
|
112
|
+
} catch (err: unknown) {
|
|
113
|
+
toast.error(err instanceof Error ? err.message : 'Install failed', { id: toastId })
|
|
114
|
+
}
|
|
64
115
|
setInstalling(null)
|
|
65
116
|
}
|
|
66
117
|
|
|
@@ -72,20 +123,127 @@ export function PluginList({ inSidebar }: { inSidebar?: boolean }) {
|
|
|
72
123
|
? 'bg-accent-soft border-accent-bright/25 text-accent-bright'
|
|
73
124
|
: 'bg-transparent border-transparent text-text-3 hover:text-text-2'}`
|
|
74
125
|
|
|
126
|
+
const pluginDescription = (plugin: PluginMeta): string => {
|
|
127
|
+
const raw = (plugin.description || '').trim()
|
|
128
|
+
if (raw) return raw
|
|
129
|
+
const sourceLabel = plugin.source === 'local' ? 'core plugin' : 'installed plugin'
|
|
130
|
+
return `No description provided. Click to view metadata and controls for this ${sourceLabel}.`
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const pluginCapabilityBadges = (plugin: PluginMeta): string[] => {
|
|
134
|
+
const badges: string[] = []
|
|
135
|
+
if (plugin.toolCount && plugin.toolCount > 0) badges.push(`${plugin.toolCount} tool${plugin.toolCount === 1 ? '' : 's'}`)
|
|
136
|
+
if (plugin.hookCount && plugin.hookCount > 0) badges.push(`${plugin.hookCount} hook${plugin.hookCount === 1 ? '' : 's'}`)
|
|
137
|
+
if (plugin.hasUI) badges.push('UI')
|
|
138
|
+
if (plugin.providerCount && plugin.providerCount > 0) badges.push(`${plugin.providerCount} provider${plugin.providerCount === 1 ? '' : 's'}`)
|
|
139
|
+
if (plugin.connectorCount && plugin.connectorCount > 0) badges.push(`${plugin.connectorCount} connector${plugin.connectorCount === 1 ? '' : 's'}`)
|
|
140
|
+
return badges
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const renderInstalledPlugin = (plugin: (typeof pluginList)[number], allowDelete: boolean) => (
|
|
144
|
+
<div
|
|
145
|
+
key={plugin.filename}
|
|
146
|
+
role="button"
|
|
147
|
+
tabIndex={0}
|
|
148
|
+
onClick={() => handleEdit(plugin.filename)}
|
|
149
|
+
onKeyDown={(e) => {
|
|
150
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
151
|
+
e.preventDefault()
|
|
152
|
+
handleEdit(plugin.filename)
|
|
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"
|
|
156
|
+
>
|
|
157
|
+
<div className="flex items-center justify-between mb-1">
|
|
158
|
+
<div className="flex items-center gap-2 min-w-0">
|
|
159
|
+
{plugin.createdByAgentId && agents[plugin.createdByAgentId] && (
|
|
160
|
+
<button
|
|
161
|
+
type="button"
|
|
162
|
+
title={`Created by ${agents[plugin.createdByAgentId].name} — click to open chat`}
|
|
163
|
+
onClick={(e) => { e.stopPropagation(); navigateToAgentChat(plugin.createdByAgentId!) }}
|
|
164
|
+
className="shrink-0 rounded-full hover:ring-2 hover:ring-accent-bright/40 transition-all cursor-pointer bg-transparent border-none p-0"
|
|
165
|
+
>
|
|
166
|
+
<AgentAvatar
|
|
167
|
+
seed={agents[plugin.createdByAgentId].avatarSeed || null}
|
|
168
|
+
avatarUrl={agents[plugin.createdByAgentId].avatarUrl}
|
|
169
|
+
name={agents[plugin.createdByAgentId].name || 'Agent'}
|
|
170
|
+
size={20}
|
|
171
|
+
/>
|
|
172
|
+
</button>
|
|
173
|
+
)}
|
|
174
|
+
<span className="font-display text-[14px] font-600 text-text truncate">{plugin.name}</span>
|
|
175
|
+
</div>
|
|
176
|
+
<div className="flex items-center gap-2 shrink-0 ml-2">
|
|
177
|
+
{!inSidebar ? (
|
|
178
|
+
<>
|
|
179
|
+
<div
|
|
180
|
+
onClick={(e) => handleToggle(e, plugin.filename, plugin.enabled)}
|
|
181
|
+
className={`w-9 h-5 rounded-full transition-all relative cursor-pointer shrink-0
|
|
182
|
+
${plugin.enabled ? 'bg-accent-bright' : 'bg-white/[0.08]'}`}
|
|
183
|
+
>
|
|
184
|
+
<div className={`absolute top-0.5 w-4 h-4 rounded-full bg-white transition-all
|
|
185
|
+
${plugin.enabled ? 'left-[18px]' : 'left-0.5'}`} />
|
|
186
|
+
</div>
|
|
187
|
+
{allowDelete && (
|
|
188
|
+
<button
|
|
189
|
+
onClick={(e) => handleDeleteClick(e, plugin.filename, plugin.name)}
|
|
190
|
+
className="text-text-3/40 hover:text-red-400 transition-colors p-0.5"
|
|
191
|
+
title="Delete"
|
|
192
|
+
>
|
|
193
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
194
|
+
<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" />
|
|
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>
|
|
203
|
+
)}
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
<div className="text-[11px] font-mono text-text-3/50 mb-1">{plugin.filename}</div>
|
|
207
|
+
<p className="text-[12px] text-text-3/70 leading-relaxed">{pluginDescription(plugin)}</p>
|
|
208
|
+
<div className="flex items-center gap-1.5 mt-2 flex-wrap">
|
|
209
|
+
<span className={`text-[10px] font-600 px-1.5 py-0.5 rounded-full ${
|
|
210
|
+
plugin.source === 'local' ? 'text-indigo-300 bg-indigo-500/10' : 'text-emerald-300 bg-emerald-500/10'
|
|
211
|
+
}`}>
|
|
212
|
+
{plugin.source === 'local' ? 'Core' : 'Extension'}
|
|
213
|
+
</span>
|
|
214
|
+
{pluginCapabilityBadges(plugin).map((badge) => (
|
|
215
|
+
<span key={badge} className="text-[10px] font-600 px-1.5 py-0.5 rounded-full text-text-3/80 bg-white/[0.05]">
|
|
216
|
+
{badge}
|
|
217
|
+
</span>
|
|
218
|
+
))}
|
|
219
|
+
</div>
|
|
220
|
+
{!inSidebar && (
|
|
221
|
+
<p className="text-[10px] text-text-3/50 mt-2">Click for full details and controls</p>
|
|
222
|
+
)}
|
|
223
|
+
{plugin.autoDisabled && (
|
|
224
|
+
<p className="mt-1 text-[11px] text-amber-400/90 line-clamp-2">
|
|
225
|
+
Auto-disabled after {plugin.failureCount ?? 0} failures
|
|
226
|
+
{plugin.lastFailureStage ? ` (${plugin.lastFailureStage})` : ''}.
|
|
227
|
+
{plugin.lastFailureError ? ` ${plugin.lastFailureError}` : ''}
|
|
228
|
+
</p>
|
|
229
|
+
)}
|
|
230
|
+
</div>
|
|
231
|
+
)
|
|
232
|
+
|
|
75
233
|
// Marketplace tab content (full-width only)
|
|
76
234
|
const renderMarketplace = () => {
|
|
77
235
|
if (mpLoading) return <p className="text-[12px] text-text-3/70 py-8 text-center">Loading marketplace...</p>
|
|
78
236
|
if (marketplace.length === 0) return <p className="text-[12px] text-text-3/70 py-8 text-center">No plugins available</p>
|
|
79
237
|
|
|
80
|
-
const allTags = Array.from(new Set(marketplace.flatMap((p) => p.tags))).sort()
|
|
238
|
+
const allTags = Array.from(new Set(marketplace.flatMap((p) => p.tags ?? []))).sort()
|
|
81
239
|
const q = search.toLowerCase()
|
|
82
240
|
const filtered = marketplace
|
|
83
241
|
.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
|
|
242
|
+
if (q && !p.name.toLowerCase().includes(q) && !p.description.toLowerCase().includes(q) && !(p.tags ?? []).some((t) => t.toLowerCase().includes(q))) return false
|
|
243
|
+
if (activeTag && !(p.tags ?? []).includes(activeTag)) return false
|
|
86
244
|
return true
|
|
87
245
|
})
|
|
88
|
-
.sort((a, b) => sort === 'downloads' ? b.downloads - a.downloads : a.name.localeCompare(b.name))
|
|
246
|
+
.sort((a, b) => sort === 'downloads' ? (b.downloads ?? 0) - (a.downloads ?? 0) : a.name.localeCompare(b.name))
|
|
89
247
|
|
|
90
248
|
return (
|
|
91
249
|
<div className="space-y-3">
|
|
@@ -146,7 +304,7 @@ export function PluginList({ inSidebar }: { inSidebar?: boolean }) {
|
|
|
146
304
|
<div className="flex items-center gap-2 mt-2">
|
|
147
305
|
<span className="text-[10px] text-text-3/70">by {p.author}</span>
|
|
148
306
|
<span className="text-[10px] text-text-3/50">·</span>
|
|
149
|
-
{p.tags.slice(0, 3).map((t) => (
|
|
307
|
+
{(p.tags ?? []).slice(0, 3).map((t) => (
|
|
150
308
|
<button
|
|
151
309
|
key={t}
|
|
152
310
|
onClick={() => setActiveTag(activeTag === t ? null : t)}
|
|
@@ -209,52 +367,48 @@ export function PluginList({ inSidebar }: { inSidebar?: boolean }) {
|
|
|
209
367
|
</button>
|
|
210
368
|
</div>
|
|
211
369
|
) : (
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
>
|
|
219
|
-
<
|
|
220
|
-
<
|
|
221
|
-
|
|
222
|
-
|
|
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-accent-bright' : '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
|
-
)}
|
|
370
|
+
inSidebar ? (
|
|
371
|
+
<div className="space-y-2">
|
|
372
|
+
{pluginList.map((plugin) => renderInstalledPlugin(plugin, plugin.source !== 'local'))}
|
|
373
|
+
</div>
|
|
374
|
+
) : (
|
|
375
|
+
<div className="space-y-6">
|
|
376
|
+
{corePlugins.length > 0 && (
|
|
377
|
+
<section>
|
|
378
|
+
<div className="mb-3 px-1">
|
|
379
|
+
<h3 className="text-[13px] font-700 text-text-2">Core Platform</h3>
|
|
380
|
+
<p className="text-[12px] text-text-3/60 mt-0.5">Official SwarmClaw plugins shipped with the platform.</p>
|
|
247
381
|
</div>
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
382
|
+
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3">
|
|
383
|
+
{corePlugins.map((plugin) => renderInstalledPlugin(plugin, false))}
|
|
384
|
+
</div>
|
|
385
|
+
</section>
|
|
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))}
|
|
396
|
+
</div>
|
|
397
|
+
</section>
|
|
398
|
+
)}
|
|
399
|
+
</div>
|
|
400
|
+
)
|
|
256
401
|
)
|
|
257
402
|
)}
|
|
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
|
+
/>
|
|
258
412
|
</div>
|
|
259
413
|
)
|
|
260
414
|
}
|
|
@@ -3,9 +3,28 @@
|
|
|
3
3
|
import { useEffect, useState, useCallback } from 'react'
|
|
4
4
|
import { useAppStore } from '@/stores/use-app-store'
|
|
5
5
|
import { BottomSheet } from '@/components/shared/bottom-sheet'
|
|
6
|
+
import { ConfirmDialog } from '@/components/shared/confirm-dialog'
|
|
6
7
|
import { api } from '@/lib/api-client'
|
|
8
|
+
import { toast } from 'sonner'
|
|
7
9
|
import type { PluginMeta, MarketplacePlugin } from '@/types'
|
|
8
10
|
|
|
11
|
+
function pluginDescription(plugin: PluginMeta): string {
|
|
12
|
+
const raw = (plugin.description || '').trim()
|
|
13
|
+
if (raw) return raw
|
|
14
|
+
const sourceLabel = plugin.source === 'local' ? 'core plugin' : 'installed plugin'
|
|
15
|
+
return `No description provided. This ${sourceLabel} is available and can be configured from this panel.`
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function pluginCapabilityBadges(plugin: PluginMeta): string[] {
|
|
19
|
+
const badges: string[] = []
|
|
20
|
+
if (plugin.toolCount && plugin.toolCount > 0) badges.push(`${plugin.toolCount} tool${plugin.toolCount === 1 ? '' : 's'}`)
|
|
21
|
+
if (plugin.hookCount && plugin.hookCount > 0) badges.push(`${plugin.hookCount} hook${plugin.hookCount === 1 ? '' : 's'}`)
|
|
22
|
+
if (plugin.hasUI) badges.push('UI extension')
|
|
23
|
+
if (plugin.providerCount && plugin.providerCount > 0) badges.push(`${plugin.providerCount} provider${plugin.providerCount === 1 ? '' : 's'}`)
|
|
24
|
+
if (plugin.connectorCount && plugin.connectorCount > 0) badges.push(`${plugin.connectorCount} connector${plugin.connectorCount === 1 ? '' : 's'}`)
|
|
25
|
+
return badges
|
|
26
|
+
}
|
|
27
|
+
|
|
9
28
|
export function PluginSheet() {
|
|
10
29
|
const open = useAppStore((s) => s.pluginSheetOpen)
|
|
11
30
|
const setOpen = useAppStore((s) => s.setPluginSheetOpen)
|
|
@@ -22,6 +41,7 @@ export function PluginSheet() {
|
|
|
22
41
|
const [urlFilename, setUrlFilename] = useState('')
|
|
23
42
|
const [urlStatus, setUrlStatus] = useState<{ ok: boolean; message: string } | null>(null)
|
|
24
43
|
const [deleting, setDeleting] = useState(false)
|
|
44
|
+
const [confirmDelete, setConfirmDelete] = useState(false)
|
|
25
45
|
const [search, setSearch] = useState('')
|
|
26
46
|
const [activeTag, setActiveTag] = useState<string | null>(null)
|
|
27
47
|
const [sort, setSort] = useState<'name' | 'downloads'>('downloads')
|
|
@@ -38,8 +58,12 @@ export function PluginSheet() {
|
|
|
38
58
|
}, [])
|
|
39
59
|
|
|
40
60
|
useEffect(() => {
|
|
41
|
-
if (open
|
|
42
|
-
|
|
61
|
+
if (!open || !!editingFilename || tab !== 'marketplace') return
|
|
62
|
+
const timer = setTimeout(() => {
|
|
63
|
+
void loadMarketplace()
|
|
64
|
+
}, 0)
|
|
65
|
+
return () => clearTimeout(timer)
|
|
66
|
+
}, [open, editingFilename, tab, loadMarketplace])
|
|
43
67
|
|
|
44
68
|
const handleClose = () => {
|
|
45
69
|
setOpen(false)
|
|
@@ -47,29 +71,43 @@ export function PluginSheet() {
|
|
|
47
71
|
setUrlInput('')
|
|
48
72
|
setUrlFilename('')
|
|
49
73
|
setUrlStatus(null)
|
|
74
|
+
setConfirmDelete(false)
|
|
50
75
|
}
|
|
51
76
|
|
|
52
77
|
const togglePlugin = async (filename: string, enabled: boolean) => {
|
|
53
|
-
|
|
54
|
-
|
|
78
|
+
try {
|
|
79
|
+
await api('POST', '/plugins', { filename, enabled })
|
|
80
|
+
toast.success(enabled ? 'Plugin enabled' : 'Plugin disabled')
|
|
81
|
+
loadPlugins()
|
|
82
|
+
} catch (err: unknown) {
|
|
83
|
+
toast.error(err instanceof Error ? err.message : 'Failed to toggle plugin')
|
|
84
|
+
}
|
|
55
85
|
}
|
|
56
86
|
|
|
57
87
|
const deletePlugin = async (filename: string) => {
|
|
58
88
|
setDeleting(true)
|
|
59
89
|
try {
|
|
60
|
-
await api('DELETE', `/plugins
|
|
90
|
+
await api('DELETE', `/plugins?filename=${encodeURIComponent(filename)}`)
|
|
91
|
+
toast.success('Plugin deleted')
|
|
61
92
|
await loadPlugins()
|
|
62
93
|
handleClose()
|
|
63
|
-
} catch
|
|
94
|
+
} catch (err: unknown) {
|
|
95
|
+
toast.error(err instanceof Error ? err.message : 'Delete failed')
|
|
96
|
+
}
|
|
64
97
|
setDeleting(false)
|
|
65
98
|
}
|
|
66
99
|
|
|
67
100
|
const installFromMarketplace = async (p: MarketplacePlugin) => {
|
|
68
101
|
setInstalling(p.id)
|
|
102
|
+
const toastId = toast.loading(`Installing ${p.name}...`)
|
|
69
103
|
try {
|
|
70
|
-
|
|
104
|
+
const safeFilename = `${p.id.replace(/[^a-zA-Z0-9.-]/g, '_')}.js`
|
|
105
|
+
await api('POST', '/plugins/install', { url: p.url, filename: safeFilename })
|
|
71
106
|
await loadPlugins()
|
|
72
|
-
|
|
107
|
+
toast.success(`Installed ${p.name}`, { id: toastId })
|
|
108
|
+
} catch (err: unknown) {
|
|
109
|
+
toast.error(err instanceof Error ? err.message : 'Install failed', { id: toastId })
|
|
110
|
+
}
|
|
73
111
|
setInstalling(null)
|
|
74
112
|
}
|
|
75
113
|
|
|
@@ -81,10 +119,11 @@ export function PluginSheet() {
|
|
|
81
119
|
await api('POST', '/plugins/install', { url: urlInput, filename: urlFilename })
|
|
82
120
|
await loadPlugins()
|
|
83
121
|
setUrlStatus({ ok: true, message: 'Installed successfully' })
|
|
122
|
+
toast.success('Plugin installed from URL')
|
|
84
123
|
setUrlInput('')
|
|
85
124
|
setUrlFilename('')
|
|
86
|
-
} catch (err:
|
|
87
|
-
setUrlStatus({ ok: false, message: err.message
|
|
125
|
+
} catch (err: unknown) {
|
|
126
|
+
setUrlStatus({ ok: false, message: err instanceof Error ? err.message : 'Install failed' })
|
|
88
127
|
}
|
|
89
128
|
setInstalling(null)
|
|
90
129
|
}
|
|
@@ -101,18 +140,61 @@ export function PluginSheet() {
|
|
|
101
140
|
<BottomSheet open={open} onClose={handleClose}>
|
|
102
141
|
{editing ? (
|
|
103
142
|
<div className="space-y-5">
|
|
104
|
-
<div className="py-
|
|
105
|
-
<div className="flex items-
|
|
106
|
-
<
|
|
107
|
-
|
|
143
|
+
<div className="py-4 px-4 rounded-[14px] bg-surface border border-white/[0.06]">
|
|
144
|
+
<div className="flex items-start justify-between gap-3 mb-2">
|
|
145
|
+
<div className="min-w-0">
|
|
146
|
+
<div className="flex items-center gap-2 mb-1">
|
|
147
|
+
<span className="text-[15px] font-700 text-text truncate">{editing.name}</span>
|
|
148
|
+
<span className="text-[10px] font-mono text-text-3/70">v{editing.version || '1.0.0'}</span>
|
|
149
|
+
{editing.openclaw && <span className="text-[9px] font-600 text-emerald-400 bg-emerald-400/10 px-1.5 py-0.5 rounded-full">OpenClaw</span>}
|
|
150
|
+
</div>
|
|
151
|
+
<p className="text-[12px] text-text-3/80 leading-relaxed">{pluginDescription(editing)}</p>
|
|
152
|
+
</div>
|
|
153
|
+
<span className={`shrink-0 text-[10px] font-600 px-2 py-1 rounded-full ${
|
|
154
|
+
editing.enabled ? 'text-emerald-300 bg-emerald-500/10' : 'text-text-3/80 bg-white/[0.05]'
|
|
155
|
+
}`}>
|
|
156
|
+
{editing.enabled ? 'Enabled' : 'Disabled'}
|
|
157
|
+
</span>
|
|
108
158
|
</div>
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
159
|
+
|
|
160
|
+
<div className="grid grid-cols-2 gap-2 mt-3">
|
|
161
|
+
<div className="rounded-[10px] bg-bg/50 border border-white/[0.05] px-2.5 py-2">
|
|
162
|
+
<div className="text-[10px] uppercase tracking-[0.08em] text-text-3/60 mb-0.5">Source</div>
|
|
163
|
+
<div className="text-[11px] text-text-2">{editing.source === 'local' ? 'Core Platform' : 'Extension'}</div>
|
|
164
|
+
</div>
|
|
165
|
+
<div className="rounded-[10px] bg-bg/50 border border-white/[0.05] px-2.5 py-2">
|
|
166
|
+
<div className="text-[10px] uppercase tracking-[0.08em] text-text-3/60 mb-0.5">Author</div>
|
|
167
|
+
<div className="text-[11px] text-text-2">{editing.author || 'Unknown'}</div>
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
|
|
171
|
+
<div className="text-[11px] font-mono text-text-3/60 mt-3 break-all">{editing.filename}</div>
|
|
172
|
+
<div className="flex items-center gap-1.5 mt-2 flex-wrap">
|
|
173
|
+
{pluginCapabilityBadges(editing).length > 0 ? (
|
|
174
|
+
pluginCapabilityBadges(editing).map((badge) => (
|
|
175
|
+
<span key={badge} className="text-[10px] font-600 px-1.5 py-0.5 rounded-full text-text-3/80 bg-white/[0.05]">
|
|
176
|
+
{badge}
|
|
177
|
+
</span>
|
|
178
|
+
))
|
|
179
|
+
) : (
|
|
180
|
+
<span className="text-[10px] text-text-3/50">No declared tools/hooks metadata</span>
|
|
181
|
+
)}
|
|
182
|
+
</div>
|
|
183
|
+
|
|
184
|
+
{editing.autoDisabled && (
|
|
185
|
+
<div className="mt-3 p-2.5 rounded-[10px] bg-amber-500/[0.06] border border-amber-500/20 text-[11px] text-amber-300/90">
|
|
186
|
+
Auto-disabled after {editing.failureCount ?? 0} failures
|
|
187
|
+
{editing.lastFailureStage ? ` at ${editing.lastFailureStage}` : ''}.
|
|
188
|
+
{editing.lastFailureError ? ` ${editing.lastFailureError}` : ''}
|
|
189
|
+
</div>
|
|
190
|
+
)}
|
|
112
191
|
</div>
|
|
113
192
|
|
|
114
193
|
<div className="flex items-center justify-between py-3 px-4 rounded-[14px] bg-surface border border-white/[0.06]">
|
|
115
|
-
<
|
|
194
|
+
<div>
|
|
195
|
+
<span className="text-[13px] font-600 text-text block">Enabled</span>
|
|
196
|
+
<span className="text-[11px] text-text-3/60">Disable to keep the plugin installed but inactive.</span>
|
|
197
|
+
</div>
|
|
116
198
|
<div
|
|
117
199
|
onClick={() => togglePlugin(editing.filename, !editing.enabled)}
|
|
118
200
|
className={`w-11 h-6 rounded-full transition-all duration-200 relative cursor-pointer shrink-0
|
|
@@ -124,7 +206,7 @@ export function PluginSheet() {
|
|
|
124
206
|
</div>
|
|
125
207
|
|
|
126
208
|
<button
|
|
127
|
-
onClick={() =>
|
|
209
|
+
onClick={() => setConfirmDelete(true)}
|
|
128
210
|
disabled={deleting}
|
|
129
211
|
className="w-full py-2.5 rounded-[10px] text-[13px] font-600 bg-red-500/10 text-red-400 border border-red-500/20
|
|
130
212
|
hover:bg-red-500/20 transition-all cursor-pointer disabled:opacity-40 disabled:cursor-default"
|
|
@@ -150,15 +232,15 @@ export function PluginSheet() {
|
|
|
150
232
|
: marketplace.length === 0
|
|
151
233
|
? <p className="text-[12px] text-text-3/70">No plugins available</p>
|
|
152
234
|
: (() => {
|
|
153
|
-
const allTags = Array.from(new Set(marketplace.flatMap((p) => p.tags))).sort()
|
|
235
|
+
const allTags = Array.from(new Set(marketplace.flatMap((p) => (p.tags ?? [])))).sort()
|
|
154
236
|
const q = search.toLowerCase()
|
|
155
237
|
const filtered = marketplace
|
|
156
238
|
.filter((p) => {
|
|
157
|
-
if (q && !p.name.toLowerCase().includes(q) && !p.description.toLowerCase().includes(q) && !p.tags.some((t) => t.toLowerCase().includes(q))) return false
|
|
158
|
-
if (activeTag && !p.tags.includes(activeTag)) return false
|
|
239
|
+
if (q && !p.name.toLowerCase().includes(q) && !p.description.toLowerCase().includes(q) && !(p.tags ?? []).some((t) => t.toLowerCase().includes(q))) return false
|
|
240
|
+
if (activeTag && !(p.tags ?? []).includes(activeTag)) return false
|
|
159
241
|
return true
|
|
160
242
|
})
|
|
161
|
-
.sort((a, b) => sort === 'downloads' ? b.downloads - a.downloads : a.name.localeCompare(b.name))
|
|
243
|
+
.sort((a, b) => sort === 'downloads' ? (b.downloads ?? 0) - (a.downloads ?? 0) : a.name.localeCompare(b.name))
|
|
162
244
|
|
|
163
245
|
return (
|
|
164
246
|
<div className="space-y-3">
|
|
@@ -224,7 +306,7 @@ export function PluginSheet() {
|
|
|
224
306
|
<div className="flex items-center gap-2 mt-2">
|
|
225
307
|
<span className="text-[10px] text-text-3/70">by {p.author}</span>
|
|
226
308
|
<span className="text-[10px] text-text-3/50">·</span>
|
|
227
|
-
{p.tags.slice(0, 3).map((t) => (
|
|
309
|
+
{(p.tags ?? []).slice(0, 3).map((t) => (
|
|
228
310
|
<button
|
|
229
311
|
key={t}
|
|
230
312
|
onClick={() => setActiveTag(activeTag === t ? null : t)}
|
|
@@ -306,6 +388,19 @@ export function PluginSheet() {
|
|
|
306
388
|
)}
|
|
307
389
|
</div>
|
|
308
390
|
)}
|
|
391
|
+
<ConfirmDialog
|
|
392
|
+
open={!!editing && confirmDelete}
|
|
393
|
+
title="Delete Plugin"
|
|
394
|
+
message={editing ? `Delete "${editing.name}"? This cannot be undone.` : ''}
|
|
395
|
+
confirmLabel={deleting ? 'Deleting...' : 'Delete'}
|
|
396
|
+
danger
|
|
397
|
+
onConfirm={() => {
|
|
398
|
+
if (!editing) return
|
|
399
|
+
setConfirmDelete(false)
|
|
400
|
+
void deletePlugin(editing.filename)
|
|
401
|
+
}}
|
|
402
|
+
onCancel={() => { if (!deleting) setConfirmDelete(false) }}
|
|
403
|
+
/>
|
|
309
404
|
</BottomSheet>
|
|
310
405
|
)
|
|
311
406
|
}
|