simplify plugin config modal opening
Move plugin config modal initialization into pluginSettingsStore.openConfig(), so callers only pass plugin name and optional scope. Resolve invalid project/agent scopes inside the store, initialize toggle state on the resolved scope, remove the old saveMode/core-save path, and delete stale saveMode guidance from docs and plugin skills.
Alessandro committed
Mar 19, 2026 at 13:53 UTC
5ed58fb10fdeb7ca8a2c3b45afc96d9d8e0d3c70
9 files changed
+118
-99
AGENTS.md
+1
-1
@@ -140,7 +140,7 @@ Key Files:
140
- Runtime hooks: Plugins may also expose hooks in hooks.py, callable by the framework through helpers.plugins.call_plugin_hook(...).
141
- Hook runtime: hooks.py executes inside the Agent Zero framework Python environment, so sys.executable -m pip installs dependencies into that same framework runtime.
142
- Environment targeting: If a plugin needs packages or binaries for the separate agent execution runtime or system environment, it must explicitly switch environments in a subprocess by targeting the correct interpreter, virtualenv, or package manager.
143
-- Settings: Use get_plugin_config(plugin_name, agent=agent) to retrieve settings. Plugins can expose a UI for settings via webui/config.html. Plugin settings modals instantiate a local context from $store.pluginSettingsPrototype; bind plugin fields to config.* and use context.* for modal-level state and actions. For plugins wrapping core settings, set context.saveMode = 'core' in x-init.
143
+- Settings: Use get_plugin_config(plugin_name, agent=agent) to retrieve settings. Plugins can expose a UI for settings via webui/config.html. Plugin settings modals instantiate a local context from $store.pluginSettingsPrototype; bind plugin fields to config.* and use context.* for modal-level state and actions.
144
- Activation: Global and scoped activation rules are stored as .toggle-1 (ON) and .toggle-0 (OFF). Scoped rules are handled via the plugin "Switch" modal.
145
- Cleanup rule: Plugins should not permanently modify the system in ways that outlive the plugin. Deleting a plugin should not leave behind symlinks, unmanaged services, or stray files outside plugin-owned paths unless the user explicitly requested that behavior.
146
plugins/_plugin_installer/webui/pluginInstallStore.js
+7
-1
@@ -7,6 +7,7 @@ import { showConfirmDialog } from "/js/confirmDialog.js";
7
import { store as imageViewerStore } from "/components/modals/image-viewer/image-viewer-store.js";
8
import { store as pluginListStore } from "/components/plugins/list/pluginListStore.js";
9
import { store as pluginExecuteStore } from "/components/plugins/list/plugin-execute-store.js";
10
+import { store as pluginSettingsStore } from "/components/plugins/plugin-settings-store.js";
11
12
const PLUGIN_API = "plugins/_plugin_installer/plugin_install";
13
const PER_PAGE = 20;
@@ -590,7 +591,12 @@ const model = {
591
592
async handleOpenConfig() {
593
if (this.installedPluginInfo) {
593
- await pluginListStore.openPluginConfig(this.installedPluginInfo);
594
+ try {
595
+ await pluginSettingsStore.openConfig(this.installedPluginInfo.name);
596
+ } catch (e) {
597
+ const message = e instanceof Error ? e.message : String(e);
598
+ void toastFrontendError(message, "Plugin Installer");
599
+ }
600
}
601
},
602
skills/a0-create-plugin/SKILL.md
-13
@@ -132,19 +132,6 @@ The modal provides Project + Agent profile context selectors. The plugin setting
132
133
The modal's Save button persists `config` to `config.json` in the correct scope (project/agent/global).
134
135
-### Surfacing core settings (e.g. memory pattern)
136
-
137
-If your plugin exposes existing core settings rather than plugin-specific ones, set `saveMode = 'core'` so Save delegates to the core settings API:
138
-
139
-```html
140
-<div x-data x-init="
141
- context.saveMode = 'core';
142
- if ($store.settings && !$store.settings.settings) $store.settings.onOpen();
143
-">
144
- <x-component path="settings/agent/memory.html"></x-component>
145
-</div>
146
-```
147
-
135
### Sidebar Button (sidebar entry point)
136
- Extension point: `sidebar-quick-actions-main-start`
137
- Class: `class="config-button"`
skills/a0-review-plugin/checklists.md
-7
@@ -188,13 +188,6 @@ save_plugin_config(
188
</html>
189
```
190
191
-Use `saveMode = 'core'` if exposing core settings instead of plugin-specific ones:
192
-```html
193
-<div x-data x-init="context.saveMode = 'core'">
194
- <!-- core settings component -->
195
-</div>
196
-```
197
-
191
---
192
193
## Sidebar Button (extension point)
webui/components/plugins/list/pluginListStore.js
+2
-13
@@ -79,21 +79,10 @@ const model = {
79
async openPluginConfig(plugin) {
80
if (!plugin?.name || !plugin?.has_config_screen) return;
81
try {
82
- // Initialize toggle store for activation state UI in settings modal
83
- if (pluginToggleStore?.open) await pluginToggleStore.open(plugin);
84
-
85
- if (!pluginSettingsStore?.open) {
82
+ if (!pluginSettingsStore?.openConfig) {
83
throw new Error("Plugin settings store is unavailable.");
84
}
88
- await pluginSettingsStore.open(plugin.name, {
89
- perProjectConfig: !!plugin.per_project_config,
90
- perAgentConfig: !!plugin.per_agent_config,
91
- });
92
- // Set saveMode after open() (open resets it to 'plugin')
93
- if (plugin.settings_sections?.includes('core')) {
94
- pluginSettingsStore.saveMode = 'core';
95
- }
96
- window.openModal?.("components/plugins/plugin-settings.html");
85
+ await pluginSettingsStore.openConfig(plugin.name);
86
} catch (e) {
87
showErrorNotification(e, "Failed to open plugin config");
88
}
webui/components/plugins/plugin-settings-store.js
+86
-46
@@ -1,6 +1,6 @@
1
import { createStore } from "/js/AlpineStore.js";
2
+import * as api from "/js/api.js";
3
import { showConfirmDialog } from "/js/confirmDialog.js";
3
-import { store as settingsStore } from "/components/settings/settings-store.js";
4
import { store as pluginToggleStore } from "/components/plugins/toggle/plugin-toggle-store.js";
5
6
const fetchApi = globalThis.fetchApi;
@@ -41,6 +41,64 @@ const model = {
41
return window.confirm("You have unsaved changes that will be lost. Continue?");
42
},
43
44
+ async _loadPluginMeta(pluginName) {
45
+ const response = await api.callJsonApi("plugins_list", {
46
+ filter: { custom: true, builtin: true, search: "" },
47
+ });
48
+ const plugins = Array.isArray(response?.plugins) ? response.plugins : [];
49
+ return plugins.find((plugin) => plugin?.name === pluginName) || null;
50
+ },
51
+
52
+ _hasProject(projectName) {
53
+ if (!projectName) return false;
54
+ return (this.projects || []).some((project) => project?.key === projectName);
55
+ },
56
+
57
+ _hasAgentProfile(agentProfileKey) {
58
+ if (!agentProfileKey) return false;
59
+ return (this.agentProfiles || []).some((profile) => profile?.key === agentProfileKey);
60
+ },
61
+
62
+ _resolveScope(pluginMeta, projectName = "", agentProfileKey = "") {
63
+ const resolvedProjectName =
64
+ pluginMeta?.per_project_config && this._hasProject(projectName)
65
+ ? projectName
66
+ : "";
67
+ const resolvedAgentProfileKey =
68
+ pluginMeta?.per_agent_config && this._hasAgentProfile(agentProfileKey)
69
+ ? agentProfileKey
70
+ : "";
71
+
72
+ return {
73
+ projectName: resolvedProjectName,
74
+ agentProfileKey: resolvedAgentProfileKey,
75
+ };
76
+ },
77
+
78
+ _applyPluginState(pluginMeta, { projectName = "", agentProfileKey = "" } = {}) {
79
+ this.pluginName = pluginMeta?.name || null;
80
+ this.pluginMeta = pluginMeta || null;
81
+ this.settings = {};
82
+ this.settingsSnapshotJson = "";
83
+ this.error = null;
84
+ this.projectName = projectName;
85
+ this.agentProfileKey = agentProfileKey;
86
+ this.previousProjectName = projectName;
87
+ this.previousAgentProfileKey = agentProfileKey;
88
+ this.loadedPath = "";
89
+ this.loadedProjectName = "";
90
+ this.loadedAgentProfile = "";
91
+ this.perProjectConfig = !!pluginMeta?.per_project_config;
92
+ this.perAgentConfig = !!pluginMeta?.per_agent_config;
93
+ },
94
+
95
+ async _syncToggleScope(projectName = "", agentProfileKey = "") {
96
+ if (!pluginToggleStore?.loadToggleStatus) return;
97
+ pluginToggleStore.projectName = projectName;
98
+ pluginToggleStore.agentProfileKey = agentProfileKey;
99
+ await pluginToggleStore.loadToggleStatus();
100
+ },
101
+
102
async onScopeChanged() {
103
const nextProject = this.projectName || "";
104
const nextProfile = this.agentProfileKey || "";
@@ -56,13 +114,7 @@ const model = {
114
}
115
116
await this.loadSettings();
59
-
60
- // Mirror scope change to pluginToggle so activation state stays in sync
61
- if (pluginToggleStore?.loadToggleStatus) {
62
- pluginToggleStore.projectName = nextProject;
63
- pluginToggleStore.agentProfileKey = nextProfile;
64
- await pluginToggleStore.loadToggleStatus();
65
- }
117
+ await this._syncToggleScope(nextProject, nextProfile);
118
},
119
120
// where the settings were actually loaded from
@@ -131,6 +183,7 @@ const model = {
183
this.projectName = projectName || "";
184
this.agentProfileKey = agentProfile || "";
185
await this.loadSettings();
186
+ await this._syncToggleScope(this.projectName, this.agentProfileKey);
187
await window.closeModal?.();
188
},
189
@@ -168,10 +221,6 @@ const model = {
221
}
222
},
223
171
- // 'plugin' = save to plugin settings API
172
- // 'core' = save via $store.settings.saveSettings() (for plugins that surface core settings)
173
- saveMode: 'plugin',
174
-
224
perProjectConfig: true,
225
perAgentConfig: true,
226
@@ -179,32 +228,30 @@ const model = {
228
isSaving: false,
229
error: null,
230
182
- // Called by the subsection button before openModal()
183
- // Optional scope: { projectName, agentProfileKey } — skips redundant global loadSettings()
184
- // when the caller already knows which scope to open at.
185
- async open(pluginName, { projectName = "", agentProfileKey = "", perProjectConfig = true, perAgentConfig = true } = {}) {
186
- this.pluginName = pluginName;
187
- this.pluginMeta = null;
188
- this.settings = {};
189
- this.settingsSnapshotJson = "";
190
- this.error = null;
191
- this.saveMode = 'plugin';
192
- this.perProjectConfig = perProjectConfig;
193
- this.perAgentConfig = perAgentConfig;
194
- this.projectName = projectName;
195
- this.agentProfileKey = agentProfileKey;
196
- this.previousProjectName = projectName;
197
- this.previousAgentProfileKey = agentProfileKey;
198
- this.loadedPath = "";
199
- this.loadedProjectName = "";
200
- this.loadedAgentProfile = "";
231
+ async openConfig(pluginName, projectName = "", agentProfile = "") {
232
+ if (!pluginName) {
233
+ throw new Error("Missing plugin name.");
234
+ }
235
+
236
+ this.cleanup();
237
+ const pluginMeta = await this._loadPluginMeta(pluginName);
238
+ if (!pluginMeta) {
239
+ throw new Error(`Plugin "${pluginName}" not found.`);
240
+ }
241
+ if (!pluginMeta.has_config_screen) {
242
+ throw new Error(`Plugin "${pluginName}" has no config screen.`);
243
+ }
244
+
245
await Promise.all([this.loadProjects(), this.loadAgentProfiles()]);
246
+ const resolvedScope = this._resolveScope(pluginMeta, projectName || "", agentProfile || "");
247
+ this._applyPluginState(pluginMeta, resolvedScope);
248
await this.loadSettings();
203
- },
249
205
- // Called by x-create inside the modal on every open
206
- async onModalOpen() {
207
- if (this.pluginName) await this.loadSettings();
250
+ if (!pluginToggleStore?.open) {
251
+ throw new Error("Plugin toggle store is unavailable.");
252
+ }
253
+ await pluginToggleStore.open(pluginMeta, resolvedScope);
254
+ await window.openModal?.("/components/plugins/plugin-settings.html");
255
},
256
257
async loadAgentProfiles() {
@@ -290,17 +337,6 @@ const model = {
337
338
async save() {
339
if (!this.pluginName) return;
293
-
294
- // Core-backed plugins (e.g. memory) delegate to the settings store
295
- if (this.saveMode === 'core') {
296
- if (settingsStore?.saveSettings) {
297
- const ok = await settingsStore.saveSettings();
298
- if (ok) window.closeModal?.();
299
- }
300
- return;
301
- }
302
-
303
- // Plugin-specific settings: persist to plugin settings API
340
this.isSaving = true;
341
this.error = null;
342
try {
@@ -331,6 +367,10 @@ const model = {
367
cleanup() {
368
this.pluginName = null;
369
this.pluginMeta = null;
370
+ this.projects = [];
371
+ this.agentProfiles = [];
372
+ this.projectName = "";
373
+ this.agentProfileKey = "";
374
this.settings = {};
375
this.settingsSnapshotJson = "";
376
this.previousProjectName = "";
webui/components/plugins/plugin-settings.html
+1
-1
@@ -17,7 +17,7 @@
17
})()">
18
<template x-if="context">
19
<div>
20
- <div x-create="context.onModalOpen(); (() => { const modal = $el.closest('.modal'); if (modal) modal.__pluginSettingsContext = context; })()"
20
+ <div x-create="(() => { const modal = $el.closest('.modal'); if (modal) modal.__pluginSettingsContext = context; })()"
21
x-destroy="context.cleanup()">
22
23
<!-- Context toolbar: Project + Agent profile (only when at least one scope is configurable) -->
webui/components/plugins/toggle/plugin-toggle-store.js
+12
-15
@@ -32,13 +32,13 @@ const model = {
32
33
configs: [],
34
35
- async open(plugin) {
35
+ async open(plugin, { projectName = "", agentProfileKey = "" } = {}) {
36
this.isLoading = true;
37
this.error = null;
38
this.projects = [];
39
this.agentProfiles = [];
40
- this.projectName = "";
41
- this.agentProfileKey = "";
40
+ this.projectName = projectName || "";
41
+ this.agentProfileKey = agentProfileKey || "";
42
this.configs = [];
43
this.status = 'enabled';
44
this.hasExplicitRuleForScope = false;
@@ -172,19 +172,16 @@ const model = {
172
173
async openConfigWithScope() {
174
if (!this.pluginName) return;
175
-
176
- if (settingsStore.pluginName !== this.pluginName) {
177
- // Different plugin — full init with current scope
178
- await settingsStore.open(this.pluginName, {
179
- projectName: this.projectName || "",
180
- agentProfileKey: this.agentProfileKey || "",
181
- });
182
- } else {
183
- // Same plugin — push current scope explicitly.
184
- settingsStore.projectName = this.projectName || "";
185
- settingsStore.agentProfileKey = this.agentProfileKey || "";
175
+ this.error = null;
176
+ try {
177
+ await settingsStore.openConfig(
178
+ this.pluginName,
179
+ this.projectName || "",
180
+ this.agentProfileKey || ""
181
+ );
182
+ } catch (e) {
183
+ this.error = e?.message || "Failed to open plugin config";
184
}
187
- await window.openModal?.("/components/plugins/plugin-settings.html");
185
},
186
187
async openConfigListModal() {
webui/components/settings/plugins/plugins-subsection-store.js
+9
-2
@@ -1,6 +1,7 @@
1
import { createStore } from "/js/AlpineStore.js";
2
import * as api from "/js/api.js";
3
-import { store as pluginListStore } from "/components/plugins/list/pluginListStore.js";
3
+import { toastFrontendError } from "/components/notifications/notification-store.js";
4
+import { store as pluginSettingsStore } from "/components/plugins/plugin-settings-store.js";
5
6
const model = {
7
tab: "",
@@ -51,7 +52,13 @@ const model = {
52
},
53
54
async openPluginConfig(plugin) {
54
- await pluginListStore.openPluginConfig(plugin);
55
+ if (!plugin?.name) return;
56
+ try {
57
+ await pluginSettingsStore.openConfig(plugin.name);
58
+ } catch (e) {
59
+ const message = e instanceof Error ? e.message : String(e);
60
+ void toastFrontendError(message, "Plugin Settings");
61
+ }
62
},
63
};
64