toggling inheritance (match configs)
These changes improve the plugin's ability to manage toggle states across different scopes the same way we handle plugin configs.
Alessandro committed
Feb 26, 2026 at 10:51 UTC
601a5277ce7221e5c03d2345919bad2e21bff2ec
2 files changed
+115
-31
python/api/plugins.py
+48
@@ -51,6 +51,54 @@ class Plugins(ApiHandler):
51
"data": settings,
52
}
53
54
+ if action == "get_toggle_status":
55
+ plugin_name = input.get("plugin_name", "")
56
+ project_name = input.get("project_name", "")
57
+ agent_profile = input.get("agent_profile", "")
58
+ if not plugin_name:
59
+ return Response(status=400, response="Missing plugin_name")
60
+
61
+ meta = plugins.get_plugin_meta(plugin_name)
62
+ if not meta:
63
+ return Response(status=404, response="Plugin not found")
64
+
65
+ if meta.always_enabled:
66
+ return {
67
+ "ok": True,
68
+ "status": "enabled",
69
+ "loaded_project_name": project_name,
70
+ "loaded_agent_profile": agent_profile,
71
+ "loaded_path": "",
72
+ }
73
+
74
+ result = plugins.find_plugin_assets(
75
+ plugins.TOGGLE_FILE_PATTERN,
76
+ plugin_name=plugin_name,
77
+ project_name=project_name,
78
+ agent_profile=agent_profile,
79
+ only_first=True,
80
+ )
81
+
82
+ if result:
83
+ entry = result[0]
84
+ path = entry.get("path", "")
85
+ status = "enabled" if path.endswith(plugins.ENABLED_FILE_NAME) else "disabled"
86
+ return {
87
+ "ok": True,
88
+ "status": status,
89
+ "loaded_project_name": entry.get("project_name", ""),
90
+ "loaded_agent_profile": entry.get("agent_profile", ""),
91
+ "loaded_path": path,
92
+ }
93
+
94
+ return {
95
+ "ok": True,
96
+ "status": "enabled",
97
+ "loaded_project_name": "",
98
+ "loaded_agent_profile": "",
99
+ "loaded_path": "",
100
+ }
101
+
102
if action == "list_configs":
103
plugin_name = input.get("plugin_name", "")
104
asset_type = input.get("asset_type", "config")
webui/components/plugins/toggle/plugin-toggle-store.js
+67
-31
@@ -22,8 +22,13 @@ const model = {
22
alwaysEnabled: false,
23
perProjectConfig: true,
24
perAgentConfig: true,
25
- explicitPath: null,
25
hasExplicitRuleForScope: false,
26
+
27
+ // Where the effective toggle was actually resolved (mirrors plugin-settings-store loadedXxx fields)
28
+ loadedPath: "",
29
+ loadedProjectName: "",
30
+ loadedAgentProfile: "",
31
+
32
configs: [],
33
34
async open(plugin) {
@@ -34,6 +39,9 @@ const model = {
39
this.projectName = "";
40
this.agentProfileKey = "";
41
this.configs = [];
42
+ this.loadedPath = "";
43
+ this.loadedProjectName = "";
44
+ this.loadedAgentProfile = "";
45
46
const pluginName = typeof plugin === 'string' ? plugin : plugin?.name;
47
this.pluginName = pluginName;
@@ -61,6 +69,9 @@ const model = {
69
this.alwaysEnabled = false;
70
this.hasConfigScreen = false;
71
this.hasExplicitRuleForScope = false;
72
+ this.loadedPath = "";
73
+ this.loadedProjectName = "";
74
+ this.loadedAgentProfile = "";
75
},
76
77
async loadProjects() {
@@ -106,7 +117,7 @@ const model = {
117
});
118
const result = await response.json().catch(() => ({}));
119
this.configs = result.ok ? (result.data || []) : [];
109
- this.calculateStatus();
120
+ await this.loadToggleStatus();
121
} catch (e) {
122
this.error = e?.message || "Failed to load configurations";
123
} finally {
@@ -114,6 +125,46 @@ const model = {
125
}
126
},
127
128
+ async loadToggleStatus() {
129
+ if (!this.pluginName) return;
130
+
131
+ if (this.alwaysEnabled) {
132
+ this.status = 'enabled';
133
+ this.hasExplicitRuleForScope = true;
134
+ this.loadedPath = "";
135
+ this.loadedProjectName = this.projectName || "";
136
+ this.loadedAgentProfile = this.agentProfileKey || "";
137
+ return;
138
+ }
139
+
140
+ try {
141
+ const response = await fetchApi("/plugins", {
142
+ method: "POST",
143
+ headers: { "Content-Type": "application/json" },
144
+ body: JSON.stringify({
145
+ action: "get_toggle_status",
146
+ plugin_name: this.pluginName,
147
+ project_name: this.projectName || "",
148
+ agent_profile: this.agentProfileKey || "",
149
+ }),
150
+ });
151
+ const result = await response.json().catch(() => ({}));
152
+ if (result.ok) {
153
+ this.status = result.status || 'enabled';
154
+ this.loadedPath = result.loaded_path || "";
155
+ this.loadedProjectName = result.loaded_project_name || "";
156
+ this.loadedAgentProfile = result.loaded_agent_profile || "";
157
+ const p = this.projectName || "";
158
+ const a = this.agentProfileKey || "";
159
+ this.hasExplicitRuleForScope = !!(this.loadedPath) &&
160
+ this.loadedProjectName === p &&
161
+ this.loadedAgentProfile === a;
162
+ }
163
+ } catch (e) {
164
+ this.error = e?.message || "Failed to load toggle status";
165
+ }
166
+ },
167
+
168
async openConfigWithScope() {
169
if (!this.pluginName) return;
170
@@ -172,31 +223,6 @@ const model = {
223
}
224
},
225
175
- calculateStatus() {
176
- if (this.alwaysEnabled) {
177
- this.explicitPath = null;
178
- this.hasExplicitRuleForScope = true;
179
- this.status = 'enabled';
180
- return;
181
- }
182
-
183
- const p = this.projectName || "";
184
- const a = this.agentProfileKey || "";
185
- const explicit = this.configs.find(
186
- c => (c.project_name||"") === p && (c.agent_profile||"") === a
187
- );
188
-
189
- if (explicit) {
190
- this.explicitPath = explicit.path;
191
- this.hasExplicitRuleForScope = true;
192
- this.status = explicit.path.endsWith(".toggle-1") ? 'enabled' : 'disabled';
193
- } else {
194
- this.explicitPath = null;
195
- this.hasExplicitRuleForScope = false;
196
- this.status = 'enabled'; // default when no explicit config
197
- }
198
- },
199
-
226
async setEnabled(enabled) {
227
if (!this.pluginName || this.alwaysEnabled) return;
228
this.isSaving = true;
@@ -224,7 +250,7 @@ const model = {
250
},
251
252
async onScopeChanged() {
227
- this.calculateStatus();
253
+ await this.loadToggleStatus();
254
255
// Sync scope with settings store so its loadSettings picks up the right context
256
settingsStore.projectName = this.projectName || "";
@@ -254,9 +280,19 @@ const model = {
280
281
get noScopeRuleMessage() {
282
if (this.alwaysEnabled || this.isLoading) return "";
257
- if (this.configs.length === 0) return "No activation rules configured. Plugin defaults to ON.";
258
- if (!this.explicitPath) return "No rule set for this scope. Status defaults to ON.";
259
- return "";
283
+ if (this.hasExplicitRuleForScope) return "";
284
+
285
+ if (!this.loadedPath) {
286
+ return this.configs.length === 0
287
+ ? "No activation rules configured. Plugin defaults to ON."
288
+ : "No rule for this scope. Defaults to ON.";
289
+ }
290
+
291
+ // Inherited from a parent scope - mirrors plugin-settings-store scopeMismatchMessage
292
+ const pLabel = this.projectLabel(this.loadedProjectName || "");
293
+ const aLabel = this.agentProfileLabel(this.loadedAgentProfile || "");
294
+ const state = this.status === 'enabled' ? 'ON' : 'OFF';
295
+ return `No rule for this scope. Inheriting from ${pLabel}, ${aLabel}: ${state}.`;
296
}
297
};
298