redesign plugin marketplace; simplify API

- Updated the plugin installation API to fetch and augment plugin discussions from GitHub, improving the index response with discussion links. - Refactored the plugin installation UI to include better handling of plugin details, including management buttons for installed plugins. - Improved the layout and styling of the plugin browsing interface, including search and sort functionalities, and added a summary of available plugins.

Alessandro committed Mar 7, 2026 at 18:32 UTC eac0d3bc7473466d4d56bb0d7d3b1e3b551a73ab
11 files changed +1641 -486
plugins/plugin_installer/api/plugin_install.py
+44 -7
@@ -1,6 +1,9 @@
1 from __future__ import annotations
2
3 +import json
4 +import logging
5 import time
6 +import urllib.request
7 import uuid
8 from pathlib import Path
9
@@ -9,7 +12,6 @@ from helpers import files
12 from werkzeug.datastructures import FileStorage
13 from werkzeug.utils import secure_filename
14
12 -
15 class PluginInstall(ApiHandler):
16 """Plugin installation API. Handles ZIP upload, Git clone, and index fetch."""
17
@@ -64,10 +66,45 @@ class PluginInstall(ApiHandler):
66 def _fetch_index(self, input: dict) -> dict:
67 from plugins.plugin_installer.helpers.install import fetch_plugin_index
68 from helpers.plugins import get_plugins_list
69 +
70 index_data = fetch_plugin_index()
68 - installed = get_plugins_list()
69 - return {
70 - "success": True,
71 - "index": index_data,
72 - "installed_plugins": installed,
73 - }
71 + plugins = index_data.get("plugins", {})
72 + installed_dirs = set(get_plugins_list())
73 +
74 + def _dir_name(github: str) -> str:
75 + seg = (github or "").rstrip("/").split("/")[-1]
76 + return seg[:-4] if seg.endswith(".git") else seg
77 +
78 + installed_keys = [
79 + key for key, p in plugins.items()
80 + if _dir_name(p.get("github", "")) in installed_dirs
81 + ]
82 +
83 + if all(plugin.get("discussion") for plugin in plugins.values()):
84 + return {"success": True, "index": index_data, "installed_plugins": installed_keys}
85 +
86 + try:
87 + req = urllib.request.Request(
88 + "https://api.github.com/repos/agent0ai/a0-plugins/discussions?per_page=100",
89 + headers={
90 + "User-Agent": "AgentZero",
91 + "Accept": "application/vnd.github+json",
92 + },
93 + )
94 + with urllib.request.urlopen(req, timeout=10) as resp:
95 + discussions = json.loads(resp.read().decode())
96 +
97 + for discussion in discussions:
98 + title = discussion.get("title", "")
99 + if not title.startswith("Plugin: "):
100 + continue
101 +
102 + key = title[len("Plugin: "):]
103 + if key in plugins and not plugins[key].get("discussion"):
104 + plugins[key]["discussion"] = discussion["html_url"]
105 + except Exception as e:
106 + logging.getLogger(__name__).warning(
107 + "Failed to augment plugin index discussions: %s", e
108 + )
109 +
110 + return {"success": True, "index": index_data, "installed_plugins": installed_keys}
plugins/plugin_installer/helpers/install.py
-20
@@ -1,7 +1,6 @@
1 from __future__ import annotations
2
3 import os
4 -import re
4 import shutil
5 import time
6 import zipfile
@@ -16,28 +15,11 @@ from helpers.plugins import (
15 )
16 from helpers import yaml as yaml_helper
17
19 -_SAFE_NAME_RE = re.compile(r"^[a-zA-Z0-9][a-zA-Z0-9_]*$")
20 -
21 -
18 def _get_user_plugins_dir() -> str:
19 """Return absolute path to usr/plugins/."""
20 return files.get_abs_path(files.USER_DIR, files.PLUGINS_DIR)
21
22
27 -def _sanitize_plugin_name(name: str) -> str:
28 - """Validate and sanitize a plugin directory name.
29 - Converts dots and dashes to underscores for Python import compatibility.
30 - Raises ValueError if the name is unsafe for filesystem use."""
31 - name = name.strip().strip(".")
32 - name = re.sub(r"[-.]", "_", name)
33 - if not name or not _SAFE_NAME_RE.match(name):
34 - raise ValueError(
35 - f"Invalid plugin name: '{name}'. "
36 - "Names must start with a letter or digit and contain only letters, digits, or underscores."
37 - )
38 - return name
39 -
40 -
23 def validate_plugin_dir(path: str) -> PluginMetadata:
24 """Check directory contains plugin.yaml and return parsed metadata.
25 Raises ValueError if plugin.yaml is missing or invalid."""
@@ -100,7 +82,6 @@ def install_from_zip(zip_path: str) -> dict:
82 # plugin.yaml at root of extraction — use zip filename stem
83 plugin_name = Path(zip_path).stem
84
103 - plugin_name = _sanitize_plugin_name(plugin_name)
85 check_plugin_conflict(plugin_name)
86
87 # Move to usr/plugins/
@@ -136,7 +117,6 @@ def install_from_git(url: str, token: Optional[str] = None) -> dict:
117 if not repo_name:
118 raise ValueError("Could not derive plugin name from URL")
119
139 - repo_name = _sanitize_plugin_name(repo_name)
120 check_plugin_conflict(repo_name)
121
122 dest = os.path.join(_get_user_plugins_dir(), repo_name)
plugins/plugin_installer/webui/install-detail.html
+543 -160
@@ -8,130 +8,181 @@
8 <body>
9 <div x-data>
10 <template x-if="$store.pluginInstallStore?.selectedPlugin">
11 - <div>
12 - <div class="pi-detail-header">
13 - <div class="pi-detail-thumb">
14 - <template x-if="$store.pluginInstallStore.selectedPlugin.thumbnail">
15 - <img :src="$store.pluginInstallStore.selectedPlugin.thumbnail"
16 - :alt="$store.pluginInstallStore.selectedPlugin.title">
11 + <div class="pi-detail-container"
12 + x-destroy="$store.pluginInstallStore.refreshPluginList()">
13 + <div class="pi-hero">
14 + <div class="pi-hero-thumb">
15 + <template x-if="$store.pluginInstallStore.getDetailThumbnailUrl()">
16 + <img :src="$store.pluginInstallStore.getDetailThumbnailUrl()"
17 + :alt="$store.pluginInstallStore.selectedPlugin.title"
18 + @error="$store.pluginInstallStore.nextDetailThumbnail()">
19 </template>
18 - <template x-if="!$store.pluginInstallStore.selectedPlugin.thumbnail">
19 - <span class="material-symbols-outlined pi-detail-placeholder">extension</span>
20 + <template x-if="!$store.pluginInstallStore.getDetailThumbnailUrl()">
21 + <span class="material-symbols-outlined pi-hero-placeholder">extension</span>
22 </template>
23 </div>
22 - <div class="pi-detail-info">
23 - <h3 class="pi-detail-title" x-text="$store.pluginInstallStore.selectedPlugin.title || $store.pluginInstallStore.selectedPlugin.key"></h3>
24 - <div class="pi-detail-stars">
25 - <span class="material-symbols-outlined" style="font-size:1rem">star</span>
26 - <span x-text="($store.pluginInstallStore.selectedPlugin.stars || 0) + ' stars'"></span>
27 - </div>
28 - <div class="pi-detail-tags" x-show="$store.pluginInstallStore.selectedPlugin.tags?.length">
29 - <template x-for="tag in $store.pluginInstallStore.selectedPlugin.tags || []" :key="tag">
30 - <span class="pi-tag" x-text="tag"></span>
31 - </template>
32 - </div>
33 - </div>
34 - </div>
35 -
36 - <div class="pi-detail-desc" x-text="$store.pluginInstallStore.selectedPlugin.description || 'No description available.'"></div>
37 -
38 - <div class="pi-detail-links">
39 - <template x-if="$store.pluginInstallStore.selectedPlugin.github">
40 - <a :href="$store.pluginInstallStore.selectedPlugin.github" target="_blank" class="pi-link">
41 - <span class="material-symbols-outlined pi-link-icon">code</span>
42 - <span class="pi-link-text">GitHub Repository</span>
43 - </a>
44 - </template>
45 - <template x-if="$store.pluginInstallStore.selectedPlugin.discussion">
46 - <a :href="$store.pluginInstallStore.selectedPlugin.discussion" target="_blank" class="pi-link">
47 - <span class="material-symbols-outlined pi-link-icon">forum</span>
48 - <span class="pi-link-text">Discussion</span>
49 - </a>
50 - </template>
51 - </div>
52 -
53 - <div class="pi-detail-actions">
54 - <template x-if="$store.pluginInstallStore.selectedPlugin.installed">
55 - <div>
56 - <div class="pi-installed-badge-row">
57 - <span class="material-symbols-outlined pi-installed-check">check_circle</span>
58 - <span class="pi-installed-label">Installed</span>
59 - </div>
60 -
61 - <div x-show="$store.pluginInstallStore.installedPluginInfoLoading" class="pi-loading-inline">
62 - Loading plugin info...
63 - </div>
64 -
65 - <template x-if="$store.pluginInstallStore.installedPluginInfo">
66 - <div class="pi-manage-actions">
24 + <div class="pi-hero-info">
25 + <div class="pi-hero-title-row">
26 + <h2 class="pi-hero-title" x-text="$store.pluginInstallStore.selectedPlugin.title || $store.pluginInstallStore.selectedPlugin.key"></h2>
27 + <template x-if="$store.pluginInstallStore.selectedPlugin.installed && $store.pluginInstallStore.installedPluginInfo">
28 + <div class="pi-hero-manage">
29 <template x-if="$store.pluginInstallStore.installedPluginInfo.has_main_screen">
30 <button type="button" class="button"
31 + title="Open"
32 @click="$store.pluginInstallStore.manageOpenPlugin()">
33 <span class="icon material-symbols-outlined">dashboard</span> Open
34 </button>
35 </template>
36 <template x-if="$store.pluginInstallStore.installedPluginInfo.has_config_screen">
37 <button type="button" class="button"
38 + title="Config"
39 @click="$store.pluginInstallStore.manageOpenConfig()">
40 <span class="icon material-symbols-outlined">settings</span> Config
41 </button>
42 </template>
43 <template x-if="$store.pluginInstallStore.installedPluginInfo.has_readme">
44 <button type="button" class="button"
45 + title="README"
46 @click="$store.pluginInstallStore.manageOpenDoc('readme')">
47 <span class="icon material-symbols-outlined">description</span> README
48 </button>
49 </template>
50 <template x-if="$store.pluginInstallStore.installedPluginInfo.has_license">
51 <button type="button" class="button"
52 + title="LICENSE"
53 @click="$store.pluginInstallStore.manageOpenDoc('license')">
54 <span class="icon material-symbols-outlined">gavel</span> License
55 </button>
56 </template>
57 + <template x-if="$store.pluginInstallStore.installedPluginInfo.has_init_script">
58 + <button type="button" class="button"
59 + title="Run initializer script"
60 + @click="$store.pluginInstallStore.manageOpenInit()">
61 + <span class="icon material-symbols-outlined">terminal</span> Init
62 + </button>
63 + </template>
64 <button type="button" class="button"
65 + title="Info"
66 @click="$store.pluginInstallStore.manageOpenInfo()">
67 <span class="icon material-symbols-outlined">info</span> Info
68 </button>
95 - <template x-if="$store.pluginInstallStore.installedPluginInfo.is_custom">
96 - <button type="button" class="button cancel"
97 - @click="$confirmClick($event, () => $store.pluginInstallStore.manageDeletePlugin())">
98 - <span class="icon material-symbols-outlined">delete</span> Delete
99 - </button>
100 - </template>
69 </div>
70 </template>
71 </div>
104 - </template>
72 + <template x-if="$store.pluginInstallStore.selectedPlugin.installed">
73 + <div class="pi-installed-badge-simple">
74 + <span class="material-symbols-outlined">check_circle</span>
75 + <span>Installed</span>
76 + </div>
77 + </template>
78 + <div class="pi-hero-meta">
79 + <template x-if="$store.pluginInstallStore.selectedPlugin.author">
80 + <span class="pi-hero-author">
81 + <span class="pi-hero-label">Author</span>
82 + <span x-text="$store.pluginInstallStore.selectedPlugin.author"></span>
83 + </span>
84 + </template>
85 + <span class="pi-hero-stars">
86 + <span class="material-symbols-outlined">star</span>
87 + <span x-text="($store.pluginInstallStore.selectedPlugin.stars || 0)"></span>
88 + </span>
89 + </div>
90 + <div class="pi-hero-tags" x-show="$store.pluginInstallStore.selectedPlugin.tags?.length">
91 + <template x-for="tag in $store.pluginInstallStore.selectedPlugin.tags || []" :key="tag">
92 + <span class="pi-tag" x-text="tag"></span>
93 + </template>
94 + </div>
95 + </div>
96 + </div>
97 +
98 + <div class="pi-screenshots-section"
99 + x-show="$store.pluginInstallStore.selectedPlugin.screenshots?.length">
100 + <div class="pi-screenshots-header">Screenshots</div>
101 + <div class="pi-screenshots-grid">
102 + <template x-for="(url, i) in ($store.pluginInstallStore.selectedPlugin.screenshots || [])" :key="i">
103 + <img class="pi-screenshot-thumb"
104 + :src="url"
105 + :alt="$store.pluginInstallStore.selectedPlugin.title + ' screenshot ' + (i + 1)"
106 + @click="$store.imageViewer.open(url, { name: $store.pluginInstallStore.selectedPlugin.title })">
107 + </template>
108 + </div>
109 + </div>
110 +
111 + <div class="pi-description" x-text="$store.pluginInstallStore.selectedPlugin.description || 'No description available.'"></div>
112 +
113 + <div class="pi-actions-primary">
114 <template x-if="!$store.pluginInstallStore.selectedPlugin.installed">
106 - <button class="button confirm"
115 + <button class="pi-btn-install"
116 @click="$store.pluginInstallStore.installFromIndex($store.pluginInstallStore.selectedPlugin)"
117 :disabled="$store.pluginInstallStore.loading">
118 <template x-if="$store.pluginInstallStore.loading">
110 - <span class="pi-button-loading">
119 + <span class="pi-btn-loading">
120 <span class="spinner"></span>
121 <span x-text="$store.pluginInstallStore.loadingMessage || 'Installing...'"></span>
122 </span>
123 </template>
124 <template x-if="!$store.pluginInstallStore.loading">
116 - <span>
117 - <span class="icon material-symbols-outlined">download</span> Install Plugin
118 - </span>
125 + <span class="material-symbols-outlined">download</span>
126 + </template>
127 + <template x-if="!$store.pluginInstallStore.loading">
128 + <span>Install</span>
129 </template>
130 </button>
131 </template>
132 + <template x-if="$store.pluginInstallStore.selectedPlugin.installed && $store.pluginInstallStore.installedPluginInfo?.is_custom">
133 + <button type="button" class="pi-btn-uninstall"
134 + @click="$confirmClick($event, () => $store.pluginInstallStore.manageDeletePlugin())">
135 + <span class="material-symbols-outlined">delete</span>
136 + <span>Uninstall</span>
137 + </button>
138 + </template>
139 + <template x-if="$store.pluginInstallStore.selectedPlugin.discussion">
140 + <a :href="$store.pluginInstallStore.selectedPlugin.discussion" target="_blank" class="pi-btn-discussion">
141 + <span class="material-symbols-outlined">forum</span>
142 + <span>Join Discussion</span>
143 + </a>
144 + </template>
145 </div>
146
124 - <div x-show="$store.pluginInstallStore.error" class="pi-error">
147 + <div x-show="$store.pluginInstallStore.error" class="pi-message pi-message-error">
148 <span x-text="$store.pluginInstallStore.error"></span>
149 </div>
150
128 - <div x-show="$store.pluginInstallStore.result" class="pi-result">
129 - <div class="pi-result-icon">
130 - <span class="material-symbols-outlined">check_circle</span>
151 + <div class="pi-readme-section" x-show="$store.pluginInstallStore.readmeContent || $store.pluginInstallStore.readmeLoading">
152 + <div class="pi-readme-header">Readme</div>
153 + <div x-show="$store.pluginInstallStore.readmeLoading" class="pi-loading-text">
154 + Loading readme...
155 </div>
132 - <div class="pi-result-text">
133 - <strong>Plugin installed successfully!</strong>
134 - <div class="pi-result-path" x-text="$store.pluginInstallStore.result?.path || ''"></div>
156 + <div x-show="$store.pluginInstallStore.readmeContent && !$store.pluginInstallStore.readmeLoading"
157 + class="pi-readme-content" x-html="$store.pluginInstallStore.readmeContent"></div>
158 + </div>
159 +
160 + <div class="pi-developer-section">
161 + <div class="pi-developer-header">Plugin Code</div>
162 + <div class="pi-developer-links">
163 + <template x-if="$store.pluginInstallStore.selectedPlugin.github">
164 + <a :href="$store.pluginInstallStore.selectedPlugin.github" target="_blank" class="pi-dev-link">
165 + <span class="material-symbols-outlined">code</span>
166 + <span>GitHub Repository</span>
167 + </a>
168 + </template>
169 + <template x-if="$store.pluginInstallStore.selectedPlugin.key">
170 + <a :href="$store.pluginInstallStore.getIndexUrl($store.pluginInstallStore.selectedPlugin.key)" target="_blank" class="pi-dev-link">
171 + <span class="material-symbols-outlined">inventory_2</span>
172 + <span>Plugin Index</span>
173 + </a>
174 + </template>
175 + </div>
176 + </div>
177 + </div>
178 + </template>
179 +
180 + <template x-if="$store.pluginInstallStore?.selectedPlugin">
181 + <div class="modal-footer pi-footer" data-modal-footer>
182 + <div class="buttons-container">
183 + <div class="buttons-left"></div>
184 + <div class="buttons-right">
185 + <button class="btn btn-cancel" @click="window.closeModal?.()">Close</button>
186 </div>
187 </div>
188 </div>
@@ -139,191 +190,523 @@
190 </div>
191
192 <style>
142 - .pi-detail-header {
193 + .pi-detail-container {
194 + padding: 1rem;
195 + margin: 0 auto;
196 + }
197 +
198 + .pi-hero {
199 display: flex;
144 - gap: 1rem;
145 - margin-bottom: 1rem;
200 + align-items: flex-start;
201 + gap: 1.5rem;
202 + margin-bottom: 1.25rem;
203 }
204
148 - .pi-detail-thumb {
149 - width: 100px;
150 - height: 100px;
151 - border-radius: 8px;
205 + .pi-hero-thumb {
206 + width: 120px;
207 + height: 120px;
208 + border-radius: 12px;
209 overflow: hidden;
210 flex-shrink: 0;
211 display: flex;
212 align-items: center;
213 justify-content: center;
214 background: var(--color-panel);
215 + border: 1px solid var(--color-border);
216 }
217
160 - .pi-detail-thumb img {
218 + .pi-hero-thumb img {
219 width: 100%;
220 height: 100%;
221 object-fit: contain;
222 + padding: 0.5rem;
223 }
224
166 - .pi-detail-placeholder {
167 - font-size: 4rem;
225 + .pi-hero-placeholder {
226 + font-size: 5rem;
227 color: var(--color-text-muted);
228 }
229
171 - .pi-detail-info {
230 + .pi-hero-info {
231 flex: 1;
232 + min-width: 0;
233 }
234
175 - .pi-detail-title {
176 - margin: 0 0 0.3rem 0;
177 - font-size: 1.2rem;
235 + .pi-hero-title-row {
236 + display: flex;
237 + align-items: flex-start;
238 + justify-content: space-between;
239 + gap: 0.75rem;
240 + margin-bottom: 0.5rem;
241 }
242
180 - .pi-detail-stars {
181 - display: inline-flex;
243 + .pi-hero-title {
244 + margin: 0;
245 + font-size: 1.75rem;
246 + font-weight: 600;
247 + line-height: 1.2;
248 + color: var(--color-text-primary);
249 + }
250 +
251 + .pi-hero-manage {
252 + display: flex;
253 align-items: center;
183 - gap: 0.25rem;
254 + gap: 0.5rem;
255 + flex-wrap: wrap;
256 + flex-shrink: 0;
257 + justify-content: flex-end;
258 + }
259 +
260 + .pi-hero-manage .button {
261 + padding: 0.3rem 0.6rem;
262 font-size: 0.9rem;
185 - color: var(--color-text-secondary);
186 - margin-bottom: 0.4rem;
263 }
264
189 - .pi-detail-tags {
265 + .pi-hero-manage .button .icon {
266 + font-size: 1.1rem;
267 + }
268 +
269 + .pi-hero-meta {
270 display: flex;
271 + align-items: center;
272 + gap: 1rem;
273 flex-wrap: wrap;
192 - gap: 0.3rem;
274 + margin-bottom: 0.5rem;
275 }
276
195 - .pi-tag {
196 - font-size: 0.75rem;
197 - padding: 0.15rem 0.5rem;
198 - border-radius: 3px;
199 - background: var(--color-panel);
277 + .pi-hero-label {
278 + font-size: 0.8rem;
279 + color: var(--color-text-muted);
280 + margin-right: 0.2rem;
281 + }
282 +
283 + .pi-hero-author {
284 + font-size: 1rem;
285 color: var(--color-text-secondary);
201 - border: 1px solid var(--color-border);
286 + font-weight: 500;
287 + display: inline-flex;
288 + align-items: baseline;
289 + gap: 0.15rem;
290 }
291
204 - .pi-detail-desc {
205 - color: var(--color-text-primary);
292 + .pi-hero-stars {
293 + display: inline-flex;
294 + align-items: center;
295 + gap: 0.3rem;
296 font-size: 0.95rem;
207 - line-height: 1.5;
208 - margin-bottom: 1rem;
297 + color: var(--color-text-secondary);
298 + }
299 +
300 + .pi-hero-stars .material-symbols-outlined {
301 + font-size: 1.1rem;
302 + color: #fbbf24;
303 }
304
211 - .pi-detail-links {
305 + .pi-hero-tags {
306 display: flex;
213 - gap: 1rem;
214 - margin-bottom: 1rem;
307 + flex-wrap: wrap;
308 + gap: 0.4rem;
309 }
310
217 - .pi-link {
311 + .pi-installed-badge-simple {
312 display: inline-flex;
313 align-items: center;
220 - gap: 0.3rem;
221 - text-decoration: none;
314 + gap: 0.35rem;
315 font-size: 0.9rem;
316 + font-weight: 600;
317 + color: #22c55e;
318 + margin-bottom: 0.35rem;
319 }
320
225 - .pi-link-icon {
226 - color: var(--color-highlight);
227 - display: inline-block;
321 + .pi-installed-badge-simple .material-symbols-outlined {
322 font-size: 1.1rem;
229 - flex-shrink: 0;
323 }
324
232 - .pi-link-text {
233 - color: var(--color-highlight);
325 + .pi-tag {
326 + display: flex;
327 + flex-wrap: wrap;
328 + gap: 0.4rem;
329 }
330
236 - .pi-link:hover .pi-link-text {
237 - text-decoration: underline;
238 - text-underline-offset: 2px;
331 + .pi-tag {
332 + font-size: 0.8rem;
333 + padding: 0.25rem 0.6rem;
334 + border-radius: 4px;
335 + background: var(--color-panel);
336 + color: var(--color-text-secondary);
337 + border: 1px solid var(--color-border);
338 + font-weight: 500;
339 }
340
241 - .pi-detail-actions {
242 - margin: 1rem 0;
341 + .pi-description {
342 + font-size: 1rem;
343 + line-height: 1.6;
344 + color: var(--color-text-primary);
345 + margin-bottom: 1.5rem;
346 }
347
245 - .pi-button-loading {
246 - display: inline-flex;
348 + .pi-actions-primary {
349 + display: flex;
350 + gap: 0.75rem;
351 + margin-bottom: 1.5rem;
352 + flex-wrap: wrap;
353 + }
354 +
355 + .pi-btn-install {
356 + flex: 1;
357 + min-width: 200px;
358 + padding: 0.85rem 1.5rem;
359 + border: none;
360 + border-radius: 8px;
361 + background: var(--color-highlight);
362 + color: #fff;
363 + font-family: 'Rubik';
364 + font-size: 1rem;
365 + font-weight: 600;
366 + cursor: pointer;
367 + transition: all 0.2s;
368 + display: flex;
369 align-items: center;
370 + justify-content: center;
371 gap: 0.5rem;
372 }
373
251 - .pi-error {
252 - color: var(--color-error);
253 - padding: 0.75rem;
254 - background: var(--color-error-bg, rgba(239,68,68,0.1));
255 - border-radius: 4px;
256 - margin-top: 0.75rem;
374 + .pi-btn-install:hover:not(:disabled) {
375 + opacity: 0.9;
376 + transform: translateY(-1px);
377 + box-shadow: 0 4px 12px rgba(0,0,0,0.15);
378 + }
379 +
380 + .pi-btn-install:disabled {
381 + opacity: 0.6;
382 + cursor: not-allowed;
383 }
384
259 - .pi-result {
385 + .pi-btn-install .material-symbols-outlined {
386 + font-size: 1.3rem;
387 + }
388 +
389 + .pi-btn-uninstall {
390 + flex: 1;
391 + min-width: 200px;
392 + padding: 0.85rem 1.5rem;
393 + border: 2px solid var(--color-accent);
394 + border-radius: 8px;
395 + background: transparent;
396 + color: var(--color-accent);
397 + font-family: 'Rubik';
398 + font-size: 1rem;
399 + font-weight: 600;
400 + cursor: pointer;
401 + transition: all 0.2s;
402 display: flex;
403 align-items: center;
262 - gap: 0.75rem;
263 - padding: 0.75rem;
264 - background: rgba(34,197,94,0.1);
265 - border: 1px solid rgba(34,197,94,0.3);
266 - border-radius: 4px;
267 - margin-top: 0.75rem;
404 + justify-content: center;
405 + gap: 0.5rem;
406 }
407
270 - .pi-result-icon .material-symbols-outlined {
271 - font-size: 2rem;
272 - color: #22c55e;
408 + .pi-btn-uninstall:hover {
409 + box-shadow: 0 4px 12px rgba(0,0,0,0.15);
410 + background: var(--color-accent);
411 + color: #fff;
412 }
413
275 - .pi-result-path {
276 - font-size: 0.85rem;
414 + .pi-btn-uninstall .material-symbols-outlined {
415 + font-size: 1.3rem;
416 + }
417 +
418 + .pi-btn-discussion {
419 + flex: 1;
420 + min-width: 200px;
421 + padding: 0.85rem 1.5rem;
422 + border: 2px solid var(--color-highlight);
423 + border-radius: 8px;
424 + background: transparent;
425 + color: var(--color-highlight);
426 + font-size: 1rem;
427 + font-weight: 600;
428 + cursor: pointer;
429 + transition: all 0.2s;
430 + display: flex;
431 + align-items: center;
432 + justify-content: center;
433 + gap: 0.5rem;
434 + text-decoration: none;
435 + }
436 +
437 + .pi-btn-discussion:hover {
438 + background: var(--color-highlight);
439 + color: #fff;
440 + box-shadow: 0 4px 12px rgba(0,0,0,0.15);
441 + }
442 +
443 + .pi-btn-discussion .material-symbols-outlined {
444 + font-size: 1.3rem;
445 + }
446 +
447 + .pi-btn-loading {
448 + display: flex;
449 + align-items: center;
450 + gap: 0.6rem;
451 + }
452 +
453 + .pi-loading-text {
454 + font-size: 0.9rem;
455 color: var(--color-text-secondary);
278 - margin-top: 0.25rem;
279 - font-family: var(--font-family-code);
456 + padding: 0.75rem 0;
457 }
458
282 - .pi-installed-badge-row {
459 + .pi-message {
460 display: flex;
461 align-items: center;
285 - gap: 0.4rem;
462 + gap: 0.75rem;
463 + padding: 1rem;
464 + border-radius: 8px;
465 + margin-bottom: 1rem;
466 + }
467 +
468 + .pi-message-error {
469 + background: rgba(239,68,68,0.1);
470 + border: 1px solid rgba(239,68,68,0.3);
471 + color: var(--color-error);
472 + }
473 +
474 + .pi-screenshots-section {
475 + margin-top: 1.5rem;
476 + padding-top: 1.5rem;
477 + border-top: 1px solid var(--color-border);
478 + }
479 +
480 + .pi-screenshots-header {
481 + font-size: 0.85rem;
482 + font-weight: 600;
483 + color: var(--color-text-muted);
484 margin-bottom: 0.75rem;
485 }
486
289 - .pi-installed-check {
290 - font-size: 1.3rem;
291 - color: #22c55e;
487 + .pi-screenshots-grid {
488 + display: flex;
489 + gap: 0.75rem;
490 + flex-wrap: wrap;
491 + }
492 +
493 + .pi-screenshot-thumb {
494 + height: 120px;
495 + flex: 1;
496 + min-width: 0;
497 + object-fit: cover;
498 + border-radius: 8px;
499 + cursor: pointer;
500 + border: 1px solid var(--color-border);
501 + transition: opacity 0.15s;
502 }
503
294 - .pi-installed-label {
504 + .pi-screenshot-thumb:hover {
505 + opacity: 0.8;
506 + }
507 +
508 + .pi-readme-section {
509 + margin-top: 2rem;
510 + padding-top: 1.5rem;
511 + border-top: 1px solid var(--color-border);
512 + }
513 +
514 + .pi-readme-header {
515 + font-size: 0.85rem;
516 font-weight: 600;
517 + color: var(--color-text-muted);
518 + margin-bottom: 0.75rem;
519 + }
520 +
521 + .pi-readme-content {
522 font-size: 0.95rem;
297 - color: #22c55e;
523 + line-height: 1.6;
524 + color: var(--color-text-primary);
525 + overflow-y: auto;
526 + padding-right: 0.5rem;
527 + scrollbar-width: none;
528 + -ms-overflow-style: none;
529 + }
530 + .pi-readme-content::-webkit-scrollbar {
531 + display: none;
532 + }
533 +
534 + .pi-readme-content h1,
535 + .pi-readme-content h2,
536 + .pi-readme-content h3,
537 + .pi-readme-content h4 {
538 + margin-top: 1rem;
539 + margin-bottom: 0.5rem;
540 + color: var(--color-text-primary);
541 + }
542 +
543 + .pi-readme-content h1 { font-size: 1.5rem; }
544 + .pi-readme-content h2 { font-size: 1.25rem; }
545 + .pi-readme-content h3 { font-size: 1.1rem; }
546 + .pi-readme-content h4 { font-size: 1rem; }
547 +
548 + .pi-readme-content p {
549 + margin-bottom: 0.75rem;
550 + }
551 +
552 + .pi-readme-content code {
553 + background: var(--color-panel);
554 + padding: 0.15rem 0.35rem;
555 + border-radius: 4px;
556 + font-family: var(--font-family-code);
557 + font-size: 0.85em;
558 + }
559 +
560 + .pi-readme-content pre {
561 + background: var(--color-panel);
562 + padding: 1rem;
563 + border-radius: 8px;
564 + overflow-x: auto;
565 + margin-bottom: 1rem;
566 }
567
300 - .pi-manage-actions {
568 + .pi-readme-content pre code {
569 + background: none;
570 + padding: 0;
571 + }
572 +
573 + .pi-readme-content ul,
574 + .pi-readme-content ol {
575 + margin-bottom: 0.75rem;
576 + padding-left: 1.5rem;
577 + }
578 +
579 + .pi-readme-content li {
580 + margin-bottom: 0.25rem;
581 + }
582 +
583 + .pi-readme-content a {
584 + color: var(--color-highlight);
585 + text-decoration: none;
586 + }
587 +
588 + .pi-readme-content a:hover {
589 + text-decoration: underline;
590 + }
591 +
592 + .pi-readme-content img {
593 + max-width: 100%;
594 + border-radius: 8px;
595 + margin: 0.5rem 0;
596 + }
597 +
598 + .pi-readme-content blockquote {
599 + border-left: 3px solid var(--color-border);
600 + padding-left: 1rem;
601 + margin-left: 0;
602 + color: var(--color-text-secondary);
603 + }
604 +
605 + .pi-readme-content table {
606 + width: 100%;
607 + border-collapse: collapse;
608 + margin-bottom: 1rem;
609 + }
610 +
611 + .pi-readme-content th,
612 + .pi-readme-content td {
613 + border: 1px solid var(--color-border);
614 + padding: 0.5rem;
615 + text-align: left;
616 + }
617 +
618 + .pi-readme-content th {
619 + background: var(--color-panel);
620 + }
621 +
622 + .pi-developer-section {
623 + margin-top: 2rem;
624 + padding-top: 1.5rem;
625 + border-top: 1px solid var(--color-border);
626 + }
627 +
628 + .pi-developer-header {
629 + font-size: 0.85rem;
630 + font-weight: 600;
631 + color: var(--color-text-muted);
632 + margin-bottom: 0.75rem;
633 + }
634 +
635 + .pi-developer-links {
636 display: flex;
302 - flex-wrap: wrap;
637 + flex-direction: column;
638 gap: 0.5rem;
639 }
640
306 - .pi-manage-actions .button {
307 - padding: 0.3rem 0.6rem;
641 + .pi-dev-link {
642 + display: inline-flex;
643 + align-items: center;
644 + gap: 0.5rem;
645 + padding: 0.5rem 0;
646 + text-decoration: none;
647 font-size: 0.9rem;
648 + color: var(--color-text-secondary);
649 + transition: color 0.15s;
650 }
651
311 - .pi-manage-actions .button .icon {
652 + .pi-dev-link:hover {
653 + color: var(--color-highlight);
654 + }
655 +
656 + .pi-dev-link .material-symbols-outlined {
657 font-size: 1.1rem;
658 + color: var(--color-text-muted);
659 }
660
315 - .pi-loading-inline {
316 - font-size: 0.85rem;
317 - color: var(--color-text-secondary);
318 - padding: 0.5rem 0;
661 + .pi-dev-link:hover .material-symbols-outlined {
662 + color: var(--color-highlight);
663 + }
664 +
665 + .pi-footer .buttons-container {
666 + width: 100%;
667 }
668
321 - @media (max-width: 500px) {
322 - .pi-detail-header {
669 + @media (max-width: 600px) {
670 + .pi-hero {
671 flex-direction: column;
672 align-items: center;
673 text-align: center;
674 }
675 +
676 + .pi-hero-thumb {
677 + width: 100px;
678 + height: 100px;
679 + }
680 +
681 + .pi-hero-title {
682 + font-size: 1.5rem;
683 + }
684 +
685 + .pi-hero-title-row {
686 + flex-direction: column;
687 + align-items: center;
688 + }
689 +
690 + .pi-hero-manage {
691 + justify-content: center;
692 + }
693 +
694 + .pi-hero-meta {
695 + justify-content: center;
696 + }
697 +
698 + .pi-hero-tags {
699 + justify-content: center;
700 + }
701 +
702 + .pi-actions-primary {
703 + flex-direction: column;
704 + }
705 +
706 + .pi-btn-install,
707 + .pi-btn-discussion {
708 + min-width: 100%;
709 + }
710 }
711 </style>
712 </body>
plugins/plugin_installer/webui/install-git.html
+2 -1
@@ -8,7 +8,8 @@
8 <body>
9 <div x-data>
10 <template x-if="$store.pluginInstallStore">
11 - <div x-init="$store.pluginInstallStore.resetGit()">
11 + <div x-init="$store.pluginInstallStore.resetGit()"
12 + x-destroy="$store.pluginInstallStore.refreshPluginList()">
13
14 <div class="pi-form-group">
15 <label class="pi-label">Git Repository URL</label>
plugins/plugin_installer/webui/install-index.html
+383 -112
@@ -8,35 +8,73 @@
8 <body>
9 <div x-data>
10 <template x-if="$store.pluginInstallStore">
11 - <div x-init="$store.pluginInstallStore.resetIndex(); $store.pluginInstallStore.fetchIndex()">
11 + <div class="pi-browse-shell"
12 + x-init="$store.pluginInstallStore.resetIndex(); $store.pluginInstallStore.fetchIndex()"
13 + x-destroy="$store.pluginInstallStore.refreshPluginList()">
14 +
15 + <section class="pi-browse-hero">
16 + <div class="pi-browse-copy">
17 + <div class="pi-browse-eyebrow">Community Marketplace</div>
18 + <h2 class="pi-browse-title">Discover plugins for Agent Zero</h2>
19 + <p class="pi-browse-description">
20 + Browse community plugins, filter by what you already have installed, and open each listing for screenshots, README, and install actions.
21 + </p>
22 + </div>
23 + <div class="pi-browse-summary" x-text="$store.pluginInstallStore.browseResultsSummary"></div>
24 + </section>
25
13 - <!-- Search & Sort Bar -->
26 <div class="pi-index-toolbar">
15 - <input type="text" class="pi-search-input"
16 - x-model="$store.pluginInstallStore.search"
17 - @input="$store.pluginInstallStore.page = 1"
18 - placeholder="Search plugins...">
19 - <select class="pi-sort-select"
20 - x-model="$store.pluginInstallStore.sortBy">
21 - <option value="stars">Stars</option>
22 - <option value="name">Name</option>
23 - </select>
27 + <label class="pi-search-field">
28 + <span class="material-symbols-outlined">search</span>
29 + <input type="text"
30 + class="pi-search-input"
31 + x-model="$store.pluginInstallStore.search"
32 + @input="$store.pluginInstallStore.page = 1"
33 + placeholder="Search plugins, authors, or tags...">
34 + </label>
35 +
36 + <label class="pi-sort-field">
37 + <span class="pi-field-label">Sort</span>
38 + <select class="pi-sort-select"
39 + x-model="$store.pluginInstallStore.sortBy">
40 + <option value="stars">Stars</option>
41 + <option value="name">Name</option>
42 + </select>
43 + </label>
44 + </div>
45 +
46 + <div class="pi-filter-row" x-show="$store.pluginInstallStore.browseFilters.length > 1">
47 + <template x-for="filter in $store.pluginInstallStore.browseFilters" :key="filter.key">
48 + <button type="button"
49 + class="pi-filter-chip"
50 + :class="{ active: $store.pluginInstallStore.browseFilter === filter.key }"
51 + @click="$store.pluginInstallStore.setBrowseFilter(filter.key)">
52 + <span x-text="filter.label"></span>
53 + <span class="pi-filter-count" x-text="filter.count"></span>
54 + </button>
55 + </template>
56 </div>
57
26 - <!-- Loading -->
27 - <div x-show="$store.pluginInstallStore.loading" class="pi-loading">
28 - <span x-text="$store.pluginInstallStore.loadingMessage || 'Loading...'"></span>
58 + <div x-show="$store.pluginInstallStore.loading" class="pi-state-card pi-state-loading">
59 + <span class="material-symbols-outlined">progress_activity</span>
60 + <div>
61 + <div class="pi-state-title">Loading marketplace</div>
62 + <div class="pi-state-text" x-text="$store.pluginInstallStore.loadingMessage || 'Loading plugins...'"></div>
63 + </div>
64 </div>
65
31 - <!-- Error -->
32 - <div x-show="$store.pluginInstallStore.error && !$store.pluginInstallStore.loading" class="pi-error">
33 - <span x-text="$store.pluginInstallStore.error"></span>
66 + <div x-show="$store.pluginInstallStore.error && !$store.pluginInstallStore.loading" class="pi-state-card pi-state-error">
67 + <span class="material-symbols-outlined">error</span>
68 + <div>
69 + <div class="pi-state-title">Marketplace unavailable</div>
70 + <div class="pi-state-text" x-text="$store.pluginInstallStore.error"></div>
71 + </div>
72 </div>
73
36 - <!-- Plugin Grid -->
37 - <div x-show="$store.pluginInstallStore.index && !$store.pluginInstallStore.loading" class="pi-grid">
74 + <div x-show="$store.pluginInstallStore.index && !$store.pluginInstallStore.loading && $store.pluginInstallStore.filteredPlugins.length > 0"
75 + class="pi-grid">
76 <template x-for="plugin in $store.pluginInstallStore.paginatedPlugins" :key="plugin.key">
39 - <div class="pi-card" @click="$store.pluginInstallStore.openDetail(plugin)">
77 + <button type="button" class="pi-card" @click="$store.pluginInstallStore.openDetail(plugin)">
78 <div class="pi-card-thumb">
79 <template x-if="plugin.thumbnail">
80 <img :src="plugin.thumbnail" :alt="plugin.title" loading="lazy">
@@ -44,33 +82,55 @@
82 <template x-if="!plugin.thumbnail">
83 <span class="material-symbols-outlined pi-card-placeholder">extension</span>
84 </template>
47 - <div class="pi-card-thumb-overlay">
48 - <span class="pi-card-thumb-title" x-text="plugin.title || plugin.key"></span>
49 - <template x-if="plugin.installed">
50 - <span class="pi-badge-installed">Installed</span>
51 - </template>
52 - </div>
85 +
86 + <template x-if="plugin.installed">
87 + <span class="pi-card-installed-pill">Installed</span>
88 + </template>
89 </div>
90 +
91 <div class="pi-card-body">
55 - <div class="pi-card-desc" x-text="$store.pluginInstallStore.truncate(plugin.description, 100)"></div>
92 + <div class="pi-card-head">
93 + <div class="pi-card-heading">
94 + <h3 class="pi-card-title" x-text="plugin.title || plugin.key"></h3>
95 + <div class="pi-card-subtitle"
96 + x-text="$store.pluginInstallStore.getBrowseSubtitle(plugin)">
97 + </div>
98 + </div>
99 + <span class="material-symbols-outlined pi-card-arrow">arrow_outward</span>
100 + </div>
101 +
102 + <div class="pi-card-desc"
103 + x-text="$store.pluginInstallStore.truncate(plugin.description, 110)">
104 + </div>
105 </div>
106 +
107 <div class="pi-card-footer">
58 - <span class="pi-stars">
59 - <span class="material-symbols-outlined" style="font-size:0.9rem">star</span>
108 + <span class="pi-card-stars">
109 + <span class="material-symbols-outlined">star</span>
110 <span x-text="plugin.stars || 0"></span>
111 </span>
112 +
113 + <template x-if="$store.pluginInstallStore.getBrowsePrimaryTag(plugin)">
114 + <span class="pi-card-tag"
115 + x-text="$store.pluginInstallStore.getBrowsePrimaryTag(plugin)">
116 + </span>
117 + </template>
118 </div>
63 - </div>
119 + </button>
120 </template>
121 </div>
122
67 - <!-- Empty state -->
123 <div x-show="$store.pluginInstallStore.index && !$store.pluginInstallStore.loading && $store.pluginInstallStore.filteredPlugins.length === 0"
69 - class="pi-empty">
70 - No plugins found matching your search.
124 + class="pi-state-card pi-state-empty">
125 + <span class="material-symbols-outlined">search_off</span>
126 + <div>
127 + <div class="pi-state-title">No plugins match this view</div>
128 + <div class="pi-state-text">
129 + Try a different search or switch to another marketplace filter.
130 + </div>
131 + </div>
132 </div>
133
73 - <!-- Pagination -->
134 <div x-show="$store.pluginInstallStore.totalPages > 1" class="pi-pagination">
135 <button class="button"
136 :disabled="$store.pluginInstallStore.page <= 1"
@@ -78,7 +138,7 @@
138 <span class="material-symbols-outlined">chevron_left</span>
139 </button>
140 <span class="pi-page-info"
81 - x-text="$store.pluginInstallStore.page + ' / ' + $store.pluginInstallStore.totalPages">
141 + x-text="'Page ' + $store.pluginInstallStore.page + ' of ' + $store.pluginInstallStore.totalPages">
142 </span>
143 <button class="button"
144 :disabled="$store.pluginInstallStore.page >= $store.pluginInstallStore.totalPages"
@@ -86,140 +146,353 @@
146 <span class="material-symbols-outlined">chevron_right</span>
147 </button>
148 </div>
89 -
149 </div>
150 </template>
151 </div>
152
153 <style>
154 + .pi-browse-shell {
155 + display: flex;
156 + flex-direction: column;
157 + gap: 1rem;
158 + font-family: "Rubik", Arial, Helvetica, sans-serif;
159 + }
160 +
161 + .pi-browse-shell button,
162 + .pi-browse-shell input,
163 + .pi-browse-shell select {
164 + font-family: inherit;
165 + }
166 +
167 + .pi-browse-hero {
168 + display: flex;
169 + align-items: flex-start;
170 + justify-content: space-between;
171 + gap: 1rem;
172 + padding: 0.25rem 0 0.35rem;
173 + }
174 +
175 + .pi-browse-copy {
176 + max-width: 44rem;
177 + }
178 +
179 + .pi-browse-eyebrow {
180 + margin-bottom: 0.4rem;
181 + font-size: 0.78rem;
182 + font-weight: 700;
183 + letter-spacing: 0.08em;
184 + text-transform: uppercase;
185 + color: var(--color-highlight);
186 + }
187 +
188 + .pi-browse-title {
189 + margin: 0;
190 + font-size: 1.6rem;
191 + line-height: 1.15;
192 + color: var(--color-text-primary);
193 + }
194 +
195 + .pi-browse-description {
196 + margin: 0.5rem 0 0;
197 + max-width: 38rem;
198 + font-size: 0.95rem;
199 + line-height: 1.55;
200 + color: var(--color-text-secondary);
201 + }
202 +
203 + .pi-browse-summary {
204 + flex-shrink: 0;
205 + padding: 0.6rem 0.85rem;
206 + border: 1px solid var(--color-border);
207 + border-radius: 999px;
208 + background: var(--color-panel);
209 + font-size: 0.85rem;
210 + font-weight: 600;
211 + color: var(--color-text-primary);
212 + }
213 +
214 .pi-index-toolbar {
215 display: flex;
97 - gap: 0.5rem;
98 - margin-bottom: 1rem;
216 + gap: 0.75rem;
217 + align-items: stretch;
218 }
219
101 - .pi-search-input {
220 + .pi-search-field,
221 + .pi-sort-field {
222 + display: flex;
223 + align-items: center;
224 + gap: 0.55rem;
225 + padding: 0.55rem 0;
226 + border: 0;
227 + border-bottom: 1px solid var(--color-border);
228 + border-radius: 0;
229 + background: transparent;
230 + }
231 +
232 + .pi-search-field {
233 flex: 1;
234 min-width: 0;
104 - padding: 0.5rem 0.75rem;
105 - border: 1px solid var(--color-border);
106 - border-radius: 4px;
107 - background: var(--color-bg-primary);
235 + }
236 +
237 + .pi-search-field .material-symbols-outlined {
238 + font-size: 1.15rem;
239 + color: var(--color-text-muted);
240 + }
241 +
242 + .pi-search-input {
243 + width: 100%;
244 + min-width: 0;
245 + padding: 0;
246 + border: 0;
247 + outline: none;
248 + background: transparent;
249 color: var(--color-text-primary);
250 font-size: 0.95rem;
251 }
252
112 - .pi-sort-select {
113 - max-width: 140px;
253 + .pi-sort-field {
254 + min-width: 160px;
255 flex-shrink: 0;
115 - padding: 0.5rem;
256 + justify-content: space-between;
257 + }
258 +
259 + .pi-field-label {
260 + font-size: 0.8rem;
261 + font-weight: 600;
262 + color: var(--color-text-muted);
263 + text-transform: uppercase;
264 + letter-spacing: 0.05em;
265 + }
266 +
267 + .pi-sort-select {
268 + width: 100%;
269 + padding: 0;
270 + border: 0;
271 + outline: none;
272 + background: transparent;
273 + color: var(--color-text-primary);
274 + font-size: 0.95rem;
275 + }
276 +
277 + .pi-filter-row {
278 + display: flex;
279 + flex-wrap: wrap;
280 + gap: 0.6rem;
281 + }
282 +
283 + .pi-filter-chip {
284 + display: inline-flex;
285 + align-items: center;
286 + gap: 0.45rem;
287 + padding: 0.45rem 0.75rem;
288 border: 1px solid var(--color-border);
117 - border-radius: 4px;
118 - background: var(--color-bg-primary);
289 + border-radius: 999px;
290 + background: var(--color-panel);
291 + color: var(--color-text-secondary);
292 + font-size: 0.85rem;
293 + font-weight: 600;
294 + cursor: pointer;
295 + transition: border-color 0.15s, background 0.15s, color 0.15s;
296 + }
297 +
298 + .pi-filter-chip:hover {
299 color: var(--color-text-primary);
120 - font-size: 0.9rem;
300 + background: rgba(255, 255, 255, 0.04);
301 + }
302 +
303 + .pi-filter-chip.active {
304 + border-color: var(--color-border);
305 + background: rgba(255, 255, 255, 0.08);
306 + color: var(--color-text-primary);
307 + }
308 +
309 + .pi-filter-count {
310 + padding: 0.12rem 0.4rem;
311 + border-radius: 999px;
312 + background: rgba(255, 255, 255, 0.06);
313 + color: var(--color-text-muted);
314 + font-size: 0.75rem;
315 + }
316 +
317 + .pi-state-card {
318 + display: flex;
319 + align-items: center;
320 + gap: 0.85rem;
321 + padding: 1rem 1.05rem;
322 + border: 1px solid var(--color-border);
323 + border-radius: 14px;
324 + background: var(--color-panel);
325 + }
326 +
327 + .pi-state-card .material-symbols-outlined {
328 + font-size: 1.5rem;
329 + }
330 +
331 + .pi-state-title {
332 + font-size: 0.95rem;
333 + font-weight: 600;
334 + color: var(--color-text-primary);
335 + }
336 +
337 + .pi-state-text {
338 + margin-top: 0.15rem;
339 + font-size: 0.88rem;
340 + color: var(--color-text-secondary);
341 + }
342 +
343 + .pi-state-loading .material-symbols-outlined {
344 + color: var(--color-highlight);
345 + }
346 +
347 + .pi-state-error {
348 + border-color: rgba(239, 68, 68, 0.28);
349 + background: rgba(239, 68, 68, 0.1);
350 + color: var(--color-error);
351 + }
352 +
353 + .pi-state-empty .material-symbols-outlined {
354 + color: var(--color-text-muted);
355 }
356
357 .pi-grid {
358 display: grid;
125 - grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
126 - gap: 0.75rem;
359 + grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
360 + gap: 0.95rem;
361 }
362
363 .pi-card {
364 + width: 100%;
365 + padding: 0;
366 border: 1px solid var(--color-border);
131 - border-radius: 6px;
367 + border-radius: 16px;
368 background: var(--color-background);
369 + color: inherit;
370 + text-align: left;
371 cursor: pointer;
134 - transition: border-color 0.15s, box-shadow 0.15s;
372 display: flex;
373 flex-direction: column;
374 overflow: hidden;
375 + transition: transform 0.15s, border-color 0.15s, box-shadow 0.15s;
376 }
377
378 .pi-card:hover {
141 - border-color: var(--color-highlight);
142 - box-shadow: 0 2px 8px rgba(0,0,0,0.15);
379 + transform: translateY(-2px);
380 + box-shadow: 0 12px 30px rgba(0, 0, 0, 0.18);
381 }
382
383 .pi-card-thumb {
146 - width: 100%;
147 - height: 140px;
148 - overflow: hidden;
384 + position: relative;
385 display: flex;
386 align-items: center;
387 justify-content: center;
152 - background: var(--color-panel);
153 - position: relative;
388 + height: 148px;
389 + padding: 1rem;
390 + background:
391 + radial-gradient(circle at top, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0)),
392 + var(--color-panel);
393 + border-bottom: 1px solid var(--color-border);
394 }
395
396 .pi-card-thumb img {
397 width: 100%;
398 height: 100%;
399 object-fit: contain;
160 - padding: 0.5rem;
400 }
401
402 .pi-card-placeholder {
164 - font-size: 4.5rem;
403 + font-size: 4rem;
404 color: var(--color-text-muted);
405 }
406
168 - .pi-card-thumb-overlay {
407 + .pi-card-installed-pill {
408 position: absolute;
170 - bottom: 0;
171 - left: 0;
172 - right: 0;
173 - padding: 0.4rem 0.6rem;
174 - background: rgba(0, 0, 0, 0.55);
409 + top: 0.75rem;
410 + right: 0.75rem;
411 + padding: 0.24rem 0.5rem;
412 + border-radius: 999px;
413 + background: rgba(34, 197, 94, 0.15);
414 + color: #4ade80;
415 + font-size: 0.72rem;
416 + font-weight: 700;
417 + }
418 +
419 + .pi-card-body {
420 + flex: 1;
421 display: flex;
176 - align-items: center;
177 - gap: 0.4rem;
422 + flex-direction: column;
423 + gap: 0.8rem;
424 + padding: 1rem;
425 }
426
180 - .pi-card-thumb-title {
181 - font-weight: 600;
182 - font-size: 0.85rem;
183 - color: #fff;
427 + .pi-card-head {
428 + display: flex;
429 + align-items: flex-start;
430 + justify-content: space-between;
431 + gap: 0.75rem;
432 + }
433 +
434 + .pi-card-heading {
435 + min-width: 0;
436 + }
437 +
438 + .pi-card-title {
439 + margin: 0;
440 + font-size: 1rem;
441 + line-height: 1.25;
442 + color: var(--color-text-primary);
443 + }
444 +
445 + .pi-card-subtitle {
446 + margin-top: 0.25rem;
447 + font-size: 0.82rem;
448 + color: var(--color-text-muted);
449 white-space: nowrap;
450 overflow: hidden;
451 text-overflow: ellipsis;
187 - flex: 1;
188 - min-width: 0;
452 }
453
191 - .pi-badge-installed {
192 - font-size: 0.65rem;
193 - font-weight: 600;
194 - padding: 0.1rem 0.4rem;
195 - border-radius: 3px;
196 - background: rgba(34,197,94,0.3);
197 - color: #4ade80;
454 + .pi-card-arrow {
455 + font-size: 1rem;
456 + color: var(--color-text-muted);
457 flex-shrink: 0;
458 }
459
201 - .pi-card-body {
202 - padding: 0.5rem 0.75rem;
203 - flex: 1;
204 - }
205 -
460 .pi-card-desc {
207 - font-size: 0.8rem;
461 + font-size: 0.88rem;
462 + line-height: 1.5;
463 color: var(--color-text-secondary);
209 - line-height: 1.3;
464 }
465
466 .pi-card-footer {
213 - padding: 0.4rem 0.75rem;
467 + display: flex;
468 + align-items: center;
469 + justify-content: space-between;
470 + gap: 0.5rem;
471 + padding: 0.9rem 1rem 1rem;
472 border-top: 1px solid var(--color-border);
215 - font-size: 0.8rem;
216 - color: var(--color-text-secondary);
473 }
474
219 - .pi-stars {
475 + .pi-card-stars {
476 display: inline-flex;
477 align-items: center;
222 - gap: 0.2rem;
478 + gap: 0.3rem;
479 + font-size: 0.85rem;
480 + color: var(--color-text-secondary);
481 + }
482 +
483 + .pi-card-stars .material-symbols-outlined {
484 + font-size: 1rem;
485 + color: #fbbf24;
486 + }
487 +
488 + .pi-card-tag {
489 + padding: 0.22rem 0.55rem;
490 + border-radius: 999px;
491 + font-size: 0.75rem;
492 + font-weight: 600;
493 + background: var(--color-panel);
494 + color: var(--color-text-secondary);
495 + border: 1px solid var(--color-border);
496 }
497
498 .pi-pagination {
@@ -227,8 +500,7 @@
500 align-items: center;
501 justify-content: center;
502 gap: 0.75rem;
230 - margin-top: 1rem;
231 - padding: 0.5rem 0;
503 + padding: 0.4rem 0 0.1rem;
504 }
505
506 .pi-page-info {
@@ -236,29 +508,28 @@
508 color: var(--color-text-secondary);
509 }
510
239 - .pi-loading {
240 - text-align: center;
241 - padding: 2rem;
242 - color: var(--color-text-secondary);
243 - }
511 + @media (max-width: 720px) {
512 + .pi-browse-hero {
513 + flex-direction: column;
514 + }
515
245 - .pi-error {
246 - color: var(--color-error);
247 - padding: 0.75rem;
248 - background: var(--color-error-bg, rgba(239,68,68,0.1));
249 - border-radius: 4px;
250 - }
516 + .pi-index-toolbar {
517 + flex-direction: column;
518 + }
519
252 - .pi-empty {
253 - text-align: center;
254 - padding: 2rem;
255 - color: var(--color-text-secondary);
520 + .pi-sort-field {
521 + min-width: 0;
522 + }
523 }
524
525 @media (max-width: 500px) {
526 .pi-grid {
527 grid-template-columns: 1fr;
528 }
529 +
530 + .pi-card-thumb {
531 + height: 164px;
532 + }
533 }
534 </style>
535 </body>
plugins/plugin_installer/webui/install-zip.html
+2 -1
@@ -8,7 +8,8 @@
8 <body>
9 <div x-data>
10 <template x-if="$store.pluginInstallStore">
11 - <div x-init="$store.pluginInstallStore.resetZip()">
11 + <div x-init="$store.pluginInstallStore.resetZip()"
12 + x-destroy="$store.pluginInstallStore.refreshPluginList()">
13
14 <div class="pi-upload-section">
15 <label for="plugin-zip-file" class="pi-upload-btn button confirm"
plugins/plugin_installer/webui/main.html
+423 -124
@@ -8,7 +8,8 @@
8 <body>
9 <div x-data>
10 <template x-if="$store.pluginInstallStore">
11 - <div x-init="$store.pluginInstallStore.setTab('store')">
11 + <div x-init="$store.pluginInstallStore.setTab('store')"
12 + x-destroy="$store.pluginInstallStore.refreshPluginList()">
13
14 <ul class="nav nav-tabs" role="tablist">
15 <li class="nav-item" role="presentation">
@@ -41,75 +42,141 @@
42 <div x-show="$store.pluginInstallStore.activeTab === 'store'"
43 x-init="$store.pluginInstallStore.resetIndex(); $store.pluginInstallStore.fetchIndex()">
44
44 - <div class="pi-index-toolbar">
45 - <input type="text" class="pi-search-input"
46 - x-model="$store.pluginInstallStore.search"
47 - @input="$store.pluginInstallStore.page = 1"
48 - placeholder="Search plugins...">
49 - <select class="pi-sort-select"
50 - x-model="$store.pluginInstallStore.sortBy">
51 - <option value="stars">Stars</option>
52 - <option value="name">Name</option>
53 - </select>
54 - </div>
45 + <div class="pi-browse-shell">
46 + <section class="pi-browse-hero">
47 + <div class="pi-browse-copy">
48 + <div class="pi-browse-eyebrow">Community Marketplace</div>
49 + <h2 class="pi-browse-title">Discover plugins for Agent Zero</h2>
50 + <p class="pi-browse-description">
51 + Browse community plugins, filter by what you already have installed, and open each listing for screenshots, README, and install actions.
52 + </p>
53 + </div>
54 + <div class="pi-browse-summary" x-text="$store.pluginInstallStore.browseResultsSummary"></div>
55 + </section>
56 +
57 + <div class="pi-index-toolbar">
58 + <label class="pi-search-field">
59 + <span class="material-symbols-outlined">search</span>
60 + <input type="text"
61 + class="pi-search-input"
62 + x-model="$store.pluginInstallStore.search"
63 + @input="$store.pluginInstallStore.page = 1"
64 + placeholder="Search plugins, authors, or tags...">
65 + </label>
66 +
67 + <label class="pi-sort-field">
68 + <span class="pi-field-label">Sort</span>
69 + <select class="pi-sort-select"
70 + x-model="$store.pluginInstallStore.sortBy">
71 + <option value="stars">Stars</option>
72 + <option value="name">Name</option>
73 + </select>
74 + </label>
75 + </div>
76
56 - <div x-show="$store.pluginInstallStore.loading" class="pi-loading">
57 - <span x-text="$store.pluginInstallStore.loadingMessage || 'Loading...'"></span>
58 - </div>
77 + <div class="pi-filter-row" x-show="$store.pluginInstallStore.browseFilters.length > 1">
78 + <template x-for="filter in $store.pluginInstallStore.browseFilters" :key="filter.key">
79 + <button type="button"
80 + class="pi-filter-chip"
81 + :class="{ active: $store.pluginInstallStore.browseFilter === filter.key }"
82 + @click="$store.pluginInstallStore.setBrowseFilter(filter.key)">
83 + <span x-text="filter.label"></span>
84 + <span class="pi-filter-count" x-text="filter.count"></span>
85 + </button>
86 + </template>
87 + </div>
88
60 - <div x-show="$store.pluginInstallStore.error && !$store.pluginInstallStore.loading" class="pi-error">
61 - <span x-text="$store.pluginInstallStore.error"></span>
62 - </div>
89 + <div x-show="$store.pluginInstallStore.loading" class="pi-state-card pi-state-loading">
90 + <span class="material-symbols-outlined">progress_activity</span>
91 + <div>
92 + <div class="pi-state-title">Loading marketplace</div>
93 + <div class="pi-state-text" x-text="$store.pluginInstallStore.loadingMessage || 'Loading plugins...'"></div>
94 + </div>
95 + </div>
96 +
97 + <div x-show="$store.pluginInstallStore.error && !$store.pluginInstallStore.loading" class="pi-state-card pi-state-error">
98 + <span class="material-symbols-outlined">error</span>
99 + <div>
100 + <div class="pi-state-title">Marketplace unavailable</div>
101 + <div class="pi-state-text" x-text="$store.pluginInstallStore.error"></div>
102 + </div>
103 + </div>
104 +
105 + <div x-show="$store.pluginInstallStore.index && !$store.pluginInstallStore.loading && $store.pluginInstallStore.filteredPlugins.length > 0"
106 + class="pi-grid">
107 + <template x-for="plugin in $store.pluginInstallStore.paginatedPlugins" :key="plugin.key">
108 + <button type="button" class="pi-card" @click="$store.pluginInstallStore.openDetail(plugin)">
109 + <div class="pi-card-thumb">
110 + <template x-if="plugin.thumbnail">
111 + <img :src="plugin.thumbnail" :alt="plugin.title" loading="lazy">
112 + </template>
113 + <template x-if="!plugin.thumbnail">
114 + <span class="material-symbols-outlined pi-card-placeholder">extension</span>
115 + </template>
116
64 - <div x-show="$store.pluginInstallStore.index && !$store.pluginInstallStore.loading" class="pi-grid">
65 - <template x-for="plugin in $store.pluginInstallStore.paginatedPlugins" :key="plugin.key">
66 - <div class="pi-card" @click="$store.pluginInstallStore.openDetail(plugin)">
67 - <div class="pi-card-thumb">
68 - <template x-if="plugin.thumbnail">
69 - <img :src="plugin.thumbnail" :alt="plugin.title" loading="lazy">
70 - </template>
71 - <template x-if="!plugin.thumbnail">
72 - <span class="material-symbols-outlined pi-card-placeholder">extension</span>
73 - </template>
74 - <div class="pi-card-thumb-overlay">
75 - <span class="pi-card-thumb-title" x-text="plugin.title || plugin.key"></span>
117 <template x-if="plugin.installed">
77 - <span class="pi-badge-installed">Installed</span>
118 + <span class="pi-card-installed-pill">Installed</span>
119 </template>
120 </div>
80 - </div>
81 - <div class="pi-card-body">
82 - <div class="pi-card-desc" x-text="$store.pluginInstallStore.truncate(plugin.description, 100)"></div>
83 - </div>
84 - <div class="pi-card-footer">
85 - <span class="pi-stars">
86 - <span class="material-symbols-outlined" style="font-size:0.9rem">star</span>
87 - <span x-text="plugin.stars || 0"></span>
88 - </span>
121 +
122 + <div class="pi-card-body">
123 + <div class="pi-card-head">
124 + <div class="pi-card-heading">
125 + <h3 class="pi-card-title" x-text="plugin.title || plugin.key"></h3>
126 + <div class="pi-card-subtitle"
127 + x-text="$store.pluginInstallStore.getBrowseSubtitle(plugin)">
128 + </div>
129 + </div>
130 + <span class="material-symbols-outlined pi-card-arrow">arrow_outward</span>
131 + </div>
132 +
133 + <div class="pi-card-desc"
134 + x-text="$store.pluginInstallStore.truncate(plugin.description, 110)">
135 + </div>
136 + </div>
137 +
138 + <div class="pi-card-footer">
139 + <span class="pi-card-stars">
140 + <span class="material-symbols-outlined">star</span>
141 + <span x-text="plugin.stars || 0"></span>
142 + </span>
143 +
144 + <template x-if="$store.pluginInstallStore.getBrowsePrimaryTag(plugin)">
145 + <span class="pi-card-tag"
146 + x-text="$store.pluginInstallStore.getBrowsePrimaryTag(plugin)">
147 + </span>
148 + </template>
149 + </div>
150 + </button>
151 + </template>
152 + </div>
153 +
154 + <div x-show="$store.pluginInstallStore.index && !$store.pluginInstallStore.loading && $store.pluginInstallStore.filteredPlugins.length === 0"
155 + class="pi-state-card pi-state-empty">
156 + <span class="material-symbols-outlined">search_off</span>
157 + <div>
158 + <div class="pi-state-title">No plugins match this view</div>
159 + <div class="pi-state-text">
160 + Try a different search or switch to another marketplace filter.
161 </div>
162 </div>
91 - </template>
92 - </div>
93 -
94 - <div x-show="$store.pluginInstallStore.index && !$store.pluginInstallStore.loading && $store.pluginInstallStore.filteredPlugins.length === 0"
95 - class="pi-empty">
96 - No plugins found matching your search.
97 - </div>
163 + </div>
164
99 - <div x-show="$store.pluginInstallStore.totalPages > 1" class="pi-pagination">
100 - <button class="button"
101 - :disabled="$store.pluginInstallStore.page <= 1"
102 - @click="$store.pluginInstallStore.setPage($store.pluginInstallStore.page - 1)">
103 - <span class="material-symbols-outlined">chevron_left</span>
104 - </button>
105 - <span class="pi-page-info"
106 - x-text="$store.pluginInstallStore.page + ' / ' + $store.pluginInstallStore.totalPages">
107 - </span>
108 - <button class="button"
109 - :disabled="$store.pluginInstallStore.page >= $store.pluginInstallStore.totalPages"
110 - @click="$store.pluginInstallStore.setPage($store.pluginInstallStore.page + 1)">
111 - <span class="material-symbols-outlined">chevron_right</span>
112 - </button>
165 + <div x-show="$store.pluginInstallStore.totalPages > 1" class="pi-pagination">
166 + <button class="button"
167 + :disabled="$store.pluginInstallStore.page <= 1"
168 + @click="$store.pluginInstallStore.setPage($store.pluginInstallStore.page - 1)">
169 + <span class="material-symbols-outlined">chevron_left</span>
170 + </button>
171 + <span class="pi-page-info"
172 + x-text="'Page ' + $store.pluginInstallStore.page + ' of ' + $store.pluginInstallStore.totalPages">
173 + </span>
174 + <button class="button"
175 + :disabled="$store.pluginInstallStore.page >= $store.pluginInstallStore.totalPages"
176 + @click="$store.pluginInstallStore.setPage($store.pluginInstallStore.page + 1)">
177 + <span class="material-symbols-outlined">chevron_right</span>
178 + </button>
179 + </div>
180 </div>
181 </div>
182
@@ -251,136 +318,350 @@
318 font-size: 1.1rem;
319 }
320
254 - /* ── Store Tab: Toolbar ──────────────── */
321 + /* ── Store Tab ───────────────────────── */
322 + .pi-browse-shell {
323 + display: flex;
324 + flex-direction: column;
325 + gap: 1rem;
326 + font-family: "Rubik", Arial, Helvetica, sans-serif;
327 + }
328 +
329 + .pi-browse-shell button,
330 + .pi-browse-shell input,
331 + .pi-browse-shell select {
332 + font-family: inherit;
333 + }
334 +
335 + .pi-browse-hero {
336 + display: flex;
337 + align-items: flex-start;
338 + justify-content: space-between;
339 + gap: 1rem;
340 + padding: 0.25rem 0 0.35rem;
341 + }
342 +
343 + .pi-browse-copy {
344 + max-width: 44rem;
345 + }
346 +
347 + .pi-browse-eyebrow {
348 + margin-bottom: 0.4rem;
349 + font-size: 0.78rem;
350 + font-weight: 700;
351 + letter-spacing: 0.08em;
352 + text-transform: uppercase;
353 + color: var(--color-highlight);
354 + }
355 +
356 + .pi-browse-title {
357 + margin: 0;
358 + font-size: 1.6rem;
359 + line-height: 1.15;
360 + color: var(--color-text-primary);
361 + }
362 +
363 + .pi-browse-description {
364 + margin: 0.5rem 0 0;
365 + max-width: 38rem;
366 + font-size: 0.95rem;
367 + line-height: 1.55;
368 + color: var(--color-text-secondary);
369 + }
370 +
371 + .pi-browse-summary {
372 + flex-shrink: 0;
373 + padding: 0.6rem 0.85rem;
374 + border: 1px solid var(--color-border);
375 + border-radius: 999px;
376 + background: var(--color-panel);
377 + font-size: 0.85rem;
378 + font-weight: 600;
379 + color: var(--color-text-primary);
380 + }
381 +
382 .pi-index-toolbar {
383 display: flex;
257 - gap: 0.5rem;
258 - margin-bottom: 1rem;
384 + gap: 0.75rem;
385 + align-items: stretch;
386 }
387
261 - .pi-search-input {
388 + .pi-search-field,
389 + .pi-sort-field {
390 + display: flex;
391 + align-items: center;
392 + gap: 0.55rem;
393 + padding: 0.55rem 0;
394 + border: 0;
395 + border-bottom: 1px solid var(--color-border);
396 + border-radius: 0;
397 + background: transparent;
398 + }
399 +
400 + .pi-search-field {
401 flex: 1;
402 min-width: 0;
264 - padding: 0.5rem 0.75rem;
265 - border: 1px solid var(--color-border);
266 - border-radius: 4px;
267 - background: var(--color-bg-primary);
403 + }
404 +
405 + .pi-search-field .material-symbols-outlined {
406 + font-size: 1.15rem;
407 + color: var(--color-text-muted);
408 + }
409 +
410 + .pi-search-input {
411 + width: 100%;
412 + min-width: 0;
413 + padding: 0;
414 + border: 0;
415 + outline: none;
416 + background: transparent;
417 color: var(--color-text-primary);
418 font-size: 0.95rem;
419 }
420
421 + .pi-sort-field {
422 + min-width: 160px;
423 + flex-shrink: 0;
424 + justify-content: space-between;
425 + }
426 +
427 + .pi-field-label {
428 + font-size: 0.8rem;
429 + font-weight: 600;
430 + color: var(--color-text-muted);
431 + text-transform: uppercase;
432 + letter-spacing: 0.05em;
433 + }
434 +
435 .pi-sort-select {
273 - padding: 0.5rem;
436 + width: 100%;
437 + padding: 0;
438 + border: 0;
439 + outline: none;
440 + background: transparent;
441 + color: var(--color-text-primary);
442 + font-size: 0.95rem;
443 + }
444 +
445 + .pi-filter-row {
446 + display: flex;
447 + flex-wrap: wrap;
448 + gap: 0.6rem;
449 + }
450 +
451 + .pi-filter-chip {
452 + display: inline-flex;
453 + align-items: center;
454 + gap: 0.45rem;
455 + padding: 0.45rem 0.75rem;
456 border: 1px solid var(--color-border);
275 - border-radius: 4px;
276 - background: var(--color-bg-primary);
457 + border-radius: 999px;
458 + background: var(--color-panel);
459 + color: var(--color-text-secondary);
460 + font-size: 0.85rem;
461 + font-weight: 600;
462 + cursor: pointer;
463 + transition: border-color 0.15s, background 0.15s, color 0.15s;
464 + }
465 +
466 + .pi-filter-chip:hover {
467 color: var(--color-text-primary);
278 - font-size: 0.9rem;
279 - max-width: 140px;
280 - flex-shrink: 0;
468 + background: rgba(255, 255, 255, 0.04);
469 + }
470 +
471 + .pi-filter-chip.active {
472 + border-color: var(--color-border);
473 + background: rgba(255, 255, 255, 0.08);
474 + color: var(--color-text-primary);
475 + }
476 +
477 + .pi-filter-count {
478 + padding: 0.12rem 0.4rem;
479 + border-radius: 999px;
480 + background: rgba(255, 255, 255, 0.06);
481 + color: var(--color-text-muted);
482 + font-size: 0.75rem;
483 }
484
283 - /* ── Store Tab: Grid ─────────────────── */
485 .pi-grid {
486 display: grid;
286 - grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
287 - gap: 0.75rem;
487 + grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
488 + gap: 0.95rem;
489 }
490
491 .pi-card {
492 + width: 100%;
493 + padding: 0;
494 border: 1px solid var(--color-border);
292 - border-radius: 6px;
495 + border-radius: 16px;
496 background: var(--color-background);
497 + color: inherit;
498 + text-align: left;
499 cursor: pointer;
295 - transition: border-color 0.15s, box-shadow 0.15s;
500 + transition: transform 0.15s, border-color 0.15s, box-shadow 0.15s;
501 display: flex;
502 flex-direction: column;
503 overflow: hidden;
504 }
505
506 .pi-card:hover {
302 - border-color: var(--color-highlight);
303 - box-shadow: 0 2px 8px rgba(0,0,0,0.15);
507 + transform: translateY(-2px);
508 + box-shadow: 0 12px 30px rgba(0, 0, 0, 0.18);
509 }
510
511 .pi-card-thumb {
307 - width: 100%;
308 - height: 140px;
309 - overflow: hidden;
512 + position: relative;
513 display: flex;
514 align-items: center;
515 justify-content: center;
313 - background: var(--color-panel);
314 - position: relative;
516 + height: 148px;
517 + padding: 1rem;
518 + background:
519 + radial-gradient(circle at top, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0)),
520 + var(--color-panel);
521 + border-bottom: 1px solid var(--color-border);
522 }
523
524 .pi-card-thumb img {
525 width: 100%;
526 height: 100%;
527 object-fit: contain;
321 - padding: 0.5rem;
528 }
529
530 .pi-card-placeholder {
325 - font-size: 4.5rem;
531 + font-size: 4rem;
532 color: var(--color-text-muted);
533 }
534
329 - .pi-card-thumb-overlay {
535 + .pi-card-installed-pill {
536 position: absolute;
331 - bottom: 0;
332 - left: 0;
333 - right: 0;
334 - padding: 0.4rem 0.6rem;
335 - background: rgba(0, 0, 0, 0.55);
537 + top: 0.75rem;
538 + right: 0.75rem;
539 + padding: 0.24rem 0.5rem;
540 + border-radius: 8px;
541 + background: rgba(34, 197, 94, 0.15);
542 + color: #4ade80;
543 + font-size: 0.72rem;
544 + font-weight: 700;
545 + }
546 +
547 + .pi-card-body {
548 + flex: 1;
549 display: flex;
337 - align-items: center;
338 - gap: 0.4rem;
550 + flex-direction: column;
551 + gap: 0.8rem;
552 + padding: 1rem;
553 }
554
341 - .pi-card-thumb-title {
342 - font-weight: 600;
343 - font-size: 0.85rem;
344 - color: #fff;
555 + .pi-card-head {
556 + display: flex;
557 + align-items: flex-start;
558 + justify-content: space-between;
559 + gap: 0.75rem;
560 + }
561 +
562 + .pi-card-heading {
563 + min-width: 0;
564 + }
565 +
566 + .pi-card-title {
567 + margin: 0;
568 + font-size: 1rem;
569 + line-height: 1.25;
570 + color: var(--color-text-primary);
571 + }
572 +
573 + .pi-card-subtitle {
574 + margin-top: 0.25rem;
575 + font-size: 0.82rem;
576 + color: var(--color-text-muted);
577 white-space: nowrap;
578 overflow: hidden;
579 text-overflow: ellipsis;
348 - flex: 1;
349 - min-width: 0;
580 }
581
352 - .pi-badge-installed {
353 - font-size: 0.65rem;
354 - font-weight: 600;
355 - padding: 0.1rem 0.4rem;
356 - border-radius: 3px;
357 - background: rgba(34,197,94,0.3);
358 - color: #4ade80;
582 + .pi-card-arrow {
583 + font-size: 1rem;
584 + color: var(--color-text-muted);
585 flex-shrink: 0;
586 }
587
362 - .pi-card-body {
363 - padding: 0.5rem 0.75rem;
364 - flex: 1;
365 - }
366 -
588 .pi-card-desc {
368 - font-size: 0.8rem;
589 + font-size: 0.88rem;
590 + line-height: 1.5;
591 color: var(--color-text-secondary);
370 - line-height: 1.3;
592 }
593
594 .pi-card-footer {
374 - padding: 0.4rem 0.75rem;
595 + display: flex;
596 + align-items: center;
597 + justify-content: space-between;
598 + gap: 0.5rem;
599 + padding: 0.9rem 1rem 1rem;
600 border-top: 1px solid var(--color-border);
376 - font-size: 0.8rem;
377 - color: var(--color-text-secondary);
601 }
602
380 - .pi-stars {
603 + .pi-card-stars {
604 display: inline-flex;
605 align-items: center;
383 - gap: 0.2rem;
606 + gap: 0.3rem;
607 + font-size: 0.85rem;
608 + color: var(--color-text-secondary);
609 + }
610 +
611 + .pi-card-stars .material-symbols-outlined {
612 + font-size: 1rem;
613 + color: #fbbf24;
614 + }
615 +
616 + .pi-card-tag {
617 + padding: 0.22rem 0.55rem;
618 + border-radius: 999px;
619 + font-size: 0.75rem;
620 + font-weight: 600;
621 + background: var(--color-panel);
622 + color: var(--color-text-secondary);
623 + border: 1px solid var(--color-border);
624 + }
625 +
626 + .pi-state-card {
627 + display: flex;
628 + align-items: center;
629 + gap: 0.85rem;
630 + padding: 1rem 1.05rem;
631 + border: 1px solid var(--color-border);
632 + border-radius: 14px;
633 + background: var(--color-panel);
634 + }
635 +
636 + .pi-state-card .material-symbols-outlined {
637 + font-size: 1.5rem;
638 + }
639 +
640 + .pi-state-title {
641 + font-size: 0.95rem;
642 + font-weight: 600;
643 + color: var(--color-text-primary);
644 + }
645 +
646 + .pi-state-text {
647 + margin-top: 0.15rem;
648 + font-size: 0.88rem;
649 + color: var(--color-text-secondary);
650 + }
651 +
652 + .pi-state-loading .material-symbols-outlined {
653 + color: var(--color-highlight);
654 + }
655 +
656 + .pi-state-error {
657 + border-color: rgba(239, 68, 68, 0.28);
658 + background: rgba(239, 68, 68, 0.1);
659 + color: var(--color-error);
660 + margin-top: 0;
661 + }
662 +
663 + .pi-state-empty .material-symbols-outlined {
664 + color: var(--color-text-muted);
665 }
666
667 .pi-pagination {
@@ -388,8 +669,7 @@
669 align-items: center;
670 justify-content: center;
671 gap: 0.75rem;
391 - margin-top: 1rem;
392 - padding: 0.5rem 0;
672 + padding: 0.4rem 0 0.1rem;
673 }
674
675 .pi-page-info {
@@ -397,6 +677,25 @@
677 color: var(--color-text-secondary);
678 }
679
680 + @media (max-width: 720px) {
681 + .pi-browse-hero {
682 + flex-direction: column;
683 + }
684 +
685 + .pi-index-toolbar {
686 + flex-direction: column;
687 + }
688 +
689 + .pi-sort-field {
690 + min-width: 0;
691 + }
692 + }
693 +
694 + @media (max-width: 500px) {
695 + .pi-card-thumb {
696 + height: 164px;
697 + }
698 + }
699 /* ── Git Tab ─────────────────────────── */
700 .pi-form-group {
701 margin-bottom: 1rem;
plugins/plugin_installer/webui/pluginInstallStore.js
+238 -57
@@ -1,6 +1,12 @@
1 -import { createStore } from "/js/AlpineStore.js";
1 +import { createStore, getStore } from "/js/AlpineStore.js";
2 import * as api from "/js/api.js";
3 +import { openModal } from "/js/modals.js";
4 +import { marked } from "/vendor/marked/marked.esm.js";
5 +import { toastFrontendSuccess } from "/components/notifications/notification-store.js";
6 import { showConfirmDialog } from "/js/confirmDialog.js";
7 +import "/components/plugins/list/pluginListStore.js";
8 +import "/components/plugins/list/plugin-init-store.js";
9 +import "/components/modals/image-viewer/image-viewer-store.js";
10
11 const PLUGIN_API = "plugins/plugin_installer/plugin_install";
12 const PER_PAGE = 20;
@@ -36,13 +42,26 @@ const model = {
42 search: "",
43 page: 1,
44 sortBy: "stars",
45 + browseFilter: "all",
46 selectedPlugin: null,
47
48 // Shared state
49 loading: false,
50 loadingMessage: "",
51 error: "",
45 - result: null,
52 +
53 + // README state
54 + readmeContent: null,
55 + readmeLoading: false,
56 +
57 + // Installed plugin detail (for manage buttons)
58 + installedPluginInfo: null,
59 +
60 + // Detail hero thumbnail fallbacks
61 + detailPluginName: "",
62 + /** @type {string[]} */
63 + detailThumbnailSources: [],
64 + detailThumbnailIndex: 0,
65
66 // Tab state
67 activeTab: "store",
@@ -50,7 +69,48 @@ const model = {
69 setTab(tab) {
70 this.activeTab = tab;
71 this.error = "";
53 - this.result = null;
72 + },
73 +
74 + setBrowseFilter(filter) {
75 + this.browseFilter = filter || "all";
76 + this.page = 1;
77 + },
78 +
79 + /** Normalize GitHub URL and return raw.githubusercontent.com base (no trailing slash). */
80 + _githubRawBase(githubUrl) {
81 + if (!githubUrl || typeof githubUrl !== "string") return null;
82 + let url = githubUrl.trim().replace(/\.git$/i, "");
83 + if (!url.includes("github.com")) return null;
84 + return url.replace("https://github.com/", "https://raw.githubusercontent.com/");
85 + },
86 +
87 + _pluginName(plugin) {
88 + const repoUrl = (plugin?.github || "").replace(/\.git$/, "");
89 + return repoUrl.split("/").pop() || plugin?.key || "";
90 + },
91 +
92 + _pluginPrimaryTag(plugin) {
93 + const tags = Array.isArray(plugin?.tags) ? plugin.tags.filter(Boolean) : [];
94 + return tags[0] || "";
95 + },
96 +
97 + _formatBrowseTag(tag) {
98 + if (!tag || typeof tag !== "string") return "";
99 + return tag
100 + .split(/[-_]/)
101 + .filter(Boolean)
102 + .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
103 + .join(" ");
104 + },
105 +
106 + _matchesBrowseFilter(plugin, filterKey) {
107 + if (!filterKey || filterKey === "all") return true;
108 + if (filterKey === "installed") return !!plugin?.installed;
109 + if (filterKey === "popular") return (plugin?.stars || 0) > 0;
110 + if (filterKey.startsWith("tag:")) {
111 + return this._pluginPrimaryTag(plugin) === filterKey.slice(4);
112 + }
113 + return false;
114 },
115
116 // ── ZIP Install ──────────────────────────────
@@ -61,7 +121,6 @@ const model = {
121 this.zipFile = file;
122 this.zipFileName = file.name;
123 this.error = "";
64 - this.result = null;
124 },
125
126 async installZip() {
@@ -77,7 +136,6 @@ const model = {
136 this.loading = true;
137 this.loadingMessage = "Installing plugin from ZIP...";
138 this.error = "";
80 - this.result = null;
139
140 const formData = new FormData();
141 formData.append("action", "install_zip");
@@ -94,14 +152,10 @@ const model = {
152 return;
153 }
154
97 - this.result = data;
98 - if (window.toastFrontendSuccess) {
99 - window.toastFrontendSuccess(
100 - `Plugin "${data.title || data.plugin_name}" installed`,
101 - "Plugin Installer"
102 - );
103 - }
104 - this._refreshPluginList();
155 + toastFrontendSuccess(
156 + `Plugin "${data.title || data.plugin_name}" installed`,
157 + "Plugin Installer"
158 + );
159 } catch (e) {
160 this.error = `Installation error: ${e.message}`;
161 } finally {
@@ -126,7 +180,6 @@ const model = {
180 this.loading = true;
181 this.loadingMessage = "Cloning repository...";
182 this.error = "";
129 - this.result = null;
183
184 const data = await api.callJsonApi(PLUGIN_API, {
185 action: "install_git",
@@ -139,14 +192,10 @@ const model = {
192 return;
193 }
194
142 - this.result = data;
143 - if (window.toastFrontendSuccess) {
144 - window.toastFrontendSuccess(
145 - `Plugin "${data.title || data.plugin_name}" installed`,
146 - "Plugin Installer"
147 - );
148 - }
149 - this._refreshPluginList();
195 + toastFrontendSuccess(
196 + `Plugin "${data.title || data.plugin_name}" installed`,
197 + "Plugin Installer"
198 + );
199 } catch (e) {
200 this.error = `Clone error: ${e.message}`;
201 } finally {
@@ -193,13 +242,52 @@ const model = {
242 }));
243 },
244
245 + get browseFilters() {
246 + const plugins = this.pluginsList;
247 + const filters = [{ key: "all", label: "All", count: plugins.length }];
248 +
249 + if (!plugins.length) return filters;
250 +
251 + const installedCount = plugins.filter((plugin) => plugin.installed).length;
252 + if (installedCount) {
253 + filters.push({ key: "installed", label: "Installed", count: installedCount });
254 + }
255 +
256 + const popularCount = plugins.filter((plugin) => (plugin.stars || 0) > 0).length;
257 + if (popularCount) {
258 + filters.push({ key: "popular", label: "Popular", count: popularCount });
259 + }
260 +
261 + const tagCounts = new Map();
262 + for (const plugin of plugins) {
263 + const tag = this._pluginPrimaryTag(plugin);
264 + if (!tag) continue;
265 + tagCounts.set(tag, (tagCounts.get(tag) || 0) + 1);
266 + }
267 +
268 + for (const [tag, count] of Array.from(tagCounts.entries())
269 + .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
270 + .slice(0, 4)) {
271 + filters.push({
272 + key: `tag:${tag}`,
273 + label: this._formatBrowseTag(tag),
274 + count,
275 + });
276 + }
277 +
278 + return filters;
279 + },
280 +
281 get filteredPlugins() {
197 - let list = this.pluginsList;
282 + let list = this.pluginsList.filter((plugin) =>
283 + this._matchesBrowseFilter(plugin, this.browseFilter)
284 + );
285 const q = (this.search || "").toLowerCase().trim();
286 if (q) {
287 list = list.filter(
288 (p) =>
289 (p.title || "").toLowerCase().includes(q) ||
290 + (p.author || "").toLowerCase().includes(q) ||
291 (p.description || "").toLowerCase().includes(q) ||
292 (p.key || "").toLowerCase().includes(q) ||
293 (p.tags || []).some((t) => t.toLowerCase().includes(q))
@@ -215,6 +303,16 @@ const model = {
303 return list;
304 },
305
306 + get browseResultsSummary() {
307 + const total = this.pluginsList.length;
308 + const visible = this.filteredPlugins.length;
309 + if (!total) return "No plugins available";
310 + if (visible === total) {
311 + return `${total} plugin${total === 1 ? "" : "s"} available`;
312 + }
313 + return `Showing ${visible} of ${total} plugins`;
314 + },
315 +
316 get totalPages() {
317 return Math.max(1, Math.ceil(this.filteredPlugins.length / PER_PAGE));
318 },
@@ -224,6 +322,18 @@ const model = {
322 return this.filteredPlugins.slice(start, start + PER_PAGE);
323 },
324
325 + getBrowseSubtitle(plugin) {
326 + const author = (plugin?.author || "").trim();
327 + if (author) return author;
328 + const tag = this._pluginPrimaryTag(plugin);
329 + if (tag) return this._formatBrowseTag(tag);
330 + return plugin?.key || "";
331 + },
332 +
333 + getBrowsePrimaryTag(plugin) {
334 + return this._formatBrowseTag(this._pluginPrimaryTag(plugin));
335 + },
336 +
337 setPage(p) {
338 this.page = Math.max(1, Math.min(p, this.totalPages));
339 },
@@ -231,12 +341,36 @@ const model = {
341 openDetail(plugin) {
342 this.selectedPlugin = plugin;
343 this.error = "";
234 - this.result = null;
344 this.installedPluginInfo = null;
345 + this.readmeContent = null;
346 + this.detailPluginName = this._pluginName(plugin);
347 + this.detailThumbnailSources = this.getDetailThumbnailSources(plugin);
348 + this.detailThumbnailIndex = 0;
349 if (plugin.installed) {
237 - this.fetchInstalledPluginInfo(plugin.key);
350 + this.fetchInstalledPluginInfo(this.detailPluginName);
351 + }
352 + this.fetchReadme(plugin);
353 + openModal("/plugins/plugin_installer/webui/install-detail.html");
354 + },
355 +
356 + async fetchReadme(plugin) {
357 + const rawBase = this._githubRawBase(plugin?.github);
358 + if (!rawBase) return;
359 +
360 + try {
361 + this.readmeLoading = true;
362 + this.readmeContent = null;
363 +
364 + const response = await fetch(`${rawBase}/main/README.md`);
365 + if (response.ok) {
366 + const readme = await response.text();
367 + this.readmeContent = marked.parse(readme, { breaks: true });
368 + }
369 + } catch (e) {
370 + console.warn("Failed to fetch readme:", e);
371 + } finally {
372 + this.readmeLoading = false;
373 }
239 - window.openModal?.("../plugins/plugin_installer/webui/install-detail.html");
374 },
375
376 async installFromIndex(plugin) {
@@ -252,7 +386,6 @@ const model = {
386 this.loading = true;
387 this.loadingMessage = `Installing ${plugin.title || plugin.key}...`;
388 this.error = "";
255 - this.result = null;
389
390 const data = await api.callJsonApi(PLUGIN_API, {
391 action: "install_git",
@@ -264,19 +397,20 @@ const model = {
397 return;
398 }
399
267 - this.result = data;
400 if (!this.installedPlugins.includes(plugin.key)) {
401 this.installedPlugins.push(plugin.key);
402 }
403 plugin.installed = true;
272 -
273 - if (window.toastFrontendSuccess) {
274 - window.toastFrontendSuccess(
275 - `Plugin "${data.title || data.plugin_name}" installed`,
276 - "Plugin Installer"
277 - );
278 - }
279 - this._refreshPluginList();
404 + this.selectedPlugin = { ...this.selectedPlugin, installed: true };
405 + this.detailPluginName = data.plugin_name;
406 + this.detailThumbnailSources = this.getDetailThumbnailSources(this.selectedPlugin);
407 + this.detailThumbnailIndex = 0;
408 + this.fetchInstalledPluginInfo(data.plugin_name);
409 +
410 + toastFrontendSuccess(
411 + `Plugin "${data.title || data.plugin_name}" installed`,
412 + "Plugin Installer"
413 + );
414 } catch (e) {
415 this.error = `Installation error: ${e.message}`;
416 } finally {
@@ -287,59 +421,65 @@ const model = {
421
422 // ── Installed Plugin Info ─────────────────────
423
290 - installedPluginInfo: null,
291 - installedPluginInfoLoading: false,
292 -
293 - async fetchInstalledPluginInfo(pluginKey) {
424 + async fetchInstalledPluginInfo(pluginName) {
425 this.installedPluginInfo = null;
295 - this.installedPluginInfoLoading = true;
426 try {
427 const response = await api.callJsonApi("plugins_list", {
428 filter: { custom: true, builtin: true, search: "" },
429 });
430 const plugins = Array.isArray(response.plugins) ? response.plugins : [];
301 - this.installedPluginInfo = plugins.find(
302 - (p) => p.name === pluginKey
303 - ) || null;
431 + this.installedPluginInfo = plugins.find((p) => p.name === pluginName) || null;
432 } catch (e) {
433 this.installedPluginInfo = null;
306 - } finally {
307 - this.installedPluginInfoLoading = false;
434 }
435 },
436
437 + _pluginListStore() {
438 + return getStore("pluginListStore");
439 + },
440 +
441 + _pluginInitStore() {
442 + return getStore("pluginInitStore");
443 + },
444 +
445 manageOpenPlugin() {
446 const info = this.installedPluginInfo;
447 if (!info?.name || !info?.has_main_screen) return;
314 - window.openModal?.(`/plugins/${info.name}/webui/main.html`);
448 + openModal(`/plugins/${info.name}/webui/main.html`);
449 },
450
451 async manageOpenConfig() {
318 - const pls = Alpine.store("pluginListStore");
452 + const pls = this._pluginListStore();
453 if (pls?.openPluginConfig && this.installedPluginInfo) {
454 await pls.openPluginConfig(this.installedPluginInfo);
455 }
456 },
457
458 async manageOpenDoc(doc) {
325 - const pls = Alpine.store("pluginListStore");
459 + const pls = this._pluginListStore();
460 if (pls?.openPluginDoc && this.installedPluginInfo) {
461 await pls.openPluginDoc(this.installedPluginInfo, doc);
462 }
463 },
464
465 manageOpenInfo() {
332 - const pls = Alpine.store("pluginListStore");
466 + const pls = this._pluginListStore();
467 if (pls?.openPluginInfo && this.installedPluginInfo) {
468 pls.openPluginInfo(this.installedPluginInfo);
469 }
470 },
471
472 + manageOpenInit() {
473 + const initStore = this._pluginInitStore();
474 + if (initStore?.open && this.installedPluginInfo) {
475 + initStore.open(this.installedPluginInfo);
476 + }
477 + },
478 +
479 async manageDeletePlugin() {
339 - const pls = Alpine.store("pluginListStore");
480 + const pls = this._pluginListStore();
481 if (pls?.deletePlugin && this.installedPluginInfo) {
482 await pls.deletePlugin(this.installedPluginInfo);
342 - // Mark as no longer installed in the index view
483 if (this.selectedPlugin) {
484 this.selectedPlugin.installed = false;
485 const idx = this.installedPlugins.indexOf(this.selectedPlugin.key);
@@ -349,33 +489,74 @@ const model = {
489 }
490 },
491
492 + getIndexUrl(pluginKey) {
493 + if (!pluginKey) return "";
494 + return `https://github.com/agent0ai/a0-plugins/tree/main/plugins/${pluginKey}`;
495 + },
496 +
497 + getThumbnailUrl(plugin) {
498 + if (!plugin) return null;
499 + if (plugin.thumbnail && typeof plugin.thumbnail === "string") return plugin.thumbnail;
500 + const rawBase = this._githubRawBase(plugin?.github);
501 + return rawBase ? `${rawBase}/main/thumbnail.png` : null;
502 + },
503 +
504 + getLocalThumbnailUrl() {
505 + const name = this.detailPluginName;
506 + return name ? `/plugins/${name}/thumbnail.png` : null;
507 + },
508 +
509 + /**
510 + * Build the ordered thumbnail URLs for the detail view.
511 + * First try GitHub, then the local plugin asset.
512 + * @param {any} plugin
513 + * @returns {string[]}
514 + */
515 + getDetailThumbnailSources(plugin) {
516 + const currentPlugin = plugin || this.selectedPlugin;
517 + const sources = [
518 + this.getThumbnailUrl(currentPlugin),
519 + this.getLocalThumbnailUrl(),
520 + ];
521 + return sources.filter(
522 + (url, index) => typeof url === "string" && sources.indexOf(url) === index
523 + );
524 + },
525 +
526 + getDetailThumbnailUrl() {
527 + return this.detailThumbnailSources[this.detailThumbnailIndex] || null;
528 + },
529 +
530 + nextDetailThumbnail() {
531 + this.detailThumbnailIndex += 1;
532 + },
533 +
534 // ── Shared ───────────────────────────────────
535
536 resetZip() {
537 this.zipFile = null;
538 this.zipFileName = "";
539 this.error = "";
358 - this.result = null;
540 },
541
542 resetGit() {
543 this.gitUrl = "";
544 this.gitToken = "";
545 this.error = "";
365 - this.result = null;
546 },
547
548 resetIndex() {
549 this.search = "";
550 this.page = 1;
551 this.sortBy = "stars";
552 + this.browseFilter = "all";
553 this.error = "";
373 - this.result = null;
554 this.selectedPlugin = null;
555 },
556
377 - _refreshPluginList() {
378 - window.dispatchEvent(new CustomEvent("plugin-modal-closed"));
557 + /** Called from x-destroy when the installer modal is torn down; refreshes the plugin list store */
558 + refreshPluginList() {
559 + getStore("pluginListStore")?.refresh();
560 },
561
562 truncate(text, max) {
webui/components/plugins/list/plugin-list.html
+4 -2
@@ -7,7 +7,7 @@
7 </head>
8
9 <body>
10 - <div x-data x-on:plugin-modal-closed.window="$store.pluginListStore.refresh()">
10 + <div x-data>
11 <template x-if="$store.pluginListStore">
12 <div x-init="$store.pluginListStore.init && $store.pluginListStore.init()">
13
@@ -224,7 +224,9 @@
224
225 .plugins-toolbar-actions {
226 margin-left: auto;
227 + display: flex;
228 flex: 0 0 auto;
229 + gap: var(--spacing-sm);
230 }
231
232 .plugins-list {
@@ -233,7 +235,7 @@
235
236 .plugin-card {
237 border: 1px solid var(--color-border);
236 - border-radius: 4px;
238 + border-radius: 0.5rem;
239 padding: 0.75rem;
240 margin-top: 0.75rem;
241 background: var(--color-background);
webui/components/plugins/plugin-settings.html
+1 -1
@@ -9,7 +9,7 @@
9 <div x-data>
10 <template x-if="$store.pluginSettings">
11 <div x-create="$store.pluginSettings.onModalOpen()"
12 - x-destroy="$store.pluginSettings.cleanup(); window.dispatchEvent(new Event('plugin-modal-closed'))">
12 + x-destroy="$store.pluginSettings.cleanup(); $store.pluginListStore?.refresh()">
13
14 <!-- Context toolbar: Project + Agent profile (only when at least one scope is configurable) -->
15 <div class="plugin-settings-scope-section"
webui/components/plugins/toggle/plugin-toggle-advanced.html
+1 -1
@@ -9,7 +9,7 @@
9 <div x-data>
10 <template x-if="$store.pluginToggle">
11 <div x-create="$store.pluginToggle.open($store.pluginListStore?.selectedPlugin || '')"
12 - x-destroy="$store.pluginToggle.cleanup(); window.dispatchEvent(new Event('plugin-modal-closed'))">
12 + x-destroy="$store.pluginToggle.cleanup(); $store.pluginListStore?.refresh()">
13
14 <!-- Error -->
15 <div x-show="$store.pluginToggle.error" class="plugin-settings-error">