surface marketplace browse tab and polish plugin detail UX
Make marketplace discovery easier to find from the main Plugins screen and tighten the plugin detail layout so metadata and actions stay readable across breakpoints. - add a Browse tab to the main plugin list by reusing the existing marketplace component and matching its store icon - refresh marketplace data after detail/install actions when that tab is active - keep title, installed state, metadata, and tags grouped together while letting detail actions wrap in their own column - add the repository "Give a star" link and move screenshots below the description
Alessandro committed
Mar 10, 2026 at 16:24 UTC
976937fae956b6d103c54f926a99d1d372bc410d
4 files changed
+221
-157
plugins/plugin_installer/webui/install-detail.html
+61
-36
@@ -23,7 +23,40 @@
23
</div>
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>
26
+ <div class="pi-hero-main">
27
+ <h2 class="pi-hero-title" x-text="$store.pluginInstallStore.selectedPlugin.title || $store.pluginInstallStore.selectedPlugin.key"></h2>
28
+ <template x-if="$store.pluginInstallStore.selectedPlugin.installed">
29
+ <div class="pi-installed-badge-simple">
30
+ <span class="material-symbols-outlined">check_circle</span>
31
+ <span>Installed</span>
32
+ </div>
33
+ </template>
34
+ <div class="pi-hero-meta">
35
+ <template x-if="$store.pluginInstallStore.selectedPlugin.author">
36
+ <span class="pi-hero-author">
37
+ <span class="pi-hero-label">Author</span>
38
+ <span x-text="$store.pluginInstallStore.selectedPlugin.author"></span>
39
+ </span>
40
+ </template>
41
+ <span class="pi-hero-stars">
42
+ <span class="material-symbols-outlined">star</span>
43
+ <span x-text="($store.pluginInstallStore.selectedPlugin.stars || 0)"></span>
44
+ <template x-if="$store.pluginInstallStore.selectedPlugin.github">
45
+ <a :href="$store.pluginInstallStore.selectedPlugin.github"
46
+ target="_blank"
47
+ rel="noopener noreferrer"
48
+ class="pi-dev-link">
49
+ Give a star
50
+ </a>
51
+ </template>
52
+ </span>
53
+ </div>
54
+ <div class="pi-hero-tags" x-show="$store.pluginInstallStore.selectedPlugin.tags?.length">
55
+ <template x-for="tag in $store.pluginInstallStore.selectedPlugin.tags || []" :key="tag">
56
+ <span class="pi-tag" x-text="tag"></span>
57
+ </template>
58
+ </div>
59
+ </div>
60
<template x-if="$store.pluginInstallStore.selectedPlugin.installed && $store.pluginInstallStore.installedPluginInfo">
61
<div class="pi-hero-manage">
62
<template x-if="$store.pluginInstallStore.installedPluginInfo.has_main_screen">
@@ -63,32 +96,11 @@
96
</div>
97
</template>
98
</div>
66
- <template x-if="$store.pluginInstallStore.selectedPlugin.installed">
67
- <div class="pi-installed-badge-simple">
68
- <span class="material-symbols-outlined">check_circle</span>
69
- <span>Installed</span>
70
- </div>
71
- </template>
72
- <div class="pi-hero-meta">
73
- <template x-if="$store.pluginInstallStore.selectedPlugin.author">
74
- <span class="pi-hero-author">
75
- <span class="pi-hero-label">Author</span>
76
- <span x-text="$store.pluginInstallStore.selectedPlugin.author"></span>
77
- </span>
78
- </template>
79
- <span class="pi-hero-stars">
80
- <span class="material-symbols-outlined">star</span>
81
- <span x-text="($store.pluginInstallStore.selectedPlugin.stars || 0)"></span>
82
- </span>
83
- </div>
84
- <div class="pi-hero-tags" x-show="$store.pluginInstallStore.selectedPlugin.tags?.length">
85
- <template x-for="tag in $store.pluginInstallStore.selectedPlugin.tags || []" :key="tag">
86
- <span class="pi-tag" x-text="tag"></span>
87
- </template>
88
- </div>
99
</div>
100
</div>
101
102
+ <div class="pi-description" x-text="$store.pluginInstallStore.selectedPlugin.description || 'No description available.'"></div>
103
+
104
<div class="pi-screenshots-section"
105
x-show="$store.pluginInstallStore.selectedPlugin.screenshots?.length">
106
<div class="pi-screenshots-header">Screenshots</div>
@@ -102,8 +114,6 @@
114
</div>
115
</div>
116
105
- <div class="pi-description" x-text="$store.pluginInstallStore.selectedPlugin.description || 'No description available.'"></div>
106
-
117
<div class="pi-actions-primary">
118
<template x-if="!$store.pluginInstallStore.selectedPlugin.installed">
119
<button class="pi-btn-install"
@@ -227,19 +237,27 @@
237
}
238
239
.pi-hero-title-row {
230
- display: flex;
240
+ display: grid;
241
+ grid-template-columns: minmax(0, 1fr) clamp(18rem, 42%, 32rem);
242
align-items: flex-start;
232
- justify-content: space-between;
233
- gap: 0.75rem;
243
+ column-gap: 1rem;
244
+ row-gap: 0.5rem;
245
margin-bottom: 0.5rem;
246
}
247
248
+ .pi-hero-main {
249
+ min-width: 0;
250
+ width: 100%;
251
+ row-gap: var(--spacing-xxs);
252
+ }
253
+
254
.pi-hero-title {
255
margin: 0;
256
font-size: 1.75rem;
257
font-weight: 600;
258
line-height: 1.2;
259
color: var(--color-text-primary);
260
+ min-width: 0;
261
}
262
263
.pi-hero-manage {
@@ -247,8 +265,9 @@
265
align-items: center;
266
gap: 0.5rem;
267
flex-wrap: wrap;
250
- flex-shrink: 0;
268
justify-content: flex-end;
269
+ align-content: flex-start;
270
+ width: 100%;
271
}
272
273
.pi-hero-manage .button {
@@ -289,6 +308,7 @@
308
gap: 0.3rem;
309
font-size: 0.95rem;
310
color: var(--color-text-secondary);
311
+ flex-wrap: wrap;
312
}
313
314
.pi-hero-stars .material-symbols-outlined {
@@ -309,7 +329,7 @@
329
font-size: 0.9rem;
330
font-weight: 600;
331
color: #22c55e;
312
- margin-bottom: 0.35rem;
332
+ margin: var(--spacing-xs) 0;
333
}
334
335
.pi-installed-badge-simple .material-symbols-outlined {
@@ -462,8 +482,8 @@
482
}
483
484
.pi-screenshots-section {
465
- margin-top: 1.5rem;
466
- padding-top: 1.5rem;
485
+ margin: var(--spacing-lg) 0;
486
+ padding-top: var(--spacing-lg);
487
border-top: 1px solid var(--color-border);
488
}
489
@@ -634,7 +654,7 @@
654
gap: 0.5rem;
655
padding: 0.5rem 0;
656
text-decoration: none;
637
- font-size: 0.9rem;
657
+ font-size: 0.8rem;
658
color: var(--color-text-secondary);
659
transition: color 0.15s;
660
}
@@ -673,8 +693,8 @@
693
}
694
695
.pi-hero-title-row {
676
- flex-direction: column;
677
- align-items: center;
696
+ grid-template-columns: 1fr;
697
+ justify-items: center;
698
}
699
700
.pi-hero-manage {
@@ -685,6 +705,11 @@
705
justify-content: center;
706
}
707
708
+ .pi-hero-stars {
709
+ flex-basis: 100%;
710
+ justify-content: center;
711
+ }
712
+
713
.pi-hero-tags {
714
justify-content: center;
715
}
plugins/plugin_installer/webui/pluginInstallStore.js
+4
-1
@@ -569,8 +569,11 @@ const model = {
569
this.selectedPlugin = null;
570
},
571
572
- /** Called from x-destroy when the installer modal is torn down; refreshes the plugin list store */
572
+ /** Refresh related list views after installer/detail actions. */
573
refreshPluginList() {
574
+ if (pluginListStore.activeTab === "marketplace") {
575
+ void this.fetchIndex();
576
+ }
577
pluginListStore.refresh();
578
},
579
webui/components/plugins/list/plugin-list.html
+147
-120
@@ -36,130 +36,150 @@
36
Builtin
37
</button>
38
</li>
39
- </ul>
40
-
41
- <div class="plugins-toolbar">
42
- <div class="plugins-toolbar-search">
43
- <input type="text"
44
- class="plugins-search-input"
45
- placeholder="Search plugins by name or description..."
46
- x-model="$store.pluginListStore.searchQuery">
47
- </div>
48
- <div class="plugins-toolbar-actions">
49
- <button type="button"
50
- class="button confirm"
51
- title="Refresh"
52
- @click="$store.pluginListStore.refresh()"
53
- :disabled="$store.pluginListStore.loading">
54
- <span class="icon material-symbols-outlined">refresh</span> Refresh
39
+ <li class="nav-item" role="presentation">
40
+ <button class="nav-link"
41
+ :class="{ active: $store.pluginListStore.activeTab === 'marketplace' }"
42
+ id="plugins-marketplace-tab"
43
+ type="button"
44
+ role="tab"
45
+ aria-controls="plugins-marketplace"
46
+ :aria-selected="$store.pluginListStore.activeTab === 'marketplace'"
47
+ @click="$store.pluginListStore.setTab('marketplace')">
48
+ <span class="material-symbols-outlined pi-tab-icon">store</span> Browse
49
</button>
50
+ </li>
51
+ </ul>
52
57
- <x-extension id="plugins-list-header-buttons"></x-extension>
58
- </div>
59
- </div>
60
-
61
- <div x-show="$store.pluginListStore.loading" class="loading">
62
- <span>Loading plugins...</span>
63
- </div>
64
-
65
- <div class="plugins-list">
66
- <template x-for="plugin in (($store.pluginListStore.plugins || []).filter((plugin) => {
67
- const q = (($store.pluginListStore.searchQuery || '').trim().toLowerCase());
68
- if (!q) return true;
69
- const name = (plugin.display_name || plugin.name || '').toLowerCase();
70
- const description = (plugin.description || '').toLowerCase();
71
- return name.includes(q) || description.includes(q);
72
- }))" :key="plugin.path || plugin.name">
73
- <div class="plugin-card">
74
- <div class="plugin-header">
75
- <div class="plugin-heading">
76
- <div class="plugin-title" x-text="plugin.display_name || plugin.name || '(unnamed plugin)'"></div>
77
- <code class="plugin-path" x-text="plugin.path"></code>
78
- </div>
79
- <div class="plugin-actions">
80
- <template x-if="plugin.has_main_screen">
81
- <button type="button"
82
- class="button"
83
- title="Open"
84
- @click="$store.pluginListStore.openPlugin(plugin)">
85
- <span class="icon material-symbols-outlined">dashboard</span> Open
86
- </button>
87
- </template>
88
- <template x-if="plugin.has_config_screen">
89
- <button type="button"
90
- class="button"
91
- title="Config"
92
- @click="$store.pluginListStore.openPluginConfig(plugin)">
93
- <span class="icon material-symbols-outlined">settings</span> Config
94
- </button>
95
- </template>
96
- <template x-if="plugin.has_readme">
97
- <button type="button"
98
- class="button"
99
- title="README"
100
- @click="$store.pluginListStore.openPluginDoc(plugin, 'readme')">
101
- <span class="icon material-symbols-outlined">description</span> README
102
- </button>
103
- </template>
104
- <template x-if="plugin.has_license">
105
- <button type="button"
106
- class="button"
107
- title="LICENSE"
108
- @click="$store.pluginListStore.openPluginDoc(plugin, 'license')">
109
- <span class="icon material-symbols-outlined">gavel</span> License
110
- </button>
111
- </template>
112
- <template x-if="plugin.has_init_script">
113
- <button type="button"
114
- class="button"
115
- title="Run initializer script"
116
- @click="$store.pluginListStore.openPluginInit(plugin)">
117
- <span class="icon material-symbols-outlined">terminal</span> Init
118
- </button>
119
- </template>
120
- <button type="button"
121
- class="button"
122
- title="Info"
123
- @click="$store.pluginListStore.openPluginInfo(plugin)">
124
- <span class="icon material-symbols-outlined">info</span>
125
- </button>
126
- <template x-if="plugin.is_custom">
127
- <button type="button"
128
- class="button cancel icon-button"
129
- title="Delete"
130
- @click="$confirmClick($event, () => $store.pluginListStore.deletePlugin(plugin))">
131
- <span class="icon material-symbols-outlined">delete</span>
132
- </button>
133
- </template>
134
- </div>
53
+ <template x-if="$store.pluginListStore.activeTab !== 'marketplace'">
54
+ <div>
55
+ <div class="plugins-toolbar">
56
+ <div class="plugins-toolbar-search">
57
+ <input type="text"
58
+ class="plugins-search-input"
59
+ placeholder="Search plugins by name or description..."
60
+ x-model="$store.pluginListStore.searchQuery">
61
</div>
136
-
137
- <div class="plugin-footer-row">
138
- <div class="plugin-description" x-text="plugin.description || 'No description provided.'"></div>
139
- <div class="plugin-status-group">
140
- <template x-if="plugin.toggle_state === 'advanced'">
141
- <button type="button"
142
- class="button icon-button"
143
- title="Open Advanced"
144
- @click="$store.pluginListStore.openPluginAdvancedToggle(plugin)">
145
- <span class="icon material-symbols-outlined">rule_settings</span>
146
- </button>
147
- </template>
148
- <select class="plugin-status-select"
149
- @change="$store.pluginListStore.updateToggle(plugin, $event.target.value)"
150
- @click.stop
151
- :disabled="plugin.always_enabled">
152
- <option value="enabled" :selected="plugin.toggle_state === 'enabled'">ON</option>
153
- <option value="disabled" :selected="plugin.toggle_state === 'disabled'">OFF</option>
154
- <template x-if="plugin.per_project_config || plugin.per_agent_config">
155
- <option value="advanced" :selected="plugin.toggle_state === 'advanced'">Advanced</option>
156
- </template>
157
- </select>
158
- </div>
62
+ <div class="plugins-toolbar-actions">
63
+ <button type="button"
64
+ class="button confirm"
65
+ title="Refresh"
66
+ @click="$store.pluginListStore.refresh()"
67
+ :disabled="$store.pluginListStore.loading">
68
+ <span class="icon material-symbols-outlined">refresh</span> Refresh
69
+ </button>
70
+
71
+ <x-extension id="plugins-list-header-buttons"></x-extension>
72
</div>
73
</div>
161
- </template>
162
- </div>
74
+
75
+ <div x-show="$store.pluginListStore.loading" class="loading">
76
+ <span>Loading plugins...</span>
77
+ </div>
78
+
79
+ <div class="plugins-list">
80
+ <template x-for="plugin in (($store.pluginListStore.plugins || []).filter((plugin) => {
81
+ const q = (($store.pluginListStore.searchQuery || '').trim().toLowerCase());
82
+ if (!q) return true;
83
+ const name = (plugin.display_name || plugin.name || '').toLowerCase();
84
+ const description = (plugin.description || '').toLowerCase();
85
+ return name.includes(q) || description.includes(q);
86
+ }))" :key="plugin.path || plugin.name">
87
+ <div class="plugin-card">
88
+ <div class="plugin-header">
89
+ <div class="plugin-heading">
90
+ <div class="plugin-title" x-text="plugin.display_name || plugin.name || '(unnamed plugin)'"></div>
91
+ <code class="plugin-path" x-text="plugin.path"></code>
92
+ </div>
93
+ <div class="plugin-actions">
94
+ <template x-if="plugin.has_main_screen">
95
+ <button type="button"
96
+ class="button"
97
+ title="Open"
98
+ @click="$store.pluginListStore.openPlugin(plugin)">
99
+ <span class="icon material-symbols-outlined">dashboard</span> Open
100
+ </button>
101
+ </template>
102
+ <template x-if="plugin.has_config_screen">
103
+ <button type="button"
104
+ class="button"
105
+ title="Config"
106
+ @click="$store.pluginListStore.openPluginConfig(plugin)">
107
+ <span class="icon material-symbols-outlined">settings</span> Config
108
+ </button>
109
+ </template>
110
+ <template x-if="plugin.has_readme">
111
+ <button type="button"
112
+ class="button"
113
+ title="README"
114
+ @click="$store.pluginListStore.openPluginDoc(plugin, 'readme')">
115
+ <span class="icon material-symbols-outlined">description</span> README
116
+ </button>
117
+ </template>
118
+ <template x-if="plugin.has_license">
119
+ <button type="button"
120
+ class="button"
121
+ title="LICENSE"
122
+ @click="$store.pluginListStore.openPluginDoc(plugin, 'license')">
123
+ <span class="icon material-symbols-outlined">gavel</span> License
124
+ </button>
125
+ </template>
126
+ <template x-if="plugin.has_init_script">
127
+ <button type="button"
128
+ class="button"
129
+ title="Run initializer script"
130
+ @click="$store.pluginListStore.openPluginInit(plugin)">
131
+ <span class="icon material-symbols-outlined">terminal</span> Init
132
+ </button>
133
+ </template>
134
+ <button type="button"
135
+ class="button"
136
+ title="Info"
137
+ @click="$store.pluginListStore.openPluginInfo(plugin)">
138
+ <span class="icon material-symbols-outlined">info</span>
139
+ </button>
140
+ <template x-if="plugin.is_custom">
141
+ <button type="button"
142
+ class="button cancel icon-button"
143
+ title="Delete"
144
+ @click="$confirmClick($event, () => $store.pluginListStore.deletePlugin(plugin))">
145
+ <span class="icon material-symbols-outlined">delete</span>
146
+ </button>
147
+ </template>
148
+ </div>
149
+ </div>
150
+
151
+ <div class="plugin-footer-row">
152
+ <div class="plugin-description" x-text="plugin.description || 'No description provided.'"></div>
153
+ <div class="plugin-status-group">
154
+ <template x-if="plugin.toggle_state === 'advanced'">
155
+ <button type="button"
156
+ class="button icon-button"
157
+ title="Open Advanced"
158
+ @click="$store.pluginListStore.openPluginAdvancedToggle(plugin)">
159
+ <span class="icon material-symbols-outlined">rule_settings</span>
160
+ </button>
161
+ </template>
162
+ <select class="plugin-status-select"
163
+ @change="$store.pluginListStore.updateToggle(plugin, $event.target.value)"
164
+ @click.stop
165
+ :disabled="plugin.always_enabled">
166
+ <option value="enabled" :selected="plugin.toggle_state === 'enabled'">ON</option>
167
+ <option value="disabled" :selected="plugin.toggle_state === 'disabled'">OFF</option>
168
+ <template x-if="plugin.per_project_config || plugin.per_agent_config">
169
+ <option value="advanced" :selected="plugin.toggle_state === 'advanced'">Advanced</option>
170
+ </template>
171
+ </select>
172
+ </div>
173
+ </div>
174
+ </div>
175
+ </template>
176
+ </div>
177
+ </div>
178
+ </template>
179
+
180
+ <template x-if="$store.pluginListStore.activeTab === 'marketplace'">
181
+ <x-component path="/plugins/plugin_installer/webui/install-index.html"></x-component>
182
+ </template>
183
</div>
184
</template>
185
</div>
@@ -168,7 +188,7 @@
188
.nav {
189
display: flex;
190
padding-left: 0;
171
- margin: 0.75rem 0 0 0;
191
+ margin: 0 0 1rem 0;
192
list-style: none;
193
border-bottom: 1px solid var(--color-border);
194
gap: 0.25rem;
@@ -183,6 +203,9 @@
203
background: transparent;
204
color: var(--color-text-secondary);
205
cursor: pointer;
206
+ display: inline-flex;
207
+ align-items: center;
208
+ gap: 0.3rem;
209
}
210
211
.nav-link.active {
@@ -192,6 +215,10 @@
215
background: var(--color-background);
216
}
217
218
+ .pi-tab-icon {
219
+ font-size: 1.1rem;
220
+ }
221
+
222
.plugins-toolbar {
223
display: flex;
224
align-items: center;
webui/components/plugins/list/pluginListStore.js
+9
@@ -39,6 +39,12 @@ const model = {
39
},
40
41
async setTab(tab) {
42
+ if (tab === "marketplace") {
43
+ this.activeTab = "marketplace";
44
+ this.loading = false;
45
+ return;
46
+ }
47
+
48
this.activeTab = tab === "builtin" ? "builtin" : "custom";
49
const filter =
50
this.activeTab === "builtin"
@@ -48,6 +54,9 @@ const model = {
54
},
55
56
async refresh() {
57
+ if (this.activeTab === "marketplace") {
58
+ return;
59
+ }
60
await this.setTab(this.activeTab);
61
},
62