@swarmclawai/swarmclaw 0.6.8 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/README.md +70 -45
  2. package/next.config.ts +31 -6
  3. package/package.json +3 -2
  4. package/src/app/api/agents/[id]/thread/route.ts +1 -0
  5. package/src/app/api/agents/route.ts +18 -5
  6. package/src/app/api/approvals/route.ts +22 -0
  7. package/src/app/api/clawhub/install/route.ts +2 -2
  8. package/src/app/api/mcp-servers/[id]/conformance/route.ts +26 -0
  9. package/src/app/api/mcp-servers/[id]/invoke/route.ts +81 -0
  10. package/src/app/api/memory/route.ts +36 -5
  11. package/src/app/api/notifications/route.ts +3 -0
  12. package/src/app/api/plugins/install/route.ts +57 -5
  13. package/src/app/api/plugins/marketplace/route.ts +73 -22
  14. package/src/app/api/plugins/route.ts +61 -1
  15. package/src/app/api/plugins/ui/route.ts +34 -0
  16. package/src/app/api/settings/route.ts +62 -0
  17. package/src/app/api/setup/doctor/route.ts +22 -5
  18. package/src/app/api/tasks/[id]/approve/route.ts +4 -3
  19. package/src/app/api/tasks/[id]/route.ts +11 -3
  20. package/src/app/api/tasks/route.ts +8 -2
  21. package/src/app/globals.css +27 -0
  22. package/src/app/page.tsx +10 -5
  23. package/src/cli/index.js +13 -0
  24. package/src/components/activity/activity-feed.tsx +9 -2
  25. package/src/components/agents/agent-avatar.tsx +5 -1
  26. package/src/components/agents/agent-card.tsx +55 -9
  27. package/src/components/agents/agent-sheet.tsx +86 -29
  28. package/src/components/agents/inspector-panel.tsx +1 -1
  29. package/src/components/auth/access-key-gate.tsx +63 -54
  30. package/src/components/auth/user-picker.tsx +37 -32
  31. package/src/components/chat/chat-area.tsx +11 -0
  32. package/src/components/chat/chat-header.tsx +69 -25
  33. package/src/components/chat/chat-tool-toggles.tsx +2 -2
  34. package/src/components/chat/code-block.tsx +3 -1
  35. package/src/components/chat/exec-approval-card.tsx +8 -1
  36. package/src/components/chat/message-bubble.tsx +164 -4
  37. package/src/components/chat/message-list.tsx +30 -4
  38. package/src/components/chat/session-approval-card.tsx +80 -0
  39. package/src/components/chat/streaming-bubble.tsx +6 -5
  40. package/src/components/chat/thinking-indicator.tsx +48 -12
  41. package/src/components/chat/tool-request-banner.tsx +39 -20
  42. package/src/components/chatrooms/chatroom-list.tsx +11 -4
  43. package/src/components/chatrooms/chatroom-sheet.tsx +7 -2
  44. package/src/components/connectors/connector-list.tsx +33 -11
  45. package/src/components/connectors/connector-sheet.tsx +29 -6
  46. package/src/components/home/home-view.tsx +20 -14
  47. package/src/components/input/chat-input.tsx +22 -1
  48. package/src/components/knowledge/knowledge-list.tsx +17 -18
  49. package/src/components/knowledge/knowledge-sheet.tsx +9 -5
  50. package/src/components/layout/app-layout.tsx +73 -21
  51. package/src/components/mcp-servers/mcp-server-list.tsx +352 -50
  52. package/src/components/mcp-servers/mcp-server-sheet.tsx +25 -9
  53. package/src/components/memory/memory-list.tsx +20 -13
  54. package/src/components/plugins/plugin-list.tsx +213 -59
  55. package/src/components/plugins/plugin-sheet.tsx +119 -24
  56. package/src/components/projects/project-list.tsx +17 -9
  57. package/src/components/providers/provider-list.tsx +21 -6
  58. package/src/components/providers/provider-sheet.tsx +42 -25
  59. package/src/components/runs/run-list.tsx +17 -13
  60. package/src/components/schedules/schedule-card.tsx +10 -3
  61. package/src/components/schedules/schedule-list.tsx +2 -2
  62. package/src/components/schedules/schedule-sheet.tsx +19 -7
  63. package/src/components/secrets/secret-sheet.tsx +7 -2
  64. package/src/components/secrets/secrets-list.tsx +18 -5
  65. package/src/components/sessions/new-session-sheet.tsx +183 -376
  66. package/src/components/sessions/session-card.tsx +10 -2
  67. package/src/components/settings/gateway-connection-panel.tsx +9 -8
  68. package/src/components/shared/command-palette.tsx +13 -5
  69. package/src/components/shared/empty-state.tsx +20 -8
  70. package/src/components/shared/notification-center.tsx +134 -86
  71. package/src/components/shared/profile-sheet.tsx +4 -0
  72. package/src/components/shared/settings/plugin-manager.tsx +360 -135
  73. package/src/components/shared/settings/section-capability-policy.tsx +3 -3
  74. package/src/components/shared/settings/section-runtime-loop.tsx +144 -0
  75. package/src/components/skills/clawhub-browser.tsx +1 -0
  76. package/src/components/skills/skill-list.tsx +31 -12
  77. package/src/components/skills/skill-sheet.tsx +20 -7
  78. package/src/components/tasks/approvals-panel.tsx +170 -66
  79. package/src/components/tasks/task-board.tsx +20 -12
  80. package/src/components/tasks/task-card.tsx +21 -7
  81. package/src/components/tasks/task-column.tsx +4 -3
  82. package/src/components/tasks/task-list.tsx +1 -1
  83. package/src/components/tasks/task-sheet.tsx +130 -1
  84. package/src/components/ui/dialog.tsx +1 -0
  85. package/src/components/ui/sheet.tsx +1 -0
  86. package/src/components/usage/metrics-dashboard.tsx +66 -64
  87. package/src/components/wallets/wallet-panel.tsx +65 -41
  88. package/src/components/wallets/wallet-section.tsx +9 -3
  89. package/src/components/webhooks/webhook-list.tsx +21 -12
  90. package/src/components/webhooks/webhook-sheet.tsx +13 -3
  91. package/src/lib/approval-display.test.ts +45 -0
  92. package/src/lib/approval-display.ts +62 -0
  93. package/src/lib/clipboard.ts +38 -0
  94. package/src/lib/memory.ts +8 -0
  95. package/src/lib/providers/claude-cli.ts +5 -3
  96. package/src/lib/providers/index.ts +67 -21
  97. package/src/lib/runtime-loop.ts +3 -2
  98. package/src/lib/server/approvals.ts +150 -0
  99. package/src/lib/server/chat-execution.ts +223 -62
  100. package/src/lib/server/clawhub-client.ts +82 -6
  101. package/src/lib/server/connectors/manager.ts +27 -1
  102. package/src/lib/server/cost.test.ts +73 -0
  103. package/src/lib/server/cost.ts +165 -34
  104. package/src/lib/server/daemon-state.ts +42 -0
  105. package/src/lib/server/data-dir.ts +18 -1
  106. package/src/lib/server/integrity-monitor.ts +208 -0
  107. package/src/lib/server/llm-response-cache.test.ts +102 -0
  108. package/src/lib/server/llm-response-cache.ts +227 -0
  109. package/src/lib/server/main-agent-loop.ts +1 -1
  110. package/src/lib/server/main-session.ts +6 -3
  111. package/src/lib/server/mcp-conformance.test.ts +18 -0
  112. package/src/lib/server/mcp-conformance.ts +233 -0
  113. package/src/lib/server/memory-db.ts +180 -17
  114. package/src/lib/server/memory-retrieval.test.ts +56 -0
  115. package/src/lib/server/orchestrator-lg.ts +4 -1
  116. package/src/lib/server/orchestrator.ts +4 -3
  117. package/src/lib/server/plugins.ts +650 -142
  118. package/src/lib/server/process-manager.ts +18 -0
  119. package/src/lib/server/queue.ts +253 -11
  120. package/src/lib/server/runtime-settings.ts +9 -0
  121. package/src/lib/server/session-run-manager.test.ts +23 -0
  122. package/src/lib/server/session-run-manager.ts +11 -1
  123. package/src/lib/server/session-tools/canvas.ts +85 -50
  124. package/src/lib/server/session-tools/chatroom.ts +130 -127
  125. package/src/lib/server/session-tools/connector.ts +233 -454
  126. package/src/lib/server/session-tools/context-mgmt.ts +87 -105
  127. package/src/lib/server/session-tools/crud.ts +84 -7
  128. package/src/lib/server/session-tools/delegate.ts +351 -752
  129. package/src/lib/server/session-tools/discovery.ts +198 -0
  130. package/src/lib/server/session-tools/edit_file.ts +82 -0
  131. package/src/lib/server/session-tools/file-send.test.ts +39 -0
  132. package/src/lib/server/session-tools/file.ts +257 -425
  133. package/src/lib/server/session-tools/git.ts +87 -47
  134. package/src/lib/server/session-tools/http.ts +85 -33
  135. package/src/lib/server/session-tools/index.ts +205 -160
  136. package/src/lib/server/session-tools/memory.ts +152 -265
  137. package/src/lib/server/session-tools/monitor.ts +126 -0
  138. package/src/lib/server/session-tools/normalize-tool-args.test.ts +61 -0
  139. package/src/lib/server/session-tools/normalize-tool-args.ts +48 -0
  140. package/src/lib/server/session-tools/openclaw-nodes.ts +82 -99
  141. package/src/lib/server/session-tools/openclaw-workspace.ts +103 -93
  142. package/src/lib/server/session-tools/platform.ts +86 -0
  143. package/src/lib/server/session-tools/plugin-creator.ts +239 -0
  144. package/src/lib/server/session-tools/sample-ui.ts +97 -0
  145. package/src/lib/server/session-tools/sandbox.ts +175 -148
  146. package/src/lib/server/session-tools/schedule.ts +66 -31
  147. package/src/lib/server/session-tools/session-info.ts +104 -410
  148. package/src/lib/server/session-tools/shell-normalize.test.ts +43 -0
  149. package/src/lib/server/session-tools/shell.ts +171 -143
  150. package/src/lib/server/session-tools/subagent.ts +77 -77
  151. package/src/lib/server/session-tools/wallet.ts +182 -106
  152. package/src/lib/server/session-tools/web.ts +179 -349
  153. package/src/lib/server/storage.ts +24 -0
  154. package/src/lib/server/stream-agent-chat.ts +301 -244
  155. package/src/lib/server/task-quality-gate.test.ts +44 -0
  156. package/src/lib/server/task-quality-gate.ts +67 -0
  157. package/src/lib/server/task-validation.test.ts +78 -0
  158. package/src/lib/server/task-validation.ts +67 -2
  159. package/src/lib/server/tool-aliases.ts +68 -0
  160. package/src/lib/server/tool-capability-policy.ts +23 -5
  161. package/src/lib/tasks.ts +7 -1
  162. package/src/lib/tool-definitions.ts +23 -23
  163. package/src/lib/validation/schemas.ts +12 -0
  164. package/src/lib/view-routes.ts +2 -24
  165. package/src/stores/use-app-store.ts +23 -1
  166. package/src/types/index.ts +121 -7
@@ -1,8 +1,10 @@
1
1
  'use client'
2
2
 
3
- import { useEffect, useState, useCallback } from 'react'
3
+ import { useEffect, useState, useCallback, useMemo } from 'react'
4
4
  import { api } from '@/lib/api-client'
5
5
  import type { PluginMeta, MarketplacePlugin } from '@/types'
6
+ import { toast } from 'sonner'
7
+ import { ConfirmDialog } from '@/components/shared/confirm-dialog'
6
8
 
7
9
  export function PluginManager() {
8
10
  const [tab, setTab] = useState<'installed' | 'marketplace' | 'url'>('installed')
@@ -10,9 +12,13 @@ export function PluginManager() {
10
12
  const [marketplace, setMarketplace] = useState<MarketplacePlugin[]>([])
11
13
  const [loading, setLoading] = useState(false)
12
14
  const [installing, setInstalling] = useState<string | null>(null)
15
+ const [updating, setUpdating] = useState<string | null>(null)
16
+ const [updatingAll, setUpdatingAll] = useState(false)
13
17
  const [urlInput, setUrlInput] = useState('')
14
18
  const [urlFilename, setUrlFilename] = useState('')
15
19
  const [urlStatus, setUrlStatus] = useState<{ ok: boolean; message: string } | null>(null)
20
+ const [marketplaceQuery, setMarketplaceQuery] = useState('')
21
+ const [confirmDelete, setConfirmDelete] = useState<{ filename: string; name: string } | null>(null)
16
22
 
17
23
  const loadPlugins = useCallback(async () => {
18
24
  try {
@@ -21,33 +27,89 @@ export function PluginManager() {
21
27
  } catch { /* ignore */ }
22
28
  }, [])
23
29
 
24
- const loadMarketplace = useCallback(async () => {
30
+ const loadMarketplace = useCallback(async (q = '') => {
25
31
  setLoading(true)
26
32
  try {
27
- const data = await api<MarketplacePlugin[]>('GET', '/plugins/marketplace')
33
+ const data = await api<MarketplacePlugin[]>('GET', `/plugins/marketplace?q=${encodeURIComponent(q)}`)
28
34
  if (Array.isArray(data)) setMarketplace(data)
29
35
  } catch { /* ignore */ }
30
36
  setLoading(false)
31
37
  }, [])
32
38
 
33
- useEffect(() => { loadPlugins() }, [])
34
- useEffect(() => { if (tab === 'marketplace') loadMarketplace() }, [tab])
39
+ useEffect(() => { loadPlugins() }, [loadPlugins])
40
+ useEffect(() => { if (tab === 'marketplace') loadMarketplace(marketplaceQuery) }, [tab, loadMarketplace, marketplaceQuery])
35
41
 
36
42
  const togglePlugin = async (filename: string, enabled: boolean) => {
37
- await api('POST', '/plugins', { filename, enabled })
38
- loadPlugins()
43
+ try {
44
+ await api('POST', '/plugins', { filename, enabled })
45
+ toast.success(enabled ? 'Plugin enabled' : 'Plugin disabled')
46
+ loadPlugins()
47
+ } catch (err: unknown) {
48
+ toast.error(err instanceof Error ? err.message : 'Failed to toggle plugin')
49
+ }
50
+ }
51
+
52
+ const deletePlugin = async (filename: string, name: string) => {
53
+ try {
54
+ await api('DELETE', `/plugins?filename=${encodeURIComponent(filename)}`)
55
+ toast.success(`Deleted ${name}`)
56
+ loadPlugins()
57
+ } catch (err: unknown) {
58
+ toast.error(err instanceof Error ? err.message : 'Delete failed')
59
+ }
60
+ }
61
+
62
+ const handleUpdateOne = async (id: string) => {
63
+ setUpdating(id)
64
+ try {
65
+ await api('PATCH', `/plugins?id=${id}`)
66
+ toast.success('Plugin updated')
67
+ await loadPlugins()
68
+ } catch (err: unknown) {
69
+ toast.error(err instanceof Error ? err.message : String(err))
70
+ } finally {
71
+ setUpdating(null)
72
+ }
73
+ }
74
+
75
+ const handleUpdateAll = async () => {
76
+ setUpdatingAll(true)
77
+ try {
78
+ await api('PATCH', '/plugins?all=true')
79
+ toast.success('All plugins updated')
80
+ await loadPlugins()
81
+ } catch (err: unknown) {
82
+ toast.error(err instanceof Error ? err.message : String(err))
83
+ } finally {
84
+ setUpdatingAll(false)
85
+ }
39
86
  }
40
87
 
41
88
  const installFromMarketplace = async (p: MarketplacePlugin) => {
42
89
  setInstalling(p.id)
90
+ const toastId = toast.loading(`Installing ${p.name}...`)
43
91
  try {
44
- await api('POST', '/plugins/install', { url: p.url, filename: `${p.id}.js` })
92
+ if (!p.url) throw new Error('No functional URL found for this plugin')
93
+
94
+ const safeFilename = `${p.id.replace(/[^a-zA-Z0-9.-]/g, '_')}.js`
95
+
96
+ await api('POST', '/plugins/install', {
97
+ url: p.url,
98
+ filename: safeFilename
99
+ })
100
+
45
101
  await loadPlugins()
46
102
  setTab('installed')
47
- } catch { /* ignore */ }
48
- setInstalling(null)
103
+ toast.success(`Successfully installed ${p.name}`, { id: toastId })
104
+ } catch (err: unknown) {
105
+ console.error('[plugin-manager] Installation failed:', err)
106
+ toast.error(err instanceof Error ? err.message : 'Install failed', { id: toastId })
107
+ } finally {
108
+ setInstalling(null)
109
+ }
49
110
  }
50
111
 
112
+
51
113
  const installFromUrl = async () => {
52
114
  if (!urlInput || !urlFilename) return
53
115
  setUrlStatus(null)
@@ -58,150 +120,313 @@ export function PluginManager() {
58
120
  setUrlStatus({ ok: true, message: 'Installed successfully' })
59
121
  setUrlInput('')
60
122
  setUrlFilename('')
61
- } catch (err: any) {
62
- setUrlStatus({ ok: false, message: err.message || 'Install failed' })
123
+ toast.success('Plugin installed from URL')
124
+ } catch (err: unknown) {
125
+ setUrlStatus({ ok: false, message: err instanceof Error ? err.message : 'Install failed' })
63
126
  }
64
127
  setInstalling(null)
65
128
  }
66
129
 
67
- const installedFilenames = new Set(plugins.map((p) => p.filename))
130
+ const { corePlugins, installedPlugins } = useMemo(() => {
131
+ return {
132
+ corePlugins: plugins.filter(p => p.source === 'local'),
133
+ installedPlugins: plugins.filter(p => p.source !== 'local')
134
+ }
135
+ }, [plugins])
68
136
 
69
137
  const tabClass = (t: string) =>
70
- `py-2.5 px-4 rounded-[10px] text-center cursor-pointer transition-all text-[12px] font-600 border
138
+ `py-2 px-4 rounded-[10px] text-center cursor-pointer transition-all text-[12px] font-600 border h-9 flex items-center justify-center
71
139
  ${tab === t
72
- ? 'bg-accent-soft border-accent-bright/25 text-accent-bright'
73
- : 'bg-bg border-white/[0.06] text-text-3 hover:bg-surface-2'}`
140
+ ? 'bg-accent-soft border-accent-bright/25 text-accent-bright shadow-[0_0_15px_rgba(99,102,241,0.1)]'
141
+ : 'bg-surface border-white/[0.06] text-text-3 hover:bg-surface-2 hover:border-white/[0.12]'}`
74
142
 
75
- return (
76
- <div>
77
- <div className="flex gap-2 mb-5">
78
- <button onClick={() => setTab('installed')} className={tabClass('installed')} style={{ fontFamily: 'inherit' }}>
79
- Installed{plugins.length > 0 && ` (${plugins.length})`}
80
- </button>
81
- <button onClick={() => setTab('marketplace')} className={tabClass('marketplace')} style={{ fontFamily: 'inherit' }}>
82
- Marketplace
83
- </button>
84
- <button onClick={() => setTab('url')} className={tabClass('url')} style={{ fontFamily: 'inherit' }}>
85
- Install from URL
86
- </button>
143
+ const renderPluginItem = (p: PluginMeta) => (
144
+ <div key={p.filename} className="group flex items-center gap-4 py-3.5 px-5 rounded-[18px] bg-surface border border-white/[0.06] hover:border-white/[0.12] transition-all">
145
+ <div className="flex-1 min-w-0">
146
+ <div className="flex items-center gap-2 mb-0.5">
147
+ <span className="text-[14px] font-700 text-text truncate tracking-tight">{p.name}</span>
148
+ <span className="text-[9px] font-mono font-700 text-text-3/40 bg-white/[0.04] px-1.5 py-0.5 rounded uppercase tracking-wider">v{p.version}</span>
149
+ {p.openclaw && <span className="text-[9px] font-700 text-emerald-400 bg-emerald-400/10 px-1.5 py-0.5 rounded uppercase tracking-wider border border-emerald-400/10">OpenClaw</span>}
150
+ {p.autoDisabled && (
151
+ <span className="text-[9px] font-700 text-amber-300 bg-amber-500/10 px-1.5 py-0.5 rounded uppercase tracking-wider border border-amber-500/10">
152
+ Auto-disabled
153
+ </span>
154
+ )}
155
+ </div>
156
+ <div className="text-[11px] font-mono text-text-3/40 truncate">{p.filename}</div>
157
+ {p.description && <div className="text-[12px] text-text-3/70 mt-1 line-clamp-1">{p.description}</div>}
158
+ {p.autoDisabled && (
159
+ <div className="text-[11px] text-amber-400/90 mt-1.5 p-2 rounded-[8px] bg-amber-500/[0.03] border border-amber-500/10">
160
+ {p.lastFailureStage ? `Error at ${p.lastFailureStage}:` : 'Last error:'} {p.lastFailureError}
161
+ </div>
162
+ )}
87
163
  </div>
164
+
165
+ <div className="flex items-center gap-2">
166
+ {p.source !== 'local' && (
167
+ <div className="flex items-center opacity-0 group-hover:opacity-100 transition-opacity">
168
+ <button
169
+ onClick={() => handleUpdateOne(p.filename)}
170
+ disabled={!!updating}
171
+ className="p-2 rounded-[8px] text-text-3 hover:text-accent-bright hover:bg-accent-bright/10 transition-all border-none bg-transparent cursor-pointer"
172
+ title="Check for updates"
173
+ >
174
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" className={updating === p.filename ? 'animate-spin' : ''}>
175
+ <path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4" /><path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4" />
176
+ </svg>
177
+ </button>
178
+ <button
179
+ onClick={() => setConfirmDelete({ filename: p.filename, name: p.name })}
180
+ className="p-2 rounded-[8px] text-text-3 hover:text-red-400 hover:bg-red-400/10 transition-all border-none bg-transparent cursor-pointer"
181
+ title="Uninstall plugin"
182
+ >
183
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
184
+ <path d="M4 7l16 0" /><path d="M10 11l0 6" /><path d="M14 11l0 6" /><path d="M5 7l1 12a2 2 0 0 0 2 2h12a2 2 0 0 0 2 -2l1 -12" /><path d="M9 7v-3a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v3" />
185
+ </svg>
186
+ </button>
187
+ </div>
188
+ )}
189
+ <div
190
+ onClick={() => togglePlugin(p.filename, !p.enabled)}
191
+ className={`w-10 h-6 rounded-full transition-all duration-200 relative cursor-pointer shrink-0
192
+ ${p.enabled ? 'bg-accent-bright' : 'bg-white/[0.08]'}`}
193
+ >
194
+ <div className={`absolute top-1 w-4 h-4 rounded-full bg-white shadow-sm transition-all duration-200 ${p.enabled ? 'left-5' : 'left-1'}`} />
195
+ </div>
196
+ </div>
197
+ </div>
198
+ )
199
+
200
+ return (
201
+ <div className="flex-1 overflow-y-auto">
202
+ <div className="max-w-4xl mx-auto px-6 py-10">
203
+ <div className="flex items-center justify-between gap-4 mb-10">
204
+ <div>
205
+ <h1 className="font-display text-[32px] font-800 tracking-[-0.04em] text-text mb-1.5">Plugins</h1>
206
+ <p className="text-[14px] text-text-3 max-w-md leading-relaxed">
207
+ Extend your swarm with new capabilities, UI modules, and platform connectors.
208
+ </p>
209
+ </div>
210
+ <div className="flex bg-surface p-1.5 rounded-[14px] border border-white/[0.04]">
211
+ <button onClick={() => setTab('installed')} className={tabClass('installed')}>Installed</button>
212
+ <button onClick={() => setTab('marketplace')} className={tabClass('marketplace')}>Marketplace</button>
213
+ <button onClick={() => setTab('url')} className={tabClass('url')}>Manual</button>
214
+ </div>
215
+ </div>
88
216
 
89
- {tab === 'installed' && (
90
- plugins.length === 0
91
- ? <p className="text-[12px] text-text-3/70">No plugins installed</p>
92
- : <div className="space-y-2.5">
93
- {plugins.map((p) => (
94
- <div key={p.filename} className="flex items-center gap-3 py-3 px-4 rounded-[14px] bg-surface border border-white/[0.06]">
95
- <div className="flex-1 min-w-0">
96
- <div className="flex items-center gap-2">
97
- <span className="text-[14px] font-600 text-text truncate">{p.name}</span>
98
- {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>}
217
+ {tab === 'installed' && (
218
+ <div className="space-y-10">
219
+ <div className="flex items-center justify-between px-1">
220
+ <div className="flex items-center gap-2.5">
221
+ <div className="w-2 h-2 rounded-full bg-accent-bright animate-pulse" />
222
+ <span className="text-[11px] font-800 uppercase tracking-[0.15em] text-text-3">Active Registry</span>
223
+ </div>
224
+ {installedPlugins.length > 0 && (
225
+ <button
226
+ onClick={handleUpdateAll}
227
+ disabled={updatingAll}
228
+ className="flex items-center gap-2 text-[11px] font-700 uppercase tracking-wider text-accent-bright hover:text-accent-bright/80 disabled:opacity-50 transition-all border-none bg-transparent cursor-pointer"
229
+ >
230
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" className={updatingAll ? 'animate-spin' : ''}>
231
+ <path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4" /><path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4" />
232
+ </svg>
233
+ {updatingAll ? 'Updating...' : 'Update All'}
234
+ </button>
235
+ )}
236
+ </div>
237
+
238
+ {plugins.length === 0 ? (
239
+ <div className="py-20 text-center rounded-[24px] border border-dashed border-white/[0.06]">
240
+ <p className="text-[14px] text-text-3/50">No plugins found in the registry</p>
241
+ </div>
242
+ ) : (
243
+ <div className="space-y-10">
244
+ {corePlugins.length > 0 && (
245
+ <section>
246
+ <div className="mb-4 px-1">
247
+ <h3 className="text-[13px] font-700 text-text-2">Core Platform</h3>
248
+ <p className="text-[12px] text-text-3/50 mt-0.5">Built-in SwarmClaw official capabilities</p>
99
249
  </div>
100
- <div className="text-[11px] font-mono text-text-3 truncate">{p.filename}</div>
101
- {p.description && <div className="text-[11px] text-text-3/60 mt-0.5">{p.description}</div>}
102
- </div>
103
- <div
104
- onClick={() => togglePlugin(p.filename, !p.enabled)}
105
- className={`w-11 h-6 rounded-full transition-all duration-200 relative cursor-pointer shrink-0
106
- ${p.enabled ? 'bg-accent-bright' : 'bg-white/[0.08]'}`}
107
- >
108
- <div className={`absolute top-0.5 w-5 h-5 rounded-full bg-white transition-all duration-200
109
- ${p.enabled ? 'left-[22px]' : 'left-0.5'}`} />
110
- </div>
111
- </div>
112
- ))}
250
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
251
+ {corePlugins.map(renderPluginItem)}
252
+ </div>
253
+ </section>
254
+ )}
255
+
256
+ {installedPlugins.length > 0 && (
257
+ <section>
258
+ <div className="mb-4 px-1">
259
+ <h3 className="text-[13px] font-700 text-text-2">Extensions</h3>
260
+ <p className="text-[12px] text-text-3/50 mt-0.5">Custom and Marketplace installed plugins</p>
261
+ </div>
262
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
263
+ {installedPlugins.map(renderPluginItem)}
264
+ </div>
265
+ </section>
266
+ )}
267
+ </div>
268
+ )}
269
+ </div>
270
+ )}
271
+
272
+ {tab === 'marketplace' && (
273
+ <div className="space-y-6">
274
+ <div className="relative group">
275
+ <div className="absolute left-4 top-1/2 -translate-y-1/2 text-text-3 group-focus-within:text-accent-bright transition-colors">
276
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
277
+ <circle cx="11" cy="11" r="8" /><path d="m21 21-4.3-4.3" />
278
+ </svg>
279
+ </div>
280
+ <input
281
+ type="text"
282
+ value={marketplaceQuery}
283
+ onChange={(e) => setMarketplaceQuery(e.target.value)}
284
+ placeholder="Search ClawHub & SwarmClaw Registry..."
285
+ className="w-full h-14 pl-11 pr-4 bg-surface border border-white/[0.08] rounded-[18px] text-[15px] text-text outline-none focus:border-accent-bright/40 focus:bg-surface-2 transition-all shadow-sm"
286
+ style={{ fontFamily: 'inherit' }}
287
+ />
113
288
  </div>
114
- )}
115
-
116
- {tab === 'marketplace' && (
117
- loading
118
- ? <p className="text-[12px] text-text-3/70">Loading marketplace...</p>
119
- : marketplace.length === 0
120
- ? <p className="text-[12px] text-text-3/70">No plugins available</p>
121
- : <div className="space-y-2.5">
122
- {marketplace.map((p) => {
123
- const isInstalled = installedFilenames.has(`${p.id}.js`)
124
- return (
125
- <div key={p.id} className="py-3.5 px-4 rounded-[14px] bg-surface border border-white/[0.06]">
126
- <div className="flex items-start gap-3">
127
- <div className="flex-1 min-w-0">
128
- <div className="flex items-center gap-2">
129
- <span className="text-[14px] font-600 text-text">{p.name}</span>
130
- <span className="text-[10px] font-mono text-text-3/70">v{p.version}</span>
131
- {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>}
132
- </div>
133
- <div className="text-[11px] text-text-3/60 mt-1">{p.description}</div>
134
- <div className="flex items-center gap-2 mt-2">
135
- <span className="text-[10px] text-text-3/70">by {p.author}</span>
136
- <span className="text-[10px] text-text-3/50">·</span>
137
- {p.tags.slice(0, 3).map((t) => (
138
- <span key={t} className="text-[9px] font-600 text-text-3/50 bg-white/[0.04] px-1.5 py-0.5 rounded-full">{t}</span>
139
- ))}
140
- </div>
289
+
290
+ {loading ? (
291
+ <div className="py-20 flex flex-col items-center gap-4">
292
+ <div className="w-8 h-8 border-2 border-accent-bright/20 border-t-accent-bright rounded-full animate-spin" />
293
+ <p className="text-[12px] text-text-3/70 animate-pulse uppercase tracking-[0.1em] font-700">Searching registries...</p>
294
+ </div>
295
+ ) : (
296
+ <div className="grid grid-cols-1 gap-4">
297
+ {marketplace.map((p) => (
298
+ <div key={p.id} className="p-6 rounded-[22px] bg-surface border border-white/[0.06] flex items-start gap-6 hover:border-white/[0.12] transition-all">
299
+ <div className="w-12 h-12 rounded-[14px] bg-accent-bright/[0.03] border border-accent-bright/10 flex items-center justify-center shrink-0">
300
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" className="text-accent-bright">
301
+ <path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" />
302
+ </svg>
303
+ </div>
304
+ <div className="min-w-0 flex-1">
305
+ <div className="flex items-center gap-3 mb-1.5">
306
+ <span className="text-[16px] font-700 text-text tracking-tight">{p.name}</span>
307
+ <span className={`text-[9px] font-800 uppercase px-2 py-0.5 rounded-[6px] border ${p.source === 'clawhub' ? 'bg-indigo-500/10 text-indigo-400 border-indigo-500/20' : 'bg-emerald-500/10 text-emerald-400 border-emerald-500/20'}`}>
308
+ {p.source || 'swarmclaw'}
309
+ </span>
310
+ </div>
311
+ <p className="text-[13px] text-text-3/80 leading-relaxed mb-4 line-clamp-2">{p.description}</p>
312
+
313
+ <div className="flex items-center gap-4">
314
+ <div className="text-[11px] text-text-3/40 font-600 uppercase tracking-wider flex items-center gap-1.5">
315
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
316
+ <circle cx="12" cy="12" r="9" /><path d="M12 7v5l3 3" />
317
+ </svg>
318
+ v{p.version || '1.0.0'}
141
319
  </div>
142
- <button
143
- onClick={() => !isInstalled && installFromMarketplace(p)}
144
- disabled={isInstalled || installing === p.id}
145
- className={`shrink-0 py-2 px-4 rounded-[10px] text-[12px] font-600 transition-all cursor-pointer
146
- ${isInstalled
147
- ? 'bg-white/[0.04] text-text-3/70 cursor-default'
148
- : installing === p.id
149
- ? 'bg-accent-soft text-accent-bright animate-pulse'
150
- : 'bg-accent-soft text-accent-bright hover:bg-accent-soft/80 border border-accent-bright/20'}`}
151
- style={{ fontFamily: 'inherit' }}
320
+ {p.author && (
321
+ <div className="text-[11px] text-text-3/40 font-600 uppercase tracking-wider flex items-center gap-1.5">
322
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
323
+ <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" /><circle cx="12" cy="7" r="4" />
324
+ </svg>
325
+ {p.author}
326
+ </div>
327
+ )}
328
+ <a
329
+ href={p.url}
330
+ target="_blank"
331
+ rel="noopener noreferrer"
332
+ className="text-[11px] text-accent-bright/60 hover:text-accent-bright font-700 uppercase tracking-widest no-underline transition-colors ml-auto flex items-center gap-1"
152
333
  >
153
- {isInstalled ? 'Installed' : installing === p.id ? 'Installing...' : 'Install'}
154
- </button>
334
+ Source
335
+ <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round">
336
+ <path d="M17 7l-10 10" /><path d="M8 7l9 0l0 9" />
337
+ </svg>
338
+ </a>
155
339
  </div>
156
340
  </div>
157
- )
158
- })}
341
+ <button
342
+ disabled={!!installing}
343
+ onClick={() => installFromMarketplace(p)}
344
+ className={`px-6 py-2.5 rounded-[14px] font-display text-[13px] font-700 transition-all active:scale-[0.97] shrink-0
345
+ ${installing === p.id
346
+ ? 'bg-white/[0.04] text-text-3 animate-pulse'
347
+ : 'bg-accent-bright text-white shadow-[0_0_20px_rgba(56,189,248,0.2)] hover:bg-accent-bright/90 hover:shadow-[0_0_25px_rgba(56,189,248,0.3)]'}`}
348
+ style={{ fontFamily: 'inherit' }}
349
+ >
350
+ {installing === p.id ? '...' : 'Install'}
351
+ </button>
352
+ </div>
353
+ ))}
354
+ {marketplace.length === 0 && (
355
+ <div className="py-20 text-center opacity-40">
356
+ <p className="text-[15px] font-600 mb-1">No results for &quot;{marketplaceQuery}&quot;</p>
357
+ <p className="text-[12px]">Try searching for generic terms like &quot;wallet&quot;, &quot;social&quot;, or &quot;files&quot;</p>
358
+ </div>
359
+ )}
159
360
  </div>
160
- )}
161
-
162
- {tab === 'url' && (
163
- <div className="p-5 rounded-[14px] bg-surface border border-white/[0.06]">
164
- <div className="mb-4">
165
- <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">Plugin URL</label>
166
- <input
167
- type="url"
168
- value={urlInput}
169
- onChange={(e) => setUrlInput(e.target.value)}
170
- placeholder="https://example.com/my-plugin.js"
171
- className="w-full py-2.5 px-3 rounded-[10px] text-[13px] bg-bg border border-white/[0.06] text-text placeholder:text-text-3/60 outline-none focus:border-accent-bright/30"
172
- style={{ fontFamily: 'inherit' }}
173
- />
174
- </div>
175
- <div className="mb-4">
176
- <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">Save as filename</label>
177
- <input
178
- type="text"
179
- value={urlFilename}
180
- onChange={(e) => setUrlFilename(e.target.value)}
181
- placeholder="my-plugin.js"
182
- className="w-full py-2.5 px-3 rounded-[10px] text-[13px] bg-bg border border-white/[0.06] text-text placeholder:text-text-3/60 outline-none focus:border-accent-bright/30"
183
- style={{ fontFamily: 'inherit' }}
184
- />
361
+ )}
185
362
  </div>
186
- <button
187
- onClick={installFromUrl}
188
- disabled={!urlInput || !urlFilename || installing === 'url'}
189
- className="w-full py-2.5 rounded-[10px] text-[13px] font-600 bg-accent-soft text-accent-bright border border-accent-bright/20
190
- hover:bg-accent-soft/80 transition-all cursor-pointer disabled:opacity-40 disabled:cursor-default"
191
- style={{ fontFamily: 'inherit' }}
192
- >
193
- {installing === 'url' ? 'Installing...' : 'Install Plugin'}
194
- </button>
195
- {urlStatus && (
196
- <p className={`text-[11px] mt-3 ${urlStatus.ok ? 'text-emerald-400' : 'text-red-400'}`}>
197
- {urlStatus.message}
363
+ )}
364
+
365
+ {tab === 'url' && (
366
+ <div className="max-w-2xl mx-auto mt-10 p-8 rounded-[28px] bg-surface border border-white/[0.06] shadow-xl">
367
+ <h2 className="text-[18px] font-800 text-text mb-6 flex items-center gap-2">
368
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" className="text-accent-bright">
369
+ <path d="M10 14l11 -11" /><path d="M21 3l-6.5 18a0.55 .55 0 0 1 -1 0l-3.5 -7l-7 -3.5a0.55 .55 0 0 1 0 -1l18 -6.5" />
370
+ </svg>
371
+ Install from source URL
372
+ </h2>
373
+ <div className="space-y-5">
374
+ <div>
375
+ <label className="block text-[11px] font-700 text-text-3 uppercase tracking-widest mb-2.5 ml-1">JavaScript URL (HTTPS)</label>
376
+ <input
377
+ type="text"
378
+ value={urlInput}
379
+ onChange={(e) => setUrlInput(e.target.value)}
380
+ placeholder="https://example.com/my-plugin.js"
381
+ className="w-full h-12 px-4 bg-bg border border-white/[0.08] rounded-[14px] text-[14px] text-text outline-none focus:border-accent-bright/40 focus:ring-4 focus:ring-accent-bright/5 transition-all"
382
+ style={{ fontFamily: 'inherit' }}
383
+ />
384
+ </div>
385
+ <div>
386
+ <label className="block text-[11px] font-700 text-text-3 uppercase tracking-widest mb-2.5 ml-1">Target Filename</label>
387
+ <input
388
+ type="text"
389
+ value={urlFilename}
390
+ onChange={(e) => setUrlFilename(e.target.value)}
391
+ placeholder="my-plugin.js"
392
+ className="w-full h-12 px-4 bg-bg border border-white/[0.08] rounded-[14px] text-[14px] text-text outline-none focus:border-accent-bright/40 focus:ring-4 focus:ring-accent-bright/5 transition-all"
393
+ style={{ fontFamily: 'inherit' }}
394
+ />
395
+ </div>
396
+ <button
397
+ onClick={installFromUrl}
398
+ disabled={!urlInput || !urlFilename || installing === 'url'}
399
+ className="w-full h-12 bg-accent-bright text-white rounded-[14px] text-[14px] font-800 shadow-lg shadow-accent-bright/20 hover:brightness-110 active:scale-[0.98] transition-all disabled:opacity-50 disabled:cursor-not-allowed"
400
+ style={{ fontFamily: 'inherit' }}
401
+ >
402
+ {installing === 'url' ? 'Installing...' : 'Install Plugin'}
403
+ </button>
404
+ </div>
405
+ {urlStatus && (
406
+ <div className={`mt-5 p-4 rounded-[14px] flex items-center gap-3 text-[13px] font-600 border ${urlStatus.ok ? 'bg-emerald-500/10 text-emerald-400 border-emerald-500/20' : 'bg-red-500/10 text-red-400 border-red-500/20'}`}>
407
+ <div className={`w-2 h-2 rounded-full ${urlStatus.ok ? 'bg-emerald-400' : 'bg-red-400'}`} />
408
+ {urlStatus.message}
409
+ </div>
410
+ )}
411
+ <p className="text-[11px] text-text-3/40 mt-6 leading-relaxed text-center italic">
412
+ SwarmClaw supports standalone CommonJS plugins and OpenClaw activate/deactivate formats.
198
413
  </p>
199
- )}
200
- <p className="text-[10px] text-text-3/60 mt-3">
201
- Works with SwarmClaw and OpenClaw plugin formats. URL must be HTTPS.
202
- </p>
203
- </div>
204
- )}
414
+ </div>
415
+ )}
416
+ </div>
417
+ <ConfirmDialog
418
+ open={!!confirmDelete}
419
+ title="Delete Plugin"
420
+ message={confirmDelete ? `Delete "${confirmDelete.name}"? This cannot be undone.` : ''}
421
+ confirmLabel="Delete"
422
+ danger
423
+ onConfirm={() => {
424
+ if (!confirmDelete) return
425
+ void deletePlugin(confirmDelete.filename, confirmDelete.name)
426
+ setConfirmDelete(null)
427
+ }}
428
+ onCancel={() => setConfirmDelete(null)}
429
+ />
205
430
  </div>
206
431
  )
207
432
  }
@@ -9,7 +9,7 @@ export function CapabilityPolicySection({ appSettings, patchSettings, inputClass
9
9
  Capability Policy
10
10
  </h3>
11
11
  <p className="text-[12px] text-text-3 mb-5">
12
- Centralized guardrails for agent tool families. Applies to direct tool calls and forced auto-routing.
12
+ Centralized guardrails for agent plugin families. Applies to direct plugin calls and forced auto-routing.
13
13
  </p>
14
14
  <div className="p-6 rounded-[18px] bg-surface border border-white/[0.06]">
15
15
  <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-3">Policy Mode</label>
@@ -53,7 +53,7 @@ export function CapabilityPolicySection({ appSettings, patchSettings, inputClass
53
53
  </div>
54
54
 
55
55
  <div>
56
- <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">Blocked Tools</label>
56
+ <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">Blocked Plugins</label>
57
57
  <input
58
58
  type="text"
59
59
  value={(appSettings.capabilityBlockedTools || []).join(', ')}
@@ -70,7 +70,7 @@ export function CapabilityPolicySection({ appSettings, patchSettings, inputClass
70
70
  </div>
71
71
 
72
72
  <div>
73
- <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">Allowed Tools (Override)</label>
73
+ <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">Allowed Plugins (Override)</label>
74
74
  <input
75
75
  type="text"
76
76
  value={(appSettings.capabilityAllowedTools || []).join(', ')}