marketplace polish; add confirm-dialog extension hook and plugin_scan marketplace note

Add a generic JS extension point for the shared confirm dialog so plugins can augment the warning body and footer without hardcoding scanner logic in the installer. - webui/js/confirmDialog.js: call callJsExtensions('confirm_dialog_after_render') with dialog/body/footer nodes and optional extensionContext; defer show until extensions run; add isClosed guard and Enter key handling for extension buttons. - plugins/plugin_installer: pass extensionContext only from installFromIndex() (kind: marketplace_plugin_install_warning, gitUrl, etc.); ZIP/Git install flows unchanged. - plugins/plugin_scan: new extension confirm_dialog_after_render that, for marketplace install warnings, appends recommendation text and a "Scan with A0" button that closes the dialog and opens the scanner modal with the repo URL. - webui/css/modals.css: margin for .confirm-dialog-extension-note. - docs: document confirm_dialog_after_render and when to use JS hooks vs HTML breakpoints (AGENTS.plugins.md, developer/plugins.md).

Alessandro committed Mar 9, 2026 at 11:41 UTC ae62cfc1cae4c0d53179eda43ae2dda38cbb18c9
9 files changed +147 -20
docs/agents/AGENTS.plugins.md
+2
@@ -80,6 +80,8 @@ To contribute:
80 ### JS Hooks
81 Place *.js files in extensions/webui/<extension_point>/ and export a default async function. They are called via callJsExtensions("<point>", context).
82
83 +Core JS hooks can also expose runtime UI surfaces when static HTML breakpoints are not a fit. For example, `confirm_dialog_after_render` runs after the shared confirm dialog is built and receives the rendered dialog/body/footer nodes plus any caller-provided `extensionContext`.
84 +
85 ---
86
87 ## 4. Plugin Settings
docs/developer/plugins.md
+5
@@ -225,3 +225,8 @@ A built-in **Plugin Marketplace** (always-active plugin) will allow users to bro
225 - `docs/agents/AGENTS.plugins.md` for full architecture details
226 - `skills/a0-create-plugin/SKILL.md` for plugin authoring workflow (agent-facing)
227 - `plugins/README.md` for core plugin directory overview
228 +
229 +## Frontend Extension Notes
230 +
231 +- HTML breakpoints are preferred when the core template already exposes an `x-extension` anchor.
232 +- JS hooks are the right fit for runtime-built UI surfaces. For example, `confirm_dialog_after_render` can extend the shared confirm dialog using the supplied DOM nodes and caller `extensionContext`.
plugins/README.md
+1 -1
@@ -11,7 +11,7 @@ This directory contains the system-level plugins bundled with Agent Zero.
11
12 For detailed guides on how to create, extend, or configure plugins, refer to:
13
14 -- [`docs/agents/AGENTS.plugins.md`](../AGENTS.plugins.md): Full-stack plugin architecture, manifest format, extension points, and Plugin Index submission.
14 +- [`docs/agents/AGENTS.plugins.md`](../docs/agents/AGENTS.plugins.md): Full-stack plugin architecture, manifest format, extension points, and Plugin Index submission.
15 - [`docs/developer/plugins.md`](../docs/developer/plugins.md): Human-facing developer guide covering the full plugin lifecycle.
16 - [`AGENTS.md`](../AGENTS.md): Main framework guide and backend context.
17 - [`skills/a0-create-plugin/SKILL.md`](../skills/a0-create-plugin/SKILL.md): Agent-facing authoring workflow (local and community plugins).
plugins/plugin_installer/webui/install-index.html
+5 -5
@@ -204,7 +204,7 @@
204 flex-shrink: 0;
205 padding: 0.6rem 0.85rem;
206 border: 1px solid var(--color-border);
207 - border-radius: 999px;
207 + border-radius: 0.5rem;
208 background: var(--color-panel);
209 font-size: 0.85rem;
210 font-weight: 600;
@@ -286,7 +286,7 @@
286 gap: 0.45rem;
287 padding: 0.45rem 0.75rem;
288 border: 1px solid var(--color-border);
289 - border-radius: 999px;
289 + border-radius: 0.5rem;
290 background: var(--color-panel);
291 color: var(--color-text-secondary);
292 font-size: 0.85rem;
@@ -308,7 +308,7 @@
308
309 .pi-filter-count {
310 padding: 0.12rem 0.4rem;
311 - border-radius: 999px;
311 + border-radius: 0.5rem;
312 background: rgba(255, 255, 255, 0.06);
313 color: var(--color-text-muted);
314 font-size: 0.75rem;
@@ -409,7 +409,7 @@
409 top: 0.75rem;
410 right: 0.75rem;
411 padding: 0.24rem 0.5rem;
412 - border-radius: 999px;
412 + border-radius: 0.5rem;
413 background: rgba(34, 197, 94, 0.15);
414 color: #4ade80;
415 font-size: 0.72rem;
@@ -487,7 +487,7 @@
487
488 .pi-card-tag {
489 padding: 0.22rem 0.55rem;
490 - border-radius: 999px;
490 + border-radius: 0.5rem;
491 font-size: 0.75rem;
492 font-weight: 600;
493 background: var(--color-panel);
plugins/plugin_installer/webui/main.html
+4 -4
@@ -372,7 +372,7 @@
372 flex-shrink: 0;
373 padding: 0.6rem 0.85rem;
374 border: 1px solid var(--color-border);
375 - border-radius: 999px;
375 + border-radius: 0.5rem;
376 background: var(--color-panel);
377 font-size: 0.85rem;
378 font-weight: 600;
@@ -454,7 +454,7 @@
454 gap: 0.45rem;
455 padding: 0.45rem 0.75rem;
456 border: 1px solid var(--color-border);
457 - border-radius: 999px;
457 + border-radius: 0.5rem;
458 background: var(--color-panel);
459 color: var(--color-text-secondary);
460 font-size: 0.85rem;
@@ -476,7 +476,7 @@
476
477 .pi-filter-count {
478 padding: 0.12rem 0.4rem;
479 - border-radius: 999px;
479 + border-radius: 0.5rem;
480 background: rgba(255, 255, 255, 0.06);
481 color: var(--color-text-muted);
482 font-size: 0.75rem;
@@ -615,7 +615,7 @@
615
616 .pi-card-tag {
617 padding: 0.22rem 0.55rem;
618 - border-radius: 999px;
618 + border-radius: 0.5rem;
619 font-size: 0.75rem;
620 font-weight: 600;
621 background: var(--color-panel);
plugins/plugin_installer/webui/pluginInstallStore.js
+10 -1
@@ -379,7 +379,16 @@ const model = {
379 return;
380 }
381
382 - const confirmed = await showConfirmDialog(SECURITY_WARNING);
382 + const confirmed = await showConfirmDialog({
383 + ...SECURITY_WARNING,
384 + extensionContext: {
385 + kind: "marketplace_plugin_install_warning",
386 + source: "plugin_installer",
387 + pluginKey: plugin.key || "",
388 + pluginTitle: plugin.title || plugin.key || "",
389 + gitUrl: plugin.github,
390 + },
391 + });
392 if (!confirmed) return;
393
394 try {
plugins/plugin_scan/extensions/webui/confirm_dialog_after_render/add-marketplace-scan-action.js new
+48
@@ -0,0 +1,48 @@
1 +import { store as pluginScanStore } from "/plugins/plugin_scan/webui/plugin-scan-store.js";
2 +
3 +const NOTE_CLASS = "confirm-dialog-extension-note";
4 +const BUTTON_CLASS = "confirm-dialog-plugin-scan-button";
5 +const DIALOG_CLOSE_DELAY_MS = 220;
6 +
7 +function isMarketplaceInstallWarning(extensionContext) {
8 + return (
9 + extensionContext?.kind === "marketplace_plugin_install_warning"
10 + && extensionContext?.source === "plugin_installer"
11 + && typeof extensionContext?.gitUrl === "string"
12 + && extensionContext.gitUrl.trim().length > 0
13 + );
14 +}
15 +
16 +export default async function addMarketplaceScanAction(context) {
17 + const extensionContext = context?.extensionContext;
18 + if (!isMarketplaceInstallWarning(extensionContext)) return;
19 +
20 + const bodyElement = context?.bodyElement;
21 + const footerElement = context?.footerElement;
22 + const cancelButton = context?.cancelButton;
23 + if (!bodyElement || !footerElement || !cancelButton) return;
24 +
25 + if (!bodyElement.querySelector(`.${NOTE_CLASS}`)) {
26 + const note = document.createElement("p");
27 + note.className = NOTE_CLASS;
28 + note.textContent = "It is always recommended to scan all plugins and updates with A0 itself.";
29 + bodyElement.appendChild(note);
30 + }
31 +
32 + if (footerElement.querySelector(`.${BUTTON_CLASS}`)) return;
33 +
34 + const scanButton = document.createElement("button");
35 + scanButton.type = "button";
36 + scanButton.className = `button ${BUTTON_CLASS}`;
37 + scanButton.textContent = "Scan with A0";
38 + scanButton.addEventListener("click", (event) => {
39 + event.preventDefault();
40 + event.stopPropagation();
41 + context.close(false);
42 + window.setTimeout(() => {
43 + void pluginScanStore.openModal(extensionContext.gitUrl.trim());
44 + }, DIALOG_CLOSE_DELAY_MS);
45 + });
46 +
47 + footerElement.insertBefore(scanButton, cancelButton);
48 +}
webui/css/modals.css
+4
@@ -589,6 +589,10 @@ input[type="range"]::-moz-range-thumb {
589 margin: 0;
590 }
591
592 +.confirm-dialog-body .confirm-dialog-extension-note {
593 + margin-top: 0.75em;
594 +}
595 +
596 .confirm-dialog-footer {
597 display: flex;
598 justify-content: flex-end;
webui/js/confirmDialog.js
+68 -9
@@ -1,5 +1,7 @@
1 // Custom confirmation dialog. CSS in /css/modals.css
2
3 +import { callJsExtensions } from "/js/extensions.js";
4 +
5 const DIALOG_TYPES = {
6 warning: { icon: 'warning', color: 'var(--color-warning, #f59e0b)' },
7 danger: { icon: 'error', color: 'var(--color-error, #ef4444)' },
@@ -12,7 +14,8 @@ export function showConfirmDialog(options) {
14 message = '',
15 confirmText = 'Confirm',
16 cancelText = 'Cancel',
15 - type = 'warning'
17 + type = 'warning',
18 + extensionContext = null,
19 } = options;
20
21 const typeConfig = DIALOG_TYPES[type] || DIALOG_TYPES.warning;
@@ -40,14 +43,25 @@ export function showConfirmDialog(options) {
43 backdrop.appendChild(dialog);
44 document.body.appendChild(backdrop);
45
46 + const titleElement = dialog.querySelector('.confirm-dialog-title');
47 + const bodyElement = dialog.querySelector('.confirm-dialog-body');
48 + const footerElement = dialog.querySelector('.confirm-dialog-footer');
49 + const cancelButton = dialog.querySelector('.confirm-dialog-cancel');
50 + const confirmButton = dialog.querySelector('.confirm-dialog-confirm');
51 + let isClosed = false;
52 +
53 // Show with animation
44 - requestAnimationFrame(() => {
45 - backdrop.classList.add('visible');
46 - dialog.querySelector('.confirm-dialog-cancel').focus();
47 - });
54 + const showDialog = () => {
55 + requestAnimationFrame(() => {
56 + backdrop.classList.add('visible');
57 + cancelButton?.focus();
58 + });
59 + };
60
61 // Close handler
62 const close = (result) => {
63 + if (isClosed) return;
64 + isClosed = true;
65 backdrop.classList.remove('visible');
66 document.removeEventListener('keydown', handleKeydown);
67 setTimeout(() => {
@@ -57,15 +71,60 @@ export function showConfirmDialog(options) {
71 };
72
73 // Event listeners
60 - dialog.querySelector('.confirm-dialog-cancel').addEventListener('click', () => close(false));
61 - dialog.querySelector('.confirm-dialog-confirm').addEventListener('click', () => close(true));
74 + cancelButton.addEventListener('click', () => close(false));
75 + confirmButton.addEventListener('click', () => close(true));
76 backdrop.addEventListener('click', (e) => e.target === backdrop && close(false));
77
78 // Keyboard handling
79 const handleKeydown = (e) => {
66 - if (e.key === 'Escape') close(false);
67 - else if (e.key === 'Enter') close(true);
80 + if (e.key === 'Escape') {
81 + e.preventDefault();
82 + close(false);
83 + return;
84 + }
85 +
86 + if (e.key !== 'Enter') return;
87 +
88 + const activeElement = document.activeElement;
89 + if (
90 + activeElement
91 + && dialog.contains(activeElement)
92 + && (
93 + activeElement.tagName === 'BUTTON'
94 + || activeElement.tagName === 'A'
95 + || activeElement.tagName === 'INPUT'
96 + || activeElement.tagName === 'TEXTAREA'
97 + || activeElement.tagName === 'SELECT'
98 + || activeElement.isContentEditable
99 + )
100 + ) {
101 + return;
102 + }
103 +
104 + e.preventDefault();
105 + close(true);
106 };
107 document.addEventListener('keydown', handleKeydown);
108 +
109 + const extensionData = {
110 + options,
111 + dialog,
112 + backdrop,
113 + titleElement,
114 + bodyElement,
115 + footerElement,
116 + confirmButton,
117 + cancelButton,
118 + close,
119 + title,
120 + message,
121 + type,
122 + confirmText,
123 + cancelText,
124 + extensionContext,
125 + };
126 +
127 + callJsExtensions('confirm_dialog_after_render', extensionData)
128 + .finally(showDialog);
129 });
130 }