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">