@swarmclawai/swarmclaw 0.7.1 → 0.7.3
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 +155 -150
- package/package.json +1 -1
- package/src/app/api/agents/[id]/route.ts +26 -0
- package/src/app/api/agents/[id]/thread/route.ts +37 -9
- package/src/app/api/agents/route.ts +13 -2
- package/src/app/api/auth/route.ts +76 -7
- package/src/app/api/chatrooms/[id]/chat/route.ts +7 -2
- package/src/app/api/{sessions → chats}/[id]/browser/route.ts +5 -1
- package/src/app/api/{sessions → chats}/[id]/chat/route.ts +7 -3
- package/src/app/api/{sessions → chats}/[id]/checkpoints/route.ts +1 -1
- package/src/app/api/chats/[id]/main-loop/route.ts +13 -0
- package/src/app/api/{sessions → chats}/[id]/messages/route.ts +19 -13
- package/src/app/api/{sessions → chats}/[id]/restore/route.ts +1 -1
- package/src/app/api/{sessions → chats}/[id]/route.ts +22 -52
- package/src/app/api/{sessions → chats}/[id]/stop/route.ts +6 -1
- package/src/app/api/{sessions → chats}/route.ts +21 -7
- package/src/app/api/connectors/[id]/doctor/route.ts +26 -0
- package/src/app/api/connectors/doctor/route.ts +13 -0
- package/src/app/api/files/open/route.ts +16 -14
- package/src/app/api/memory/maintenance/route.ts +11 -1
- package/src/app/api/openclaw/agent-files/route.ts +27 -4
- package/src/app/api/openclaw/skills/route.ts +11 -3
- package/src/app/api/plugins/dependencies/route.ts +24 -0
- package/src/app/api/plugins/install/route.ts +15 -92
- package/src/app/api/plugins/route.ts +6 -26
- package/src/app/api/plugins/settings/route.ts +40 -0
- package/src/app/api/plugins/ui/route.ts +1 -0
- package/src/app/api/settings/route.ts +49 -7
- package/src/app/api/tasks/[id]/route.ts +15 -6
- package/src/app/api/tasks/bulk/route.ts +2 -2
- package/src/app/api/tasks/route.ts +9 -4
- package/src/app/api/usage/route.ts +30 -0
- package/src/app/api/webhooks/[id]/route.ts +8 -1
- package/src/app/page.tsx +9 -2
- package/src/cli/index.js +39 -33
- package/src/cli/index.ts +43 -49
- package/src/cli/spec.js +29 -27
- package/src/components/agents/agent-card.tsx +16 -13
- package/src/components/agents/agent-chat-list.tsx +104 -4
- package/src/components/agents/agent-list.tsx +54 -22
- package/src/components/agents/agent-sheet.tsx +209 -18
- package/src/components/agents/cron-job-form.tsx +3 -3
- package/src/components/agents/inspector-panel.tsx +110 -50
- package/src/components/auth/access-key-gate.tsx +36 -97
- package/src/components/auth/setup-wizard.tsx +5 -38
- package/src/components/chat/chat-area.tsx +39 -27
- package/src/components/{sessions/session-card.tsx → chat/chat-card.tsx} +7 -23
- package/src/components/chat/chat-header.tsx +299 -314
- package/src/components/{sessions/session-list.tsx → chat/chat-list.tsx} +11 -14
- 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 +5 -3
- 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/chatrooms/chatroom-view.tsx +347 -205
- package/src/components/connectors/connector-list.tsx +265 -127
- package/src/components/connectors/connector-sheet.tsx +218 -1
- package/src/components/home/home-view.tsx +129 -5
- package/src/components/layout/app-layout.tsx +392 -182
- package/src/components/layout/mobile-header.tsx +26 -8
- package/src/components/plugins/plugin-list.tsx +487 -254
- package/src/components/plugins/plugin-sheet.tsx +236 -13
- package/src/components/projects/project-detail.tsx +183 -0
- package/src/components/settings/gateway-connection-panel.tsx +1 -1
- package/src/components/shared/agent-picker-list.tsx +2 -2
- package/src/components/shared/command-palette.tsx +111 -25
- package/src/components/shared/settings/plugin-manager.tsx +20 -4
- package/src/components/shared/settings/section-capability-policy.tsx +105 -0
- package/src/components/shared/settings/section-heartbeat.tsx +78 -1
- package/src/components/shared/settings/section-orchestrator.tsx +3 -3
- package/src/components/shared/settings/section-providers.tsx +1 -1
- package/src/components/shared/settings/section-runtime-loop.tsx +5 -5
- package/src/components/shared/settings/section-secrets.tsx +6 -6
- package/src/components/shared/settings/section-user-preferences.tsx +1 -1
- package/src/components/shared/settings/section-voice.tsx +5 -1
- package/src/components/shared/settings/section-web-search.tsx +10 -2
- package/src/components/shared/settings/settings-page.tsx +244 -56
- package/src/components/tasks/approvals-panel.tsx +205 -18
- package/src/components/tasks/task-board.tsx +242 -46
- package/src/components/usage/metrics-dashboard.tsx +147 -1
- package/src/components/wallets/wallet-panel.tsx +17 -5
- package/src/components/webhooks/webhook-sheet.tsx +8 -8
- package/src/lib/auth.ts +17 -0
- package/src/lib/chat-streaming-state.test.ts +108 -0
- package/src/lib/chat-streaming-state.ts +108 -0
- package/src/lib/chat.ts +1 -1
- package/src/lib/{sessions.ts → chats.ts} +28 -18
- package/src/lib/openclaw-agent-id.test.ts +14 -0
- package/src/lib/openclaw-agent-id.ts +31 -0
- package/src/lib/providers/claude-cli.ts +1 -1
- package/src/lib/server/agent-assignment.test.ts +112 -0
- package/src/lib/server/agent-assignment.ts +169 -0
- package/src/lib/server/approval-connector-notify.test.ts +253 -0
- package/src/lib/server/approvals-auto-approve.test.ts +205 -0
- package/src/lib/server/approvals.ts +483 -75
- package/src/lib/server/autonomy-runtime.test.ts +341 -0
- package/src/lib/server/browser-state.test.ts +118 -0
- package/src/lib/server/browser-state.ts +123 -0
- package/src/lib/server/build-llm.test.ts +36 -0
- package/src/lib/server/build-llm.ts +11 -4
- package/src/lib/server/builtin-plugins.ts +34 -0
- package/src/lib/server/capability-router.ts +10 -8
- package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
- package/src/lib/server/chat-execution-tool-events.test.ts +134 -0
- package/src/lib/server/chat-execution.ts +285 -165
- package/src/lib/server/chatroom-health.test.ts +26 -0
- package/src/lib/server/chatroom-health.ts +2 -3
- package/src/lib/server/chatroom-helpers.test.ts +67 -2
- package/src/lib/server/chatroom-helpers.ts +48 -8
- package/src/lib/server/connectors/discord.ts +175 -11
- package/src/lib/server/connectors/doctor.test.ts +80 -0
- package/src/lib/server/connectors/doctor.ts +116 -0
- package/src/lib/server/connectors/manager.ts +948 -112
- package/src/lib/server/connectors/policy.test.ts +222 -0
- package/src/lib/server/connectors/policy.ts +452 -0
- package/src/lib/server/connectors/slack.ts +188 -9
- package/src/lib/server/connectors/telegram.ts +65 -15
- package/src/lib/server/connectors/thread-context.test.ts +44 -0
- package/src/lib/server/connectors/thread-context.ts +72 -0
- package/src/lib/server/connectors/types.ts +41 -11
- package/src/lib/server/cost.ts +34 -1
- package/src/lib/server/daemon-state.ts +61 -3
- package/src/lib/server/data-dir.ts +13 -0
- package/src/lib/server/delegation-jobs.test.ts +140 -0
- package/src/lib/server/delegation-jobs.ts +248 -0
- package/src/lib/server/document-utils.test.ts +47 -0
- package/src/lib/server/document-utils.ts +397 -0
- package/src/lib/server/heartbeat-service.ts +14 -40
- package/src/lib/server/heartbeat-source.test.ts +22 -0
- package/src/lib/server/heartbeat-source.ts +7 -0
- package/src/lib/server/identity-continuity.test.ts +77 -0
- package/src/lib/server/identity-continuity.ts +127 -0
- package/src/lib/server/mailbox-utils.ts +347 -0
- package/src/lib/server/main-agent-loop.ts +28 -1103
- package/src/lib/server/memory-db.ts +4 -6
- package/src/lib/server/memory-tiers.ts +40 -0
- package/src/lib/server/openclaw-agent-resolver.test.ts +70 -0
- package/src/lib/server/openclaw-agent-resolver.ts +128 -0
- package/src/lib/server/openclaw-exec-config.ts +5 -6
- package/src/lib/server/openclaw-skills-normalize.test.ts +56 -0
- package/src/lib/server/openclaw-skills-normalize.ts +136 -0
- package/src/lib/server/openclaw-sync.ts +3 -2
- package/src/lib/server/orchestrator-lg.ts +20 -9
- package/src/lib/server/orchestrator.ts +7 -7
- package/src/lib/server/playwright-proxy.mjs +27 -3
- package/src/lib/server/plugins.test.ts +207 -0
- package/src/lib/server/plugins.ts +927 -66
- package/src/lib/server/provider-health.ts +38 -6
- package/src/lib/server/queue.ts +13 -28
- package/src/lib/server/scheduler.ts +2 -0
- package/src/lib/server/session-archive-memory.test.ts +85 -0
- package/src/lib/server/session-archive-memory.ts +230 -0
- package/src/lib/server/session-mailbox.ts +8 -18
- package/src/lib/server/session-reset-policy.test.ts +99 -0
- package/src/lib/server/session-reset-policy.ts +311 -0
- package/src/lib/server/session-run-manager.ts +33 -82
- package/src/lib/server/session-tools/autonomy-tools.test.ts +105 -0
- package/src/lib/server/session-tools/calendar.ts +366 -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 +114 -10
- package/src/lib/server/session-tools/context.ts +21 -5
- package/src/lib/server/session-tools/crawl.ts +447 -0
- package/src/lib/server/session-tools/crud.ts +74 -28
- package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
- package/src/lib/server/session-tools/delegate.ts +497 -24
- package/src/lib/server/session-tools/discovery.ts +24 -6
- package/src/lib/server/session-tools/document.ts +283 -0
- package/src/lib/server/session-tools/edit_file.ts +4 -2
- package/src/lib/server/session-tools/email.ts +320 -0
- package/src/lib/server/session-tools/extract.ts +137 -0
- package/src/lib/server/session-tools/file-normalize.test.ts +93 -0
- package/src/lib/server/session-tools/file-send.test.ts +84 -1
- package/src/lib/server/session-tools/file.ts +241 -25
- 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/human-loop.ts +227 -0
- package/src/lib/server/session-tools/image-gen.ts +380 -0
- package/src/lib/server/session-tools/index.ts +130 -50
- package/src/lib/server/session-tools/mailbox.ts +276 -0
- package/src/lib/server/session-tools/memory.ts +172 -3
- package/src/lib/server/session-tools/monitor.ts +151 -8
- package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
- 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-normalize.test.ts +142 -0
- package/src/lib/server/session-tools/platform.ts +148 -7
- package/src/lib/server/session-tools/plugin-creator.ts +89 -26
- package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
- package/src/lib/server/session-tools/replicate.ts +301 -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 +24 -12
- package/src/lib/server/session-tools/session-info.ts +43 -7
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +31 -17
- package/src/lib/server/session-tools/shell.ts +5 -2
- package/src/lib/server/session-tools/subagent.ts +194 -28
- package/src/lib/server/session-tools/table.ts +587 -0
- package/src/lib/server/session-tools/wallet.ts +42 -12
- package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
- package/src/lib/server/session-tools/web.ts +926 -91
- package/src/lib/server/storage.ts +255 -16
- package/src/lib/server/stream-agent-chat.ts +116 -268
- package/src/lib/server/structured-extract.test.ts +72 -0
- package/src/lib/server/structured-extract.ts +373 -0
- package/src/lib/server/task-mention.test.ts +16 -2
- package/src/lib/server/task-mention.ts +61 -10
- package/src/lib/server/tool-aliases.ts +66 -18
- package/src/lib/server/tool-capability-policy.test.ts +9 -9
- package/src/lib/server/tool-capability-policy.ts +38 -27
- package/src/lib/server/tool-retry.ts +2 -0
- package/src/lib/server/watch-jobs.test.ts +173 -0
- package/src/lib/server/watch-jobs.ts +532 -0
- package/src/lib/server/ws-hub.ts +5 -3
- package/src/lib/tool-definitions.ts +4 -0
- package/src/lib/validation/schemas.test.ts +26 -0
- package/src/lib/validation/schemas.ts +10 -1
- package/src/lib/ws-client.ts +14 -12
- package/src/proxy.ts +5 -5
- package/src/stores/use-app-store.ts +5 -11
- package/src/stores/use-chat-store.ts +38 -9
- package/src/types/index.ts +352 -47
- package/src/app/api/sessions/[id]/main-loop/route.ts +0 -94
- package/src/components/sessions/new-session-sheet.tsx +0 -253
- package/src/lib/server/main-session.ts +0 -24
- package/src/lib/server/session-run-manager.test.ts +0 -23
- /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]/retry/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.isBuiltin), [pluginList])
|
|
66
|
+
const extensionPlugins = useMemo(() => pluginList.filter((p) => !p.isBuiltin), [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,343 @@ 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.isBuiltin ? '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
|
+
if (plugin.hasDependencyManifest) badges.push(`${plugin.dependencyCount ?? 0} dep${plugin.dependencyCount === 1 ? '' : 's'}`)
|
|
308
|
+
return badges
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// --- Installed plugins grid ---
|
|
312
|
+
|
|
313
|
+
function InstalledGrid({ plugins, allowDelete, search, agents, onEdit, onToggle, onDelete, onNavigateToAgent, emptyMessage, emptyAction }: {
|
|
314
|
+
plugins: PluginMeta[]
|
|
315
|
+
allowDelete: boolean
|
|
316
|
+
search: string
|
|
317
|
+
agents: Record<string, Agent>
|
|
318
|
+
onEdit: (filename: string) => void
|
|
319
|
+
onToggle: (e: React.MouseEvent, filename: string, enabled: boolean) => void
|
|
320
|
+
onDelete: (e: React.MouseEvent, filename: string, name: string) => void
|
|
321
|
+
onNavigateToAgent: (agentId: string) => void
|
|
322
|
+
emptyMessage: string
|
|
323
|
+
emptyAction?: React.ReactNode
|
|
324
|
+
}) {
|
|
325
|
+
if (plugins.length === 0) {
|
|
326
|
+
return (
|
|
327
|
+
<div className="text-center py-16">
|
|
328
|
+
<div className="inline-flex items-center justify-center w-12 h-12 rounded-full bg-white/[0.03] mb-3">
|
|
329
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="text-text-3/30">
|
|
330
|
+
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" strokeLinecap="round" strokeLinejoin="round" />
|
|
331
|
+
</svg>
|
|
332
|
+
</div>
|
|
333
|
+
<p className="text-[13px] text-text-3/50">{emptyMessage}</p>
|
|
334
|
+
{emptyAction}
|
|
335
|
+
</div>
|
|
336
|
+
)
|
|
141
337
|
}
|
|
142
338
|
|
|
143
|
-
|
|
339
|
+
// Group enabled first, then disabled
|
|
340
|
+
const enabled = plugins.filter((p) => p.enabled)
|
|
341
|
+
const disabled = plugins.filter((p) => !p.enabled)
|
|
342
|
+
const sorted = [...enabled, ...disabled]
|
|
343
|
+
|
|
344
|
+
return (
|
|
345
|
+
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3">
|
|
346
|
+
{sorted.map((plugin) => (
|
|
347
|
+
<PluginCard
|
|
348
|
+
key={plugin.filename}
|
|
349
|
+
plugin={plugin}
|
|
350
|
+
allowDelete={allowDelete}
|
|
351
|
+
agents={agents}
|
|
352
|
+
onEdit={onEdit}
|
|
353
|
+
onToggle={onToggle}
|
|
354
|
+
onDelete={onDelete}
|
|
355
|
+
onNavigateToAgent={onNavigateToAgent}
|
|
356
|
+
highlight={search}
|
|
357
|
+
/>
|
|
358
|
+
))}
|
|
359
|
+
</div>
|
|
360
|
+
)
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// --- Plugin card ---
|
|
364
|
+
|
|
365
|
+
function PluginCard({ plugin, allowDelete, agents, onEdit, onToggle, onDelete, onNavigateToAgent, highlight }: {
|
|
366
|
+
plugin: PluginMeta
|
|
367
|
+
allowDelete: boolean
|
|
368
|
+
agents: Record<string, Agent>
|
|
369
|
+
onEdit: (filename: string) => void
|
|
370
|
+
onToggle: (e: React.MouseEvent, filename: string, enabled: boolean) => void
|
|
371
|
+
onDelete: (e: React.MouseEvent, filename: string, name: string) => void
|
|
372
|
+
onNavigateToAgent: (agentId: string) => void
|
|
373
|
+
highlight: string
|
|
374
|
+
}) {
|
|
375
|
+
const badges = pluginCapabilityBadges(plugin)
|
|
376
|
+
const agent = plugin.createdByAgentId ? agents[plugin.createdByAgentId] : null
|
|
377
|
+
|
|
378
|
+
return (
|
|
144
379
|
<div
|
|
145
|
-
key={plugin.filename}
|
|
146
380
|
role="button"
|
|
147
381
|
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"
|
|
382
|
+
onClick={() => onEdit(plugin.filename)}
|
|
383
|
+
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onEdit(plugin.filename) } }}
|
|
384
|
+
className={`group relative text-left p-4 rounded-[14px] border transition-all cursor-pointer
|
|
385
|
+
${plugin.enabled
|
|
386
|
+
? 'border-white/[0.06] bg-surface hover:bg-surface-2 hover:border-white/[0.1]'
|
|
387
|
+
: 'border-white/[0.03] bg-surface/50 hover:bg-surface hover:border-white/[0.06] opacity-70 hover:opacity-100'
|
|
388
|
+
}`}
|
|
156
389
|
>
|
|
157
|
-
|
|
390
|
+
{/* Top row: name + toggle */}
|
|
391
|
+
<div className="flex items-center justify-between gap-2 mb-1.5">
|
|
158
392
|
<div className="flex items-center gap-2 min-w-0">
|
|
159
|
-
{
|
|
393
|
+
{agent && (
|
|
160
394
|
<button
|
|
161
395
|
type="button"
|
|
162
|
-
title={`Created by ${
|
|
163
|
-
onClick={(e) => { e.stopPropagation();
|
|
396
|
+
title={`Created by ${agent.name}`}
|
|
397
|
+
onClick={(e) => { e.stopPropagation(); onNavigateToAgent(plugin.createdByAgentId!) }}
|
|
164
398
|
className="shrink-0 rounded-full hover:ring-2 hover:ring-accent-bright/40 transition-all cursor-pointer bg-transparent border-none p-0"
|
|
165
399
|
>
|
|
166
400
|
<AgentAvatar
|
|
167
|
-
seed={
|
|
168
|
-
avatarUrl={
|
|
169
|
-
name={
|
|
401
|
+
seed={agent.avatarSeed || null}
|
|
402
|
+
avatarUrl={agent.avatarUrl}
|
|
403
|
+
name={agent.name || 'Agent'}
|
|
170
404
|
size={20}
|
|
171
405
|
/>
|
|
172
406
|
</button>
|
|
173
407
|
)}
|
|
174
|
-
<span className="font-display text-[14px] font-600 text-text truncate">
|
|
408
|
+
<span className="font-display text-[14px] font-600 text-text truncate">
|
|
409
|
+
<HighlightText text={plugin.name} highlight={highlight} />
|
|
410
|
+
</span>
|
|
411
|
+
{plugin.version && (
|
|
412
|
+
<span className="text-[10px] font-mono text-text-3/40 shrink-0">v{plugin.version}</span>
|
|
413
|
+
)}
|
|
175
414
|
</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>
|
|
415
|
+
<div className="flex items-center gap-2 shrink-0">
|
|
416
|
+
<div
|
|
417
|
+
onClick={(e) => onToggle(e, plugin.filename, plugin.enabled)}
|
|
418
|
+
className={`w-9 h-5 rounded-full transition-all relative cursor-pointer shrink-0
|
|
419
|
+
${plugin.enabled ? 'bg-accent-bright' : 'bg-white/[0.08]'}`}
|
|
420
|
+
>
|
|
421
|
+
<div className={`absolute top-0.5 w-4 h-4 rounded-full bg-white transition-all
|
|
422
|
+
${plugin.enabled ? 'left-[18px]' : 'left-0.5'}`} />
|
|
423
|
+
</div>
|
|
424
|
+
{allowDelete && (
|
|
425
|
+
<button
|
|
426
|
+
onClick={(e) => onDelete(e, plugin.filename, plugin.name)}
|
|
427
|
+
className="text-text-3/30 hover:text-red-400 transition-colors p-0.5 opacity-0 group-hover:opacity-100"
|
|
428
|
+
title="Delete"
|
|
429
|
+
>
|
|
430
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
431
|
+
<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" />
|
|
432
|
+
</svg>
|
|
433
|
+
</button>
|
|
203
434
|
)}
|
|
204
435
|
</div>
|
|
205
436
|
</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/
|
|
437
|
+
|
|
438
|
+
{/* Description */}
|
|
439
|
+
<p className="text-[12px] text-text-3/60 leading-relaxed line-clamp-2 mb-2.5">
|
|
440
|
+
{pluginDescription(plugin)}
|
|
441
|
+
</p>
|
|
442
|
+
|
|
443
|
+
{/* Badges */}
|
|
444
|
+
<div className="flex items-center gap-1.5 flex-wrap">
|
|
445
|
+
{badges.map((badge) => (
|
|
446
|
+
<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
447
|
{badge}
|
|
217
448
|
</span>
|
|
218
449
|
))}
|
|
450
|
+
{plugin.hasDependencyManifest && (
|
|
451
|
+
<span className={`text-[10px] font-700 px-1.5 py-0.5 rounded-full ${
|
|
452
|
+
plugin.dependencyInstallStatus === 'installed'
|
|
453
|
+
? 'text-emerald-400 bg-emerald-500/10'
|
|
454
|
+
: plugin.dependencyInstallStatus === 'error'
|
|
455
|
+
? 'text-red-400 bg-red-500/10'
|
|
456
|
+
: 'text-amber-400 bg-amber-500/10'
|
|
457
|
+
}`}>
|
|
458
|
+
deps {plugin.dependencyInstallStatus || 'ready'}
|
|
459
|
+
</span>
|
|
460
|
+
)}
|
|
461
|
+
{plugin.author && (
|
|
462
|
+
<span className="text-[10px] text-text-3/40 ml-auto">
|
|
463
|
+
{plugin.author}
|
|
464
|
+
</span>
|
|
465
|
+
)}
|
|
219
466
|
</div>
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
)}
|
|
467
|
+
|
|
468
|
+
{/* Failure warning */}
|
|
223
469
|
{plugin.autoDisabled && (
|
|
224
|
-
<p className="mt-
|
|
470
|
+
<p className="mt-2 text-[11px] text-amber-400/90 line-clamp-2">
|
|
225
471
|
Auto-disabled after {plugin.failureCount ?? 0} failures
|
|
226
472
|
{plugin.lastFailureStage ? ` (${plugin.lastFailureStage})` : ''}.
|
|
227
473
|
{plugin.lastFailureError ? ` ${plugin.lastFailureError}` : ''}
|
|
@@ -229,186 +475,173 @@ export function PluginList({ inSidebar }: { inSidebar?: boolean }) {
|
|
|
229
475
|
)}
|
|
230
476
|
</div>
|
|
231
477
|
)
|
|
478
|
+
}
|
|
232
479
|
|
|
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>
|
|
480
|
+
// --- Sidebar card (compact) ---
|
|
237
481
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
482
|
+
function SidebarPluginCard({ plugin, onEdit }: { plugin: PluginMeta; onEdit: (filename: string) => void }) {
|
|
483
|
+
return (
|
|
484
|
+
<div
|
|
485
|
+
role="button"
|
|
486
|
+
tabIndex={0}
|
|
487
|
+
onClick={() => onEdit(plugin.filename)}
|
|
488
|
+
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onEdit(plugin.filename) } }}
|
|
489
|
+
className="w-full text-left p-3 rounded-[12px] border border-white/[0.06] bg-surface hover:bg-surface-2 transition-all cursor-pointer"
|
|
490
|
+
>
|
|
491
|
+
<div className="flex items-center justify-between mb-0.5">
|
|
492
|
+
<span className="font-display text-[13px] font-600 text-text truncate">{plugin.name}</span>
|
|
493
|
+
<span className={`text-[10px] font-600 px-1.5 py-0.5 rounded-full ${
|
|
494
|
+
plugin.enabled ? 'text-emerald-400 bg-emerald-400/10' : 'text-text-3/50 bg-white/[0.04]'
|
|
495
|
+
}`}>
|
|
496
|
+
{plugin.enabled ? 'On' : 'Off'}
|
|
497
|
+
</span>
|
|
498
|
+
</div>
|
|
499
|
+
<p className="text-[11px] text-text-3/50 line-clamp-1">{pluginDescription(plugin)}</p>
|
|
500
|
+
</div>
|
|
501
|
+
)
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// --- Highlight text helper ---
|
|
505
|
+
|
|
506
|
+
function HighlightText({ text, highlight }: { text: string; highlight: string }) {
|
|
507
|
+
if (!highlight.trim()) return <>{text}</>
|
|
508
|
+
const idx = text.toLowerCase().indexOf(highlight.toLowerCase())
|
|
509
|
+
if (idx === -1) return <>{text}</>
|
|
510
|
+
return (
|
|
511
|
+
<>
|
|
512
|
+
{text.slice(0, idx)}
|
|
513
|
+
<span className="text-accent-bright">{text.slice(idx, idx + highlight.length)}</span>
|
|
514
|
+
{text.slice(idx + highlight.length)}
|
|
515
|
+
</>
|
|
516
|
+
)
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// --- Marketplace tab ---
|
|
520
|
+
|
|
521
|
+
function MarketplaceTab({ marketplace, loading, installing, installedFilenames, search, activeTag, setActiveTag, sort, setSort, onInstall }: {
|
|
522
|
+
marketplace: MarketplacePlugin[]
|
|
523
|
+
loading: boolean
|
|
524
|
+
installing: string | null
|
|
525
|
+
installedFilenames: Set<string>
|
|
526
|
+
search: string
|
|
527
|
+
activeTag: string | null
|
|
528
|
+
setActiveTag: (v: string | null) => void
|
|
529
|
+
sort: 'name' | 'downloads'
|
|
530
|
+
setSort: (v: 'name' | 'downloads') => void
|
|
531
|
+
onInstall: (p: MarketplacePlugin) => void
|
|
532
|
+
}) {
|
|
533
|
+
if (loading) return <p className="text-[12px] text-text-3/70 py-8 text-center">Loading marketplace...</p>
|
|
247
534
|
|
|
535
|
+
if (marketplace.length === 0) {
|
|
248
536
|
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>
|
|
537
|
+
<div className="text-center py-16">
|
|
538
|
+
<div className="inline-flex items-center justify-center w-12 h-12 rounded-full bg-white/[0.03] mb-3">
|
|
539
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="text-text-3/30">
|
|
540
|
+
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" strokeLinecap="round" strokeLinejoin="round" />
|
|
541
|
+
<polyline points="9,22 9,12 15,12 15,22" strokeLinecap="round" strokeLinejoin="round" />
|
|
542
|
+
</svg>
|
|
287
543
|
</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
|
-
)}
|
|
544
|
+
<p className="text-[13px] text-text-3/50">No plugins available in the marketplace</p>
|
|
339
545
|
</div>
|
|
340
546
|
)
|
|
341
547
|
}
|
|
342
548
|
|
|
549
|
+
const allTags = Array.from(new Set(marketplace.flatMap((p) => p.tags ?? []))).sort()
|
|
550
|
+
const q = search.toLowerCase()
|
|
551
|
+
const filtered = marketplace
|
|
552
|
+
.filter((p) => {
|
|
553
|
+
if (q && !p.name.toLowerCase().includes(q) && !p.description.toLowerCase().includes(q) && !(p.tags ?? []).some((t) => t.toLowerCase().includes(q))) return false
|
|
554
|
+
if (activeTag && !(p.tags ?? []).includes(activeTag)) return false
|
|
555
|
+
return true
|
|
556
|
+
})
|
|
557
|
+
.sort((a, b) => sort === 'downloads' ? (b.downloads ?? 0) - (a.downloads ?? 0) : a.name.localeCompare(b.name))
|
|
558
|
+
|
|
343
559
|
return (
|
|
344
|
-
<div className=
|
|
345
|
-
{/*
|
|
346
|
-
|
|
347
|
-
<
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
560
|
+
<div className="space-y-3">
|
|
561
|
+
{/* Tags + Sort */}
|
|
562
|
+
<div className="flex items-center gap-1.5 flex-wrap">
|
|
563
|
+
<button
|
|
564
|
+
onClick={() => setActiveTag(null)}
|
|
565
|
+
className={`px-2 py-1 rounded-[6px] text-[10px] font-600 cursor-pointer transition-all border-none ${
|
|
566
|
+
!activeTag ? 'bg-accent-soft text-accent-bright' : 'bg-white/[0.03] text-text-3/60 hover:text-text-3'
|
|
567
|
+
}`}
|
|
568
|
+
>
|
|
569
|
+
All
|
|
570
|
+
</button>
|
|
571
|
+
{allTags.map((t) => (
|
|
572
|
+
<button
|
|
573
|
+
key={t}
|
|
574
|
+
onClick={() => setActiveTag(activeTag === t ? null : t)}
|
|
575
|
+
className={`px-2 py-1 rounded-[6px] text-[10px] font-600 cursor-pointer transition-all border-none ${
|
|
576
|
+
activeTag === t ? 'bg-accent-soft text-accent-bright' : 'bg-white/[0.03] text-text-3/60 hover:text-text-3'
|
|
577
|
+
}`}
|
|
578
|
+
>
|
|
579
|
+
{t}
|
|
353
580
|
</button>
|
|
354
|
-
|
|
355
|
-
|
|
581
|
+
))}
|
|
582
|
+
<div className="flex-1" />
|
|
583
|
+
<select
|
|
584
|
+
value={sort}
|
|
585
|
+
onChange={(e) => setSort(e.target.value as 'name' | 'downloads')}
|
|
586
|
+
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"
|
|
587
|
+
style={{ fontFamily: 'inherit' }}
|
|
588
|
+
>
|
|
589
|
+
<option value="downloads">Popular</option>
|
|
590
|
+
<option value="name">A-Z</option>
|
|
591
|
+
</select>
|
|
592
|
+
</div>
|
|
356
593
|
|
|
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))}
|
|
594
|
+
{filtered.length === 0 ? (
|
|
595
|
+
<p className="text-[12px] text-text-3/50 text-center py-4">No plugins match your search</p>
|
|
596
|
+
) : (
|
|
597
|
+
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3">
|
|
598
|
+
{filtered.map((p) => {
|
|
599
|
+
const isInstalled = installedFilenames.has(`${p.id}.js`)
|
|
600
|
+
return (
|
|
601
|
+
<div key={p.id} className="py-3.5 px-4 rounded-[14px] bg-surface border border-white/[0.06]">
|
|
602
|
+
<div className="flex items-start gap-3">
|
|
603
|
+
<div className="flex-1 min-w-0">
|
|
604
|
+
<div className="flex items-center gap-2">
|
|
605
|
+
<span className="text-[14px] font-600 text-text">{p.name}</span>
|
|
606
|
+
<span className="text-[10px] font-mono text-text-3/70">v{p.version}</span>
|
|
607
|
+
{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>}
|
|
608
|
+
</div>
|
|
609
|
+
<div className="text-[11px] text-text-3/60 mt-1 line-clamp-2">{p.description}</div>
|
|
610
|
+
<div className="flex items-center gap-2 mt-2">
|
|
611
|
+
<span className="text-[10px] text-text-3/70">by {p.author}</span>
|
|
612
|
+
<span className="text-[10px] text-text-3/50">·</span>
|
|
613
|
+
{(p.tags ?? []).slice(0, 3).map((t) => (
|
|
614
|
+
<button
|
|
615
|
+
key={t}
|
|
616
|
+
onClick={() => setActiveTag(activeTag === t ? null : t)}
|
|
617
|
+
className={`text-[9px] font-600 px-1.5 py-0.5 rounded-full cursor-pointer transition-all border-none ${
|
|
618
|
+
activeTag === t ? 'text-accent-bright bg-accent-soft' : 'text-text-3/50 bg-white/[0.04] hover:text-text-3'
|
|
619
|
+
}`}
|
|
620
|
+
>
|
|
621
|
+
{t}
|
|
622
|
+
</button>
|
|
623
|
+
))}
|
|
624
|
+
</div>
|
|
396
625
|
</div>
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
626
|
+
<button
|
|
627
|
+
onClick={() => !isInstalled && onInstall(p)}
|
|
628
|
+
disabled={isInstalled || installing === p.id}
|
|
629
|
+
className={`shrink-0 py-2 px-4 rounded-[10px] text-[12px] font-600 transition-all cursor-pointer
|
|
630
|
+
${isInstalled
|
|
631
|
+
? 'bg-white/[0.04] text-text-3/70 cursor-default'
|
|
632
|
+
: installing === p.id
|
|
633
|
+
? 'bg-accent-soft text-accent-bright animate-pulse'
|
|
634
|
+
: 'bg-accent-soft text-accent-bright hover:bg-accent-soft/80 border border-accent-bright/20'}`}
|
|
635
|
+
style={{ fontFamily: 'inherit' }}
|
|
636
|
+
>
|
|
637
|
+
{isInstalled ? 'Installed' : installing === p.id ? 'Installing...' : 'Install'}
|
|
638
|
+
</button>
|
|
639
|
+
</div>
|
|
640
|
+
</div>
|
|
641
|
+
)
|
|
642
|
+
})}
|
|
643
|
+
</div>
|
|
402
644
|
)}
|
|
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
645
|
</div>
|
|
413
646
|
)
|
|
414
647
|
}
|