@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.
Files changed (237) hide show
  1. package/README.md +155 -150
  2. package/package.json +1 -1
  3. package/src/app/api/agents/[id]/route.ts +26 -0
  4. package/src/app/api/agents/[id]/thread/route.ts +37 -9
  5. package/src/app/api/agents/route.ts +13 -2
  6. package/src/app/api/auth/route.ts +76 -7
  7. package/src/app/api/chatrooms/[id]/chat/route.ts +7 -2
  8. package/src/app/api/{sessions → chats}/[id]/browser/route.ts +5 -1
  9. package/src/app/api/{sessions → chats}/[id]/chat/route.ts +7 -3
  10. package/src/app/api/{sessions → chats}/[id]/checkpoints/route.ts +1 -1
  11. package/src/app/api/chats/[id]/main-loop/route.ts +13 -0
  12. package/src/app/api/{sessions → chats}/[id]/messages/route.ts +19 -13
  13. package/src/app/api/{sessions → chats}/[id]/restore/route.ts +1 -1
  14. package/src/app/api/{sessions → chats}/[id]/route.ts +22 -52
  15. package/src/app/api/{sessions → chats}/[id]/stop/route.ts +6 -1
  16. package/src/app/api/{sessions → chats}/route.ts +21 -7
  17. package/src/app/api/connectors/[id]/doctor/route.ts +26 -0
  18. package/src/app/api/connectors/doctor/route.ts +13 -0
  19. package/src/app/api/files/open/route.ts +16 -14
  20. package/src/app/api/memory/maintenance/route.ts +11 -1
  21. package/src/app/api/openclaw/agent-files/route.ts +27 -4
  22. package/src/app/api/openclaw/skills/route.ts +11 -3
  23. package/src/app/api/plugins/dependencies/route.ts +24 -0
  24. package/src/app/api/plugins/install/route.ts +15 -92
  25. package/src/app/api/plugins/route.ts +6 -26
  26. package/src/app/api/plugins/settings/route.ts +40 -0
  27. package/src/app/api/plugins/ui/route.ts +1 -0
  28. package/src/app/api/settings/route.ts +49 -7
  29. package/src/app/api/tasks/[id]/route.ts +15 -6
  30. package/src/app/api/tasks/bulk/route.ts +2 -2
  31. package/src/app/api/tasks/route.ts +9 -4
  32. package/src/app/api/usage/route.ts +30 -0
  33. package/src/app/api/webhooks/[id]/route.ts +8 -1
  34. package/src/app/page.tsx +9 -2
  35. package/src/cli/index.js +39 -33
  36. package/src/cli/index.ts +43 -49
  37. package/src/cli/spec.js +29 -27
  38. package/src/components/agents/agent-card.tsx +16 -13
  39. package/src/components/agents/agent-chat-list.tsx +104 -4
  40. package/src/components/agents/agent-list.tsx +54 -22
  41. package/src/components/agents/agent-sheet.tsx +209 -18
  42. package/src/components/agents/cron-job-form.tsx +3 -3
  43. package/src/components/agents/inspector-panel.tsx +110 -50
  44. package/src/components/auth/access-key-gate.tsx +36 -97
  45. package/src/components/auth/setup-wizard.tsx +5 -38
  46. package/src/components/chat/chat-area.tsx +39 -27
  47. package/src/components/{sessions/session-card.tsx → chat/chat-card.tsx} +7 -23
  48. package/src/components/chat/chat-header.tsx +299 -314
  49. package/src/components/{sessions/session-list.tsx → chat/chat-list.tsx} +11 -14
  50. package/src/components/chat/chat-tool-toggles.tsx +26 -17
  51. package/src/components/chat/checkpoint-timeline.tsx +4 -4
  52. package/src/components/chat/message-bubble.tsx +4 -1
  53. package/src/components/chat/message-list.tsx +5 -3
  54. package/src/components/chat/session-debug-panel.tsx +1 -1
  55. package/src/components/chat/tool-request-banner.tsx +3 -3
  56. package/src/components/chatrooms/agent-hover-card.tsx +3 -3
  57. package/src/components/chatrooms/chatroom-tool-request-banner.tsx +2 -2
  58. package/src/components/chatrooms/chatroom-view.tsx +347 -205
  59. package/src/components/connectors/connector-list.tsx +265 -127
  60. package/src/components/connectors/connector-sheet.tsx +218 -1
  61. package/src/components/home/home-view.tsx +129 -5
  62. package/src/components/layout/app-layout.tsx +392 -182
  63. package/src/components/layout/mobile-header.tsx +26 -8
  64. package/src/components/plugins/plugin-list.tsx +487 -254
  65. package/src/components/plugins/plugin-sheet.tsx +236 -13
  66. package/src/components/projects/project-detail.tsx +183 -0
  67. package/src/components/settings/gateway-connection-panel.tsx +1 -1
  68. package/src/components/shared/agent-picker-list.tsx +2 -2
  69. package/src/components/shared/command-palette.tsx +111 -25
  70. package/src/components/shared/settings/plugin-manager.tsx +20 -4
  71. package/src/components/shared/settings/section-capability-policy.tsx +105 -0
  72. package/src/components/shared/settings/section-heartbeat.tsx +78 -1
  73. package/src/components/shared/settings/section-orchestrator.tsx +3 -3
  74. package/src/components/shared/settings/section-providers.tsx +1 -1
  75. package/src/components/shared/settings/section-runtime-loop.tsx +5 -5
  76. package/src/components/shared/settings/section-secrets.tsx +6 -6
  77. package/src/components/shared/settings/section-user-preferences.tsx +1 -1
  78. package/src/components/shared/settings/section-voice.tsx +5 -1
  79. package/src/components/shared/settings/section-web-search.tsx +10 -2
  80. package/src/components/shared/settings/settings-page.tsx +244 -56
  81. package/src/components/tasks/approvals-panel.tsx +205 -18
  82. package/src/components/tasks/task-board.tsx +242 -46
  83. package/src/components/usage/metrics-dashboard.tsx +147 -1
  84. package/src/components/wallets/wallet-panel.tsx +17 -5
  85. package/src/components/webhooks/webhook-sheet.tsx +8 -8
  86. package/src/lib/auth.ts +17 -0
  87. package/src/lib/chat-streaming-state.test.ts +108 -0
  88. package/src/lib/chat-streaming-state.ts +108 -0
  89. package/src/lib/chat.ts +1 -1
  90. package/src/lib/{sessions.ts → chats.ts} +28 -18
  91. package/src/lib/openclaw-agent-id.test.ts +14 -0
  92. package/src/lib/openclaw-agent-id.ts +31 -0
  93. package/src/lib/providers/claude-cli.ts +1 -1
  94. package/src/lib/server/agent-assignment.test.ts +112 -0
  95. package/src/lib/server/agent-assignment.ts +169 -0
  96. package/src/lib/server/approval-connector-notify.test.ts +253 -0
  97. package/src/lib/server/approvals-auto-approve.test.ts +205 -0
  98. package/src/lib/server/approvals.ts +483 -75
  99. package/src/lib/server/autonomy-runtime.test.ts +341 -0
  100. package/src/lib/server/browser-state.test.ts +118 -0
  101. package/src/lib/server/browser-state.ts +123 -0
  102. package/src/lib/server/build-llm.test.ts +36 -0
  103. package/src/lib/server/build-llm.ts +11 -4
  104. package/src/lib/server/builtin-plugins.ts +34 -0
  105. package/src/lib/server/capability-router.ts +10 -8
  106. package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
  107. package/src/lib/server/chat-execution-tool-events.test.ts +134 -0
  108. package/src/lib/server/chat-execution.ts +285 -165
  109. package/src/lib/server/chatroom-health.test.ts +26 -0
  110. package/src/lib/server/chatroom-health.ts +2 -3
  111. package/src/lib/server/chatroom-helpers.test.ts +67 -2
  112. package/src/lib/server/chatroom-helpers.ts +48 -8
  113. package/src/lib/server/connectors/discord.ts +175 -11
  114. package/src/lib/server/connectors/doctor.test.ts +80 -0
  115. package/src/lib/server/connectors/doctor.ts +116 -0
  116. package/src/lib/server/connectors/manager.ts +948 -112
  117. package/src/lib/server/connectors/policy.test.ts +222 -0
  118. package/src/lib/server/connectors/policy.ts +452 -0
  119. package/src/lib/server/connectors/slack.ts +188 -9
  120. package/src/lib/server/connectors/telegram.ts +65 -15
  121. package/src/lib/server/connectors/thread-context.test.ts +44 -0
  122. package/src/lib/server/connectors/thread-context.ts +72 -0
  123. package/src/lib/server/connectors/types.ts +41 -11
  124. package/src/lib/server/cost.ts +34 -1
  125. package/src/lib/server/daemon-state.ts +61 -3
  126. package/src/lib/server/data-dir.ts +13 -0
  127. package/src/lib/server/delegation-jobs.test.ts +140 -0
  128. package/src/lib/server/delegation-jobs.ts +248 -0
  129. package/src/lib/server/document-utils.test.ts +47 -0
  130. package/src/lib/server/document-utils.ts +397 -0
  131. package/src/lib/server/heartbeat-service.ts +14 -40
  132. package/src/lib/server/heartbeat-source.test.ts +22 -0
  133. package/src/lib/server/heartbeat-source.ts +7 -0
  134. package/src/lib/server/identity-continuity.test.ts +77 -0
  135. package/src/lib/server/identity-continuity.ts +127 -0
  136. package/src/lib/server/mailbox-utils.ts +347 -0
  137. package/src/lib/server/main-agent-loop.ts +28 -1103
  138. package/src/lib/server/memory-db.ts +4 -6
  139. package/src/lib/server/memory-tiers.ts +40 -0
  140. package/src/lib/server/openclaw-agent-resolver.test.ts +70 -0
  141. package/src/lib/server/openclaw-agent-resolver.ts +128 -0
  142. package/src/lib/server/openclaw-exec-config.ts +5 -6
  143. package/src/lib/server/openclaw-skills-normalize.test.ts +56 -0
  144. package/src/lib/server/openclaw-skills-normalize.ts +136 -0
  145. package/src/lib/server/openclaw-sync.ts +3 -2
  146. package/src/lib/server/orchestrator-lg.ts +20 -9
  147. package/src/lib/server/orchestrator.ts +7 -7
  148. package/src/lib/server/playwright-proxy.mjs +27 -3
  149. package/src/lib/server/plugins.test.ts +207 -0
  150. package/src/lib/server/plugins.ts +927 -66
  151. package/src/lib/server/provider-health.ts +38 -6
  152. package/src/lib/server/queue.ts +13 -28
  153. package/src/lib/server/scheduler.ts +2 -0
  154. package/src/lib/server/session-archive-memory.test.ts +85 -0
  155. package/src/lib/server/session-archive-memory.ts +230 -0
  156. package/src/lib/server/session-mailbox.ts +8 -18
  157. package/src/lib/server/session-reset-policy.test.ts +99 -0
  158. package/src/lib/server/session-reset-policy.ts +311 -0
  159. package/src/lib/server/session-run-manager.ts +33 -82
  160. package/src/lib/server/session-tools/autonomy-tools.test.ts +105 -0
  161. package/src/lib/server/session-tools/calendar.ts +366 -0
  162. package/src/lib/server/session-tools/canvas.ts +1 -1
  163. package/src/lib/server/session-tools/chatroom.ts +4 -2
  164. package/src/lib/server/session-tools/connector.ts +114 -10
  165. package/src/lib/server/session-tools/context.ts +21 -5
  166. package/src/lib/server/session-tools/crawl.ts +447 -0
  167. package/src/lib/server/session-tools/crud.ts +74 -28
  168. package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
  169. package/src/lib/server/session-tools/delegate.ts +497 -24
  170. package/src/lib/server/session-tools/discovery.ts +24 -6
  171. package/src/lib/server/session-tools/document.ts +283 -0
  172. package/src/lib/server/session-tools/edit_file.ts +4 -2
  173. package/src/lib/server/session-tools/email.ts +320 -0
  174. package/src/lib/server/session-tools/extract.ts +137 -0
  175. package/src/lib/server/session-tools/file-normalize.test.ts +93 -0
  176. package/src/lib/server/session-tools/file-send.test.ts +84 -1
  177. package/src/lib/server/session-tools/file.ts +241 -25
  178. package/src/lib/server/session-tools/git.ts +1 -1
  179. package/src/lib/server/session-tools/http.ts +1 -1
  180. package/src/lib/server/session-tools/human-loop.ts +227 -0
  181. package/src/lib/server/session-tools/image-gen.ts +380 -0
  182. package/src/lib/server/session-tools/index.ts +130 -50
  183. package/src/lib/server/session-tools/mailbox.ts +276 -0
  184. package/src/lib/server/session-tools/memory.ts +172 -3
  185. package/src/lib/server/session-tools/monitor.ts +151 -8
  186. package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
  187. package/src/lib/server/session-tools/openclaw-nodes.ts +1 -1
  188. package/src/lib/server/session-tools/openclaw-workspace.ts +1 -1
  189. package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
  190. package/src/lib/server/session-tools/platform.ts +148 -7
  191. package/src/lib/server/session-tools/plugin-creator.ts +89 -26
  192. package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
  193. package/src/lib/server/session-tools/replicate.ts +301 -0
  194. package/src/lib/server/session-tools/sample-ui.ts +1 -1
  195. package/src/lib/server/session-tools/sandbox.ts +4 -2
  196. package/src/lib/server/session-tools/schedule.ts +24 -12
  197. package/src/lib/server/session-tools/session-info.ts +43 -7
  198. package/src/lib/server/session-tools/session-tools-wiring.test.ts +31 -17
  199. package/src/lib/server/session-tools/shell.ts +5 -2
  200. package/src/lib/server/session-tools/subagent.ts +194 -28
  201. package/src/lib/server/session-tools/table.ts +587 -0
  202. package/src/lib/server/session-tools/wallet.ts +42 -12
  203. package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
  204. package/src/lib/server/session-tools/web.ts +926 -91
  205. package/src/lib/server/storage.ts +255 -16
  206. package/src/lib/server/stream-agent-chat.ts +116 -268
  207. package/src/lib/server/structured-extract.test.ts +72 -0
  208. package/src/lib/server/structured-extract.ts +373 -0
  209. package/src/lib/server/task-mention.test.ts +16 -2
  210. package/src/lib/server/task-mention.ts +61 -10
  211. package/src/lib/server/tool-aliases.ts +66 -18
  212. package/src/lib/server/tool-capability-policy.test.ts +9 -9
  213. package/src/lib/server/tool-capability-policy.ts +38 -27
  214. package/src/lib/server/tool-retry.ts +2 -0
  215. package/src/lib/server/watch-jobs.test.ts +173 -0
  216. package/src/lib/server/watch-jobs.ts +532 -0
  217. package/src/lib/server/ws-hub.ts +5 -3
  218. package/src/lib/tool-definitions.ts +4 -0
  219. package/src/lib/validation/schemas.test.ts +26 -0
  220. package/src/lib/validation/schemas.ts +10 -1
  221. package/src/lib/ws-client.ts +14 -12
  222. package/src/proxy.ts +5 -5
  223. package/src/stores/use-app-store.ts +5 -11
  224. package/src/stores/use-chat-store.ts +38 -9
  225. package/src/types/index.ts +352 -47
  226. package/src/app/api/sessions/[id]/main-loop/route.ts +0 -94
  227. package/src/components/sessions/new-session-sheet.tsx +0 -253
  228. package/src/lib/server/main-session.ts +0 -24
  229. package/src/lib/server/session-run-manager.test.ts +0 -23
  230. /package/src/app/api/{sessions → chats}/[id]/clear/route.ts +0 -0
  231. /package/src/app/api/{sessions → chats}/[id]/deploy/route.ts +0 -0
  232. /package/src/app/api/{sessions → chats}/[id]/devserver/route.ts +0 -0
  233. /package/src/app/api/{sessions → chats}/[id]/edit-resend/route.ts +0 -0
  234. /package/src/app/api/{sessions → chats}/[id]/fork/route.ts +0 -0
  235. /package/src/app/api/{sessions → chats}/[id]/mailbox/route.ts +0 -0
  236. /package/src/app/api/{sessions → chats}/[id]/retry/route.ts +0 -0
  237. /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<'installed' | 'marketplace'>('installed')
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 !== 'marketplace') return
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((plugin) => plugin.source === 'local')
66
- const extensionPlugins = pluginList.filter((plugin) => plugin.source !== 'local')
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
- const tabClass = (t: string) =>
121
- `py-1.5 px-3.5 rounded-[8px] text-[12px] font-600 cursor-pointer transition-all border
122
- ${tab === t
123
- ? 'bg-accent-soft border-accent-bright/25 text-accent-bright'
124
- : 'bg-transparent border-transparent text-text-3 hover:text-text-2'}`
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}.`
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
- 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
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
- const renderInstalledPlugin = (plugin: (typeof pluginList)[number], allowDelete: boolean) => (
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={() => 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"
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
- <div className="flex items-center justify-between mb-1">
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
- {plugin.createdByAgentId && agents[plugin.createdByAgentId] && (
393
+ {agent && (
160
394
  <button
161
395
  type="button"
162
- title={`Created by ${agents[plugin.createdByAgentId].name} — click to open chat`}
163
- onClick={(e) => { e.stopPropagation(); navigateToAgentChat(plugin.createdByAgentId!) }}
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={agents[plugin.createdByAgentId].avatarSeed || null}
168
- avatarUrl={agents[plugin.createdByAgentId].avatarUrl}
169
- name={agents[plugin.createdByAgentId].name || 'Agent'}
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">{plugin.name}</span>
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 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>
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
- <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]">
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
- {!inSidebar && (
221
- <p className="text-[10px] text-text-3/50 mt-2">Click for full details and controls</p>
222
- )}
467
+
468
+ {/* Failure warning */}
223
469
  {plugin.autoDisabled && (
224
- <p className="mt-1 text-[11px] text-amber-400/90 line-clamp-2">
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
- // Marketplace tab content (full-width only)
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
- const allTags = Array.from(new Set(marketplace.flatMap((p) => p.tags ?? []))).sort()
239
- const q = search.toLowerCase()
240
- const filtered = marketplace
241
- .filter((p) => {
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
244
- return true
245
- })
246
- .sort((a, b) => sort === 'downloads' ? (b.downloads ?? 0) - (a.downloads ?? 0) : a.name.localeCompare(b.name))
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="space-y-3">
250
- <input
251
- value={search}
252
- onChange={(e) => setSearch(e.target.value)}
253
- placeholder="Search plugins..."
254
- className="w-full px-3 py-2.5 rounded-[10px] bg-surface border border-white/[0.06] text-[12px] text-text placeholder:text-text-3/50 outline-none focus:border-accent-bright/30"
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
- {filtered.length === 0 ? (
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">&middot;</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={`flex-1 overflow-y-auto ${inSidebar ? 'px-3 pb-4' : 'px-5 pb-6'}`}>
345
- {/* Tabs full-width only */}
346
- {!inSidebar && (
347
- <div className="flex gap-1 mb-4">
348
- <button onClick={() => setTab('installed')} className={tabClass('installed')} style={{ fontFamily: 'inherit' }}>
349
- Installed
350
- </button>
351
- <button onClick={() => setTab('marketplace')} className={tabClass('marketplace')} style={{ fontFamily: 'inherit' }}>
352
- SwarmForge
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
- </div>
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
- {(!inSidebar && tab === 'marketplace') ? renderMarketplace() : (
358
- pluginList.length === 0 ? (
359
- <div className="text-center py-12">
360
- <p className="text-[13px] text-text-3/60">No plugins installed</p>
361
- <button
362
- onClick={() => { setEditingPluginFilename(null); setPluginSheetOpen(true) }}
363
- className="mt-3 px-4 py-2 rounded-[10px] bg-transparent text-accent-bright text-[13px] font-600 cursor-pointer border border-accent-bright/20 hover:bg-accent-soft transition-all"
364
- style={{ fontFamily: 'inherit' }}
365
- >
366
- + Add Plugin
367
- </button>
368
- </div>
369
- ) : (
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>
381
- </div>
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))}
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">&middot;</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
- </section>
398
- )}
399
- </div>
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
  }