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;