improve Plugin Hub cards and plugin info
Surface repository metadata for installed custom plugins and add quick actions from the plugin info modal to open plugin files or jump back into the Plugin Hub. Refresh Plugin Hub card/status styling and modal spacing.
Alessandro committed
Mar 21, 2026 at 13:01 UTC
6b8a6d959abcb7fd50c93804317184ad2edeb3a0
9 files changed
+349
-88
helpers/plugins.py
+12
-3
@@ -86,6 +86,8 @@ class PluginListItem(BaseModel):
86
display_name: str = ""
87
description: str = ""
88
version: str = ""
89
+ author: str = ""
90
+ repo: str = ""
91
settings_sections: List[str] = Field(default_factory=list)
92
per_project_config: bool = False
93
per_agent_config: bool = False
@@ -254,11 +256,16 @@ def get_enhanced_plugins_list(
256
break
257
current_commit = ""
258
current_commit_timestamp = ""
259
+ author = ""
260
+ repo_name = ""
261
if is_custom:
262
repo_info = git.get_repo_release_info(str(d))
259
- if repo_info.is_git_repo and repo_info.head:
260
- current_commit = repo_info.head.hash
261
- current_commit_timestamp = repo_info.head.committed_at
263
+ if repo_info.is_git_repo:
264
+ author = repo_info.author
265
+ repo_name = repo_info.repo
266
+ if repo_info.head:
267
+ current_commit = repo_info.head.hash
268
+ current_commit_timestamp = repo_info.head.committed_at
269
results.append(
270
PluginListItem(
271
name=d.name,
@@ -266,6 +273,8 @@ def get_enhanced_plugins_list(
273
display_name=meta.title or d.name,
274
description=meta.description,
275
version=meta.version,
276
+ author=author,
277
+ repo=repo_name,
278
settings_sections=meta.settings_sections,
279
per_project_config=meta.per_project_config,
280
per_agent_config=meta.per_agent_config,
plugins/_plugin_installer/webui/install-detail.html
+32
-1
@@ -290,7 +290,8 @@
290
display: flex;
291
align-items: center;
292
justify-content: center;
293
- background: var(--color-panel);
293
+ background: radial-gradient(circle at top, #6b728089, var(--color-panel)),
294
+ var(--color-panel);
295
border: 1px solid var(--color-border);
296
}
297
@@ -404,6 +405,36 @@
405
gap: 0.4rem;
406
}
407
408
+ .pi-card-installed-pill {
409
+ display: inline-flex;
410
+ padding: 0.24rem 0.5rem;
411
+ border-radius: 0.5rem;
412
+ background: rgba(34, 197, 94, 0.15);
413
+ color: #4ade80;
414
+ font-size: 0.72rem;
415
+ font-weight: 700;
416
+ }
417
+
418
+ body.light-mode .pi-card-installed-pill {
419
+ background: rgba(34, 197, 94, 0.22);
420
+ color: #166534;
421
+ }
422
+
423
+ .pi-card-update-pill {
424
+ display: inline-flex;
425
+ padding: 0.24rem 0.5rem;
426
+ border-radius: 0.5rem;
427
+ background: rgba(59, 130, 246, 0.16);
428
+ color: #60a5fa;
429
+ font-size: 0.72rem;
430
+ font-weight: 700;
431
+ }
432
+
433
+ body.light-mode .pi-card-update-pill {
434
+ background: rgba(59, 130, 246, 0.2);
435
+ color: #1d4ed8;
436
+ }
437
+
438
.pi-status-badges .pi-card-installed-pill {
439
position: static;
440
top: auto;
plugins/_plugin_installer/webui/install-index.html
+155
-68
@@ -86,18 +86,13 @@
86
x-text="$store.pluginInstallStore.getBrowseSubtitle(plugin)">
87
</div>
88
89
- <div class="pi-card-pills">
90
- <template x-if="plugin.installed">
91
- <span class="pi-card-installed-pill">Installed</span>
92
- </template>
93
- <template x-if="plugin.has_update">
94
- <span class="pi-card-update-pill">New version</span>
95
- </template>
96
- </div>
89
+ <span class="pi-card-stars">
90
+ <span class="material-symbols-outlined">star</span>
91
+ <span x-text="plugin.stars || 0"></span>
92
+ </span>
93
94
</div>
95
100
- <span class="material-symbols-outlined pi-card-arrow">arrow_outward</span>
96
</div>
97
98
<div class="pi-card-body">
@@ -109,16 +104,32 @@
104
</div>
105
106
<div class="pi-card-footer">
112
- <span class="pi-card-stars">
113
- <span class="material-symbols-outlined">star</span>
114
- <span x-text="plugin.stars || 0"></span>
115
- </span>
116
-
117
- <template x-if="$store.pluginInstallStore.getBrowsePrimaryTag(plugin)">
118
- <span class="pi-card-tag"
119
- x-text="$store.pluginInstallStore.getBrowsePrimaryTag(plugin)">
120
- </span>
121
- </template>
107
+
108
+ <div class="pi-card-bubbles pi-card-bubbles-tags">
109
+ <template x-if="$store.pluginInstallStore.getBrowsePrimaryTag(plugin)">
110
+ <span class="pi-card-bubble pi-card-bubble-tag">
111
+ <span class="material-symbols-outlined">sell</span>
112
+ <span class="pi-card-bubble-text"
113
+ x-text="$store.pluginInstallStore.getBrowsePrimaryTag(plugin)"></span>
114
+ </span>
115
+ </template>
116
+ </div>
117
+
118
+ <div class="pi-card-bubbles pi-card-bubbles-status">
119
+ <template x-if="plugin.installed">
120
+ <span class="pi-card-bubble pi-card-bubble-installed">
121
+ <span class="material-symbols-outlined">check_circle</span>
122
+ <span class="pi-card-bubble-text">Installed</span>
123
+ </span>
124
+ </template>
125
+ <template x-if="plugin.has_update">
126
+ <span class="pi-card-bubble pi-card-bubble-update">
127
+ <span class="material-symbols-outlined">update</span>
128
+ <span class="pi-card-bubble-text">New version</span>
129
+ </span>
130
+ </template>
131
+ </div>
132
+
133
</div>
134
</button>
135
</template>
@@ -394,7 +405,7 @@
405
width: 100%;
406
padding: 0;
407
border: 1px solid var(--color-border);
397
- border-radius: 16px;
408
+ border-radius: 10px;
409
background: var(--color-background);
410
color: inherit;
411
text-align: left;
@@ -425,7 +436,8 @@
436
justify-content: center;
437
border-radius: 0.5rem;
438
overflow: hidden;
428
- background: var(--color-panel);
439
+ background: radial-gradient(circle at top, #6b728089, var(--color-panel)),
440
+ var(--color-panel);
441
border: 1px solid var(--color-border);
442
}
443
@@ -481,51 +493,12 @@
493
color: var(--color-text-secondary);
494
}
495
484
- .pi-card-pills {
485
- display: flex;
486
- flex-wrap: wrap;
487
- padding: var(--spacing-xs) 0;
488
- gap: 0.4rem;
489
- }
490
-
491
- .pi-card-pills
492
-
493
- .pi-card-installed-pill {
494
- display: inline-flex;
495
- padding: 0.24rem 0.5rem;
496
- border-radius: 0.5rem;
497
- background: rgba(34, 197, 94, 0.15);
498
- color: #4ade80;
499
- font-size: 0.72rem;
500
- font-weight: 700;
501
- }
502
-
503
- body.light-mode .pi-card-installed-pill {
504
- background: rgba(34, 197, 94, 0.22);
505
- color: #166534;
506
- }
507
-
508
- .pi-card-update-pill {
509
- display: inline-flex;
510
- padding: 0.24rem 0.5rem;
511
- border-radius: 0.5rem;
512
- background: rgba(59, 130, 246, 0.16);
513
- color: #60a5fa;
514
- font-size: 0.72rem;
515
- font-weight: 700;
516
- }
517
-
518
- body.light-mode .pi-card-update-pill {
519
- background: rgba(59, 130, 246, 0.2);
520
- color: #1d4ed8;
521
- }
522
-
496
.pi-card-footer {
497
display: flex;
498
align-items: center;
499
justify-content: space-between;
527
- gap: 0.5rem;
528
- padding: 0.9rem 1rem 1rem;
500
+ gap: 0.75rem;
501
+ padding: 0.65rem 0.85rem 0.75rem;
502
border-top: 1px solid var(--color-border);
503
}
504
@@ -533,6 +506,7 @@
506
display: inline-flex;
507
align-items: center;
508
gap: 0.3rem;
509
+ padding: var(--spacing-xxs) 0;
510
font-size: 0.85rem;
511
color: var(--color-text-secondary);
512
}
@@ -542,14 +516,127 @@
516
color: #fbbf24;
517
}
518
545
- .pi-card-tag {
546
- padding: 0.22rem 0.55rem;
547
- border-radius: 0.5rem;
548
- font-size: 0.75rem;
549
- font-weight: 600;
519
+ .pi-card-bubbles {
520
+ display: flex;
521
+ align-items: center;
522
+ gap: 0.35rem;
523
+ flex-shrink: 0;
524
+ }
525
+
526
+ .pi-card-bubbles-tags {
527
+ min-width: 0;
528
+ flex: 1 1 auto;
529
+ justify-content: flex-start;
530
+ }
531
+
532
+ .pi-card-bubbles-status {
533
+ justify-content: flex-end;
534
+ }
535
+
536
+ .pi-card-bubble {
537
+ display: inline-flex;
538
+ align-items: center;
539
+ height: 26px;
540
+ padding: 0 0.45rem;
541
+ border-radius: 6px;
542
+ border: 1px solid var(--color-border);
543
background: var(--color-panel);
544
+ overflow: hidden;
545
+ cursor: default;
546
+ transition: background 0.2s ease, border-color 0.2s ease;
547
+ }
548
+
549
+ .pi-card-bubble .material-symbols-outlined {
550
+ font-size: 0.95rem;
551
+ flex-shrink: 0;
552
+ line-height: 1;
553
+ }
554
+
555
+ .pi-card-bubble-text {
556
+ max-width: 0;
557
+ opacity: 0;
558
+ overflow: hidden;
559
+ white-space: nowrap;
560
+ font-size: 0.72rem;
561
+ font-weight: 600;
562
+ transition: max-width 0.25s ease, opacity 0.2s ease, margin-left 0.2s ease;
563
+ margin-left: 0;
564
+ }
565
+
566
+ /* Category tag (left): label always visible */
567
+ .pi-card-bubbles-tags .pi-card-bubble {
568
+ min-width: 0;
569
+ max-width: 100%;
570
+ }
571
+
572
+ .pi-card-bubbles-tags .pi-card-bubble-text {
573
+ flex: 1 1 auto;
574
+ min-width: 0;
575
+ max-width: 10rem;
576
+ opacity: 1;
577
+ margin-left: 0.3rem;
578
+ text-overflow: ellipsis;
579
+ }
580
+
581
+ /* Status pills (right): icon until hover */
582
+ .pi-card-bubbles-status .pi-card-bubble:hover .pi-card-bubble-text {
583
+ max-width: 8rem;
584
+ opacity: 1;
585
+ margin-left: 0.3rem;
586
+ }
587
+
588
+ .pi-card-bubble-tag .material-symbols-outlined {
589
+ color: var(--color-text-muted);
590
+ }
591
+
592
+ .pi-card-bubble-tag .pi-card-bubble-text {
593
color: var(--color-text-secondary);
552
- border: 1px solid var(--color-border);
594
+ }
595
+
596
+ .pi-card-bubble-installed .material-symbols-outlined {
597
+ color: #4ade80;
598
+ }
599
+
600
+ .pi-card-bubble-installed .pi-card-bubble-text {
601
+ color: #4ade80;
602
+ }
603
+
604
+ .pi-card-bubble-installed:hover {
605
+ background: rgba(34, 197, 94, 0.1);
606
+ border-color: rgba(34, 197, 94, 0.3);
607
+ }
608
+
609
+ .pi-card-bubble-update .material-symbols-outlined {
610
+ color: #60a5fa;
611
+ }
612
+
613
+ .pi-card-bubble-update .pi-card-bubble-text {
614
+ color: #60a5fa;
615
+ }
616
+
617
+ .pi-card-bubble-update:hover {
618
+ background: rgba(59, 130, 246, 0.1);
619
+ border-color: rgba(59, 130, 246, 0.3);
620
+ }
621
+
622
+ body.light-mode .pi-card-bubble-installed .material-symbols-outlined,
623
+ body.light-mode .pi-card-bubble-installed .pi-card-bubble-text {
624
+ color: #166534;
625
+ }
626
+
627
+ body.light-mode .pi-card-bubble-installed:hover {
628
+ background: rgba(34, 197, 94, 0.12);
629
+ border-color: rgba(34, 197, 94, 0.35);
630
+ }
631
+
632
+ body.light-mode .pi-card-bubble-update .material-symbols-outlined,
633
+ body.light-mode .pi-card-bubble-update .pi-card-bubble-text {
634
+ color: #1d4ed8;
635
+ }
636
+
637
+ body.light-mode .pi-card-bubble-update:hover {
638
+ background: rgba(59, 130, 246, 0.1);
639
+ border-color: rgba(59, 130, 246, 0.35);
640
}
641
642
.pi-pagination {
plugins/_plugin_installer/webui/pluginInstallStore.js
+15
-3
@@ -608,9 +608,21 @@ const model = {
608
},
609
610
handleOpenInfo() {
611
- if (this.installedPluginInfo) {
612
- pluginListStore.openPluginInfo(this.installedPluginInfo);
613
- }
611
+ if (!this.installedPluginInfo) return;
612
+ const pluginHubKey = (this.selectedPlugin?.key || "").trim();
613
+ const plugin = pluginHubKey
614
+ ? {
615
+ ...this.installedPluginInfo,
616
+ pluginHub: {
617
+ key: pluginHubKey,
618
+ title:
619
+ this.selectedPlugin?.title ||
620
+ this.installedPluginInfo.display_name ||
621
+ this.installedPluginInfo.name,
622
+ },
623
+ }
624
+ : this.installedPluginInfo;
625
+ pluginListStore.openPluginInfo(plugin);
626
},
627
628
handleOpenExecute() {
webui/components/plugins/list/plugin-list.html
+1
-1
@@ -291,7 +291,7 @@
291
292
.plugin-card {
293
border: 1px solid var(--color-border);
294
- border-radius: 0.5rem;
294
+ border-radius: 10px;
295
padding: 0.75rem;
296
margin-top: 0.75rem;
297
background: var(--color-background);
webui/components/plugins/list/pluginListStore.js
+15
@@ -3,6 +3,7 @@ import * as api from "/js/api.js";
3
import { store as pluginSettingsStore } from "/components/plugins/plugin-settings-store.js";
4
import { store as pluginToggleStore } from "/components/plugins/toggle/plugin-toggle-store.js";
5
import { store as pluginExecuteStore } from "/components/plugins/list/plugin-execute-store.js";
6
+import { store as fileBrowserStore } from "/components/modals/file-browser/file-browser-store.js";
7
import { store as markdownModalStore } from "/components/modals/markdown/markdown-store.js";
8
import { callJsExtensions } from "/js/extensions.js";
9
import {
@@ -156,6 +157,20 @@ const model = {
157
window.openModal?.("components/plugins/plugin-info.html");
158
},
159
160
+ async openPluginFolder(plugin) {
161
+ if (!plugin?.path) return;
162
+ await fileBrowserStore.open(plugin.path);
163
+ },
164
+
165
+ async openPluginHub(plugin) {
166
+ const pluginKey = (plugin?.pluginHub?.key || "").trim();
167
+ if (!pluginKey) return;
168
+ const { store: pluginInstallStore } = await import(
169
+ "/plugins/_plugin_installer/webui/pluginInstallStore.js"
170
+ );
171
+ await pluginInstallStore.openPluginHubDetailByKey(pluginKey);
172
+ },
173
+
174
async deletePlugin(plugin) {
175
if (!plugin?.name) return;
176
webui/components/plugins/plugin-info.html
+117
-10
@@ -10,15 +10,25 @@
10
<template x-if="$store.pluginListStore && $store.pluginListStore.selectedPlugin">
11
<div class="plugin-info-view">
12
<div class="plugin-info-header">
13
- <h2 class="plugin-info-title"
14
- x-text="$store.pluginListStore.selectedPlugin.display_name || $store.pluginListStore.selectedPlugin.name"></h2>
15
- <div class="plugin-info-source"
16
- x-text="$store.pluginListStore.selectedPlugin.is_custom ? 'Custom plugin' : 'Builtin plugin'"></div>
13
+ <template x-if="$store.pluginListStore.selectedPlugin.thumbnail_url">
14
+ <div class="plugin-info-thumb">
15
+ <img :src="$store.pluginListStore.selectedPlugin.thumbnail_url"
16
+ :alt="$store.pluginListStore.selectedPlugin.display_name || $store.pluginListStore.selectedPlugin.name">
17
+ </div>
18
+ </template>
19
+ <div class="plugin-info-header-main">
20
+ <div class="plugin-info-heading">
21
+ <h2 class="plugin-info-title"
22
+ x-text="$store.pluginListStore.selectedPlugin.display_name || $store.pluginListStore.selectedPlugin.name"></h2>
23
+ <div class="plugin-info-source"
24
+ x-text="$store.pluginListStore.selectedPlugin.is_custom ? 'Custom plugin' : 'Builtin plugin'"></div>
25
+ </div>
26
+
27
+ <div class="plugin-info-description"
28
+ x-text="$store.pluginListStore.selectedPlugin.description || 'No description provided.'"></div>
29
+ </div>
30
</div>
31
19
- <div class="plugin-info-description"
20
- x-text="$store.pluginListStore.selectedPlugin.description || 'No description provided.'"></div>
21
-
32
<div class="plugin-info-grid">
33
<div class="plugin-info-label">Plugin ID:</div>
34
<div class="plugin-info-value">
@@ -29,6 +39,15 @@
39
<div class="plugin-info-value"
40
x-text="$store.pluginListStore.selectedPlugin.version || 'n/a'"></div>
41
42
+ <template x-if="$store.pluginListStore.selectedPlugin.author && $store.pluginListStore.selectedPlugin.repo">
43
+ <div class="plugin-info-grid-pair">
44
+ <div class="plugin-info-label">Repository:</div>
45
+ <div class="plugin-info-value">
46
+ <code x-text="`${$store.pluginListStore.selectedPlugin.author}/${$store.pluginListStore.selectedPlugin.repo}`"></code>
47
+ </div>
48
+ </div>
49
+ </template>
50
+
51
<div class="plugin-info-label">Path:</div>
52
<div class="plugin-info-value">
53
<code x-text="$store.pluginListStore.selectedPlugin.path"></code>
@@ -56,10 +75,32 @@
75
</div>
76
</div>
77
</template>
59
- </div>
78
61
- <div class="modal-footer" data-modal-footer>
62
- <button class="btn btn-cancel" @click="window.closeModal?.()">Close</button>
79
+ <template x-if="$store.pluginListStore?.selectedPlugin">
80
+ <div class="modal-footer plugin-info-footer" data-modal-footer>
81
+ <div class="plugin-info-footer-row">
82
+ <div class="plugin-info-footer-left">
83
+ <button type="button"
84
+ class="button"
85
+ title="Open plugin files"
86
+ @click="$store.pluginListStore.openPluginFolder($store.pluginListStore.selectedPlugin)">
87
+ <span class="icon material-symbols-outlined">folder_open</span> Files
88
+ </button>
89
+ <template x-if="$store.pluginListStore.selectedPlugin.pluginHub?.key">
90
+ <button type="button"
91
+ class="button"
92
+ title="Open in Plugin Hub"
93
+ @click="$store.pluginListStore.openPluginHub($store.pluginListStore.selectedPlugin)">
94
+ <span class="icon material-symbols-outlined">storefront</span> Hub
95
+ </button>
96
+ </template>
97
+ </div>
98
+ <div class="plugin-info-footer-right">
99
+ <button class="btn btn-cancel" @click="window.closeModal?.()">Close</button>
100
+ </div>
101
+ </div>
102
+ </div>
103
+ </template>
104
</div>
105
106
<style>
@@ -68,6 +109,38 @@
109
}
110
111
.plugin-info-header {
112
+ display: flex;
113
+ align-items: flex-start;
114
+ gap: 1rem;
115
+ margin-bottom: 1rem;
116
+ }
117
+
118
+ .plugin-info-thumb {
119
+ width: 96px;
120
+ height: 96px;
121
+ flex-shrink: 0;
122
+ display: flex;
123
+ align-items: center;
124
+ justify-content: center;
125
+ overflow: hidden;
126
+ border: 1px solid var(--color-border);
127
+ border-radius: 12px;
128
+ background: var(--color-panel);
129
+ }
130
+
131
+ .plugin-info-thumb img {
132
+ width: 100%;
133
+ height: 100%;
134
+ object-fit: contain;
135
+ padding: 0.5rem;
136
+ }
137
+
138
+ .plugin-info-header-main {
139
+ flex: 1;
140
+ min-width: 0;
141
+ }
142
+
143
+ .plugin-info-heading {
144
display: flex;
145
flex-wrap: wrap;
146
align-items: baseline;
@@ -97,6 +170,10 @@
170
gap: 0.5rem 0.75rem;
171
}
172
173
+ .plugin-info-grid-pair {
174
+ display: contents;
175
+ }
176
+
177
.plugin-info-label {
178
font-weight: 600;
179
color: var(--color-text-secondary);
@@ -108,7 +185,27 @@
185
overflow-wrap: anywhere;
186
}
187
188
+ .plugin-info-footer-row {
189
+ width: 100%;
190
+ display: flex;
191
+ align-items: center;
192
+ justify-content: space-between;
193
+ gap: 0.75rem;
194
+ flex-wrap: wrap;
195
+ }
196
+
197
+ .plugin-info-footer-left,
198
+ .plugin-info-footer-right {
199
+ display: flex;
200
+ gap: 0.5rem;
201
+ flex-wrap: wrap;
202
+ }
203
+
204
@media (max-width: 640px) {
205
+ .plugin-info-header {
206
+ flex-direction: column;
207
+ }
208
+
209
.plugin-info-grid {
210
grid-template-columns: 1fr;
211
gap: 0.25rem;
@@ -117,6 +214,16 @@
214
.plugin-info-label {
215
margin-top: 0.5rem;
216
}
217
+
218
+ .plugin-info-footer-row {
219
+ flex-direction: column;
220
+ align-items: stretch;
221
+ }
222
+
223
+ .plugin-info-footer-left,
224
+ .plugin-info-footer-right {
225
+ width: 100%;
226
+ }
227
}
228
</style>
229
</body>
webui/components/settings/plugins/plugins-subsection.html
+1
-1
@@ -61,7 +61,7 @@
61
62
.plugin-card {
63
border: 1px solid var(--color-border);
64
- border-radius: 4px;
64
+ border-radius: 10px;
65
padding: 0.75rem;
66
margin-top: 0.75rem;
67
background: var(--color-background);
webui/css/modals.css
+1
-1
@@ -176,7 +176,7 @@ some classes like modal-header are shared between the old and the new system */
176
display: flex;
177
justify-content: flex-end;
178
align-items: center;
179
- padding: 0.6rem 2rem 0.6rem 0;
179
+ padding: var(--spacing-sm) var(--spacing-lg);
180
border-top: 1px solid var(--color-border);
181
background: var(--color-background);
182
gap: 1rem;