Simplify plugin activation toggle UI

Replace the plugin list activation dropdown and advanced shortcut with a one-click ON/OFF switch. Keep project/profile-specific activation inside the plugin config flow, remove the old advanced-only modal, update plugin docs, and add regression coverage for the binary list toggle contract.

Alessandro committed May 21, 2026 at 04:31 UTC d4a9cd82d5afef76a5522e0bc61bbf581bf2c033
9 files changed +194 -370
docs/agents/AGENTS.plugins.md
+2 -2
@@ -228,9 +228,9 @@ embedding:
228
229 - Global and scoped activation are independent, with no inheritance between scopes.
230 - Activation flags are files: `.toggle-1` (ON) and `.toggle-0` (OFF).
231 -- UI states are `ON`, `OFF`, and `Advanced` (shown when any project/profile-specific override exists).
231 +- The plugin list shows a binary `ON`/`OFF` global activation switch.
232 - `always_enabled: true` in `plugin.yaml` forces ON and disables toggle controls in the UI.
233 -- The "Switch" modal is the canonical per-scope activation surface, and "Configure Plugin" keeps scope synchronized with the settings modal.
233 +- For plugins with project/profile scoping, the plugin config modal is the canonical per-scope activation surface.
234
235 ---
236
helpers/plugins.py
+4 -19
@@ -44,7 +44,7 @@ _META_TARGET_RE = re.compile(
44 )
45
46
47 -type ToggleState = Literal["enabled", "disabled", "advanced"]
47 +type ToggleState = Literal["enabled", "disabled"]
48
49
50 class PluginAssetFile(TypedDict):
@@ -517,30 +517,15 @@ def get_toggle_state(plugin_name: str) -> ToggleState:
517 if meta.always_enabled:
518 return "enabled"
519
520 - # root plugin paths
520 + # List-level activation is the global/root state. Scoped project/profile
521 + # overrides are managed inside the plugin config modal.
522 plugin_paths = get_plugin_roots(plugin_name)
522 - state = (
523 + return (
524 "enabled"
525 if determined_toggle_from_paths(True, reversed(plugin_paths))
526 else "disabled"
527 )
528
528 - # additional toggles in project/agent directories, return advanced
529 - if meta.per_agent_config or meta.per_project_config:
530 - configs = find_plugin_assets(
531 - TOGGLE_FILE_PATTERN,
532 - plugin_name=plugin_name,
533 - project_name="*" if meta.per_project_config else "",
534 - agent_profile="*" if meta.per_agent_config else "",
535 - only_first=False,
536 - )
537 -
538 - # Advanced if there are specific overrides (project or agent specific)
539 - if any(c.get("project_name") or c.get("agent_profile") for c in configs):
540 - state = "advanced"
541 -
542 - return state
543 -
529
530 @extension.extensible
531 def toggle_plugin(
tests/test_plugin_activation_ui.py new
+108
@@ -0,0 +1,108 @@
1 +import sys
2 +from pathlib import Path
3 +from types import SimpleNamespace
4 +
5 +
6 +PROJECT_ROOT = Path(__file__).resolve().parents[1]
7 +if str(PROJECT_ROOT) not in sys.path:
8 + sys.path.insert(0, str(PROJECT_ROOT))
9 +
10 +from helpers import files, plugins
11 +
12 +
13 +def test_plugins_list_uses_binary_toggle_instead_of_advanced_dropdown():
14 + html = (PROJECT_ROOT / "webui/components/plugins/list/plugin-list.html").read_text(
15 + encoding="utf-8"
16 + )
17 + store = (
18 + PROJECT_ROOT / "webui/components/plugins/list/pluginListStore.js"
19 + ).read_text(encoding="utf-8")
20 +
21 + assert "plugin-status-toggle" in html
22 + assert "plugin-status-select" not in html
23 + assert "Open Advanced" not in html
24 + assert 'value="advanced"' not in html
25 + assert "openPluginAdvancedToggle" not in store
26 + assert "plugin-toggle-advanced.html" not in store
27 +
28 +
29 +def test_advanced_plugin_toggle_modal_was_removed():
30 + assert not (
31 + PROJECT_ROOT / "webui/components/plugins/toggle/plugin-toggle-advanced.html"
32 + ).exists()
33 +
34 +
35 +def test_list_toggle_state_is_global_even_when_scoped_rules_exist(monkeypatch):
36 + monkeypatch.setattr(
37 + plugins,
38 + "get_plugin_meta",
39 + lambda _plugin_name: SimpleNamespace(always_enabled=False),
40 + )
41 + monkeypatch.setattr(
42 + plugins,
43 + "get_plugin_roots",
44 + lambda _plugin_name: ["plugins/example", "usr/plugins/example"],
45 + )
46 + monkeypatch.setattr(
47 + plugins,
48 + "determined_toggle_from_paths",
49 + lambda _default, _paths: False,
50 + )
51 +
52 + def fail_on_scoped_lookup(*_args, **_kwargs):
53 + raise AssertionError("Plugin list toggle state should not inspect scoped rules")
54 +
55 + monkeypatch.setattr(plugins, "find_plugin_assets", fail_on_scoped_lookup)
56 +
57 + assert plugins.get_toggle_state("example") == "disabled"
58 +
59 +
60 +def test_config_scope_activation_toggle_saves_immediately_for_selected_scope():
61 + html = (PROJECT_ROOT / "webui/components/plugins/plugin-settings.html").read_text(
62 + encoding="utf-8"
63 + )
64 + settings_store = (
65 + PROJECT_ROOT / "webui/components/plugins/plugin-settings-store.js"
66 + ).read_text(encoding="utf-8")
67 + toggle_store = (
68 + PROJECT_ROOT / "webui/components/plugins/toggle/plugin-toggle-store.js"
69 + ).read_text(encoding="utf-8")
70 +
71 + assert "@change=\"context.setPluginEnabled($event.target.checked)\"" in html
72 + assert "projectName: this.projectName || \"\"" in settings_store
73 + assert "agentProfileKey: this.agentProfileKey || \"\"" in settings_store
74 + assert (
75 + "async setEnabled(enabled, { projectName = this.projectName, "
76 + "agentProfileKey = this.agentProfileKey } = {})"
77 + ) in toggle_store
78 + assert "action: \"toggle_plugin\"" in toggle_store
79 +
80 +
81 +def test_toggle_plugin_writes_project_scope_file_immediately(tmp_path, monkeypatch):
82 + monkeypatch.setattr(files, "_base_dir", str(tmp_path))
83 + monkeypatch.setattr(plugins, "after_plugin_change", lambda *_args, **_kwargs: None)
84 + monkeypatch.setitem(
85 + sys.modules,
86 + "helpers.projects",
87 + SimpleNamespace(
88 + get_project_meta=lambda name, *sub_dirs: files.get_abs_path(
89 + "usr/projects",
90 + name,
91 + ".a0proj",
92 + *sub_dirs,
93 + )
94 + ),
95 + )
96 +
97 + plugins.toggle_plugin.__wrapped__("example", False, project_name="alpha")
98 +
99 + scoped_plugin_dir = (
100 + tmp_path / "usr/projects/alpha/.a0proj/plugins/example"
101 + )
102 + assert (scoped_plugin_dir / ".toggle-0").exists()
103 + assert not (scoped_plugin_dir / ".toggle-1").exists()
104 +
105 + plugins.toggle_plugin.__wrapped__("example", True, project_name="alpha")
106 +
107 + assert (scoped_plugin_dir / ".toggle-1").exists()
108 + assert not (scoped_plugin_dir / ".toggle-0").exists()
webui/components/plugins/list/plugin-list.html
+39 -41
@@ -180,24 +180,17 @@
180 <div class="plugin-footer-row">
181 <div class="plugin-description" x-text="plugin.description || 'No description provided.'"></div>
182 <div class="plugin-status-group">
183 - <template x-if="plugin.toggle_state === 'advanced'">
184 - <button type="button"
185 - class="button icon-button"
186 - title="Open Advanced"
187 - @click="$store.pluginListStore.openPluginAdvancedToggle(plugin)">
188 - <span class="icon material-symbols-outlined">rule_settings</span>
189 - </button>
190 - </template>
191 - <select class="plugin-status-select"
192 - @change="$store.pluginListStore.updateToggle(plugin, $event.target.value)"
193 - @click.stop
194 - :disabled="plugin.always_enabled">
195 - <option value="enabled" :selected="plugin.toggle_state === 'enabled'">ON</option>
196 - <option value="disabled" :selected="plugin.toggle_state === 'disabled'">OFF</option>
197 - <template x-if="plugin.per_project_config || plugin.per_agent_config">
198 - <option value="advanced" :selected="plugin.toggle_state === 'advanced'">Advanced</option>
199 - </template>
200 - </select>
183 + <label class="toggle plugin-status-toggle"
184 + :class="{ 'disabled-appearance': plugin.always_enabled || $store.pluginListStore.loading }"
185 + :title="plugin.always_enabled ? 'Always enabled' : ($store.pluginListStore.isPluginEnabled(plugin) ? 'Disable plugin' : 'Enable plugin')">
186 + <input type="checkbox"
187 + :checked="$store.pluginListStore.isPluginEnabled(plugin)"
188 + :disabled="plugin.always_enabled || $store.pluginListStore.loading"
189 + @change="$store.pluginListStore.updateToggle(plugin, $event.target.checked)"
190 + @click.stop>
191 + <span class="toggler"></span>
192 + </label>
193 + <span class="plugin-status-text" x-text="$store.pluginListStore.toggleStatusLabel(plugin)"></span>
194 </div>
195 </div>
196 </div>
@@ -389,35 +382,40 @@
382 flex: 0 0 auto;
383 }
384
392 - .plugin-status-group .button {
393 - padding: 0.3rem 0.5rem;
394 - height: 2.2rem;
395 - display: flex;
396 - align-items: center;
397 - justify-content: center;
385 + .plugin-status-toggle {
386 + width: 48px;
387 + height: 28px;
388 + flex: 0 0 48px;
389 }
390
400 - .plugin-status-group .button .icon {
401 - font-size: 1.1rem;
391 + .plugin-status-toggle .toggler {
392 + border-radius: 999px;
393 }
394
404 - .plugin-status-select {
405 - padding: 0.3rem 2rem 0.3rem 0.6rem;
406 - font-size: 0.9rem;
407 - border: 1px solid var(--color-border);
408 - border-radius: 4px;
409 - background: var(--color-background);
410 - color: var(--color-text-primary);
411 - cursor: pointer;
412 - height: 2.2rem;
413 - width: 8rem;
414 - flex: 0 0 8rem;
395 + .plugin-status-toggle .toggler:before {
396 + width: 20px;
397 + height: 20px;
398 + left: 4px;
399 + bottom: 4px;
400 + }
401 +
402 + .plugin-status-toggle input:checked + .toggler:before {
403 + transform: translateX(20px);
404 }
416 -
417 - .plugin-status-select:disabled {
418 - opacity: 0.7;
405 +
406 + .plugin-status-toggle input:disabled + .toggler {
407 cursor: default;
420 - background: var(--color-bg-secondary);
408 + }
409 +
410 + .plugin-status-toggle.disabled-appearance {
411 + opacity: 0.6;
412 + }
413 +
414 + .plugin-status-text {
415 + color: var(--color-text-primary);
416 + font-size: 0.9rem;
417 + font-weight: 600;
418 + min-width: 2rem;
419 }
420
421 .plugin-description {
webui/components/plugins/list/pluginListStore.js
+15 -26
@@ -2,7 +2,6 @@ import { createStore } from "/js/AlpineStore.js";
2 import * as api from "/js/api.js";
3 import { renderSafeMarkdown } from "/js/safe-markdown.js";
4 import { store as pluginSettingsStore } from "/components/plugins/plugin-settings-store.js";
5 -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";
@@ -114,47 +113,37 @@ const model = {
113 }
114 },
115
117 - async openPluginAdvancedToggle(plugin) {
118 - if (!plugin?.name) return;
119 - this.selectedPlugin = plugin;
120 - try {
121 - if (!pluginToggleStore?.open) {
122 - throw new Error("Plugin toggle store is unavailable.");
123 - }
124 - await pluginToggleStore.open(plugin);
125 - window.openModal?.("components/plugins/toggle/plugin-toggle-advanced.html");
126 - } catch (e) {
127 - showErrorNotification(e, "Failed to open plugin switch");
128 - }
116 + isPluginEnabled(plugin) {
117 + if (plugin?.always_enabled) return true;
118 + return plugin?.toggle_state === "enabled";
119 + },
120 +
121 + toggleStatusLabel(plugin) {
122 + return this.isPluginEnabled(plugin) ? "ON" : "OFF";
123 },
124
131 - async updateToggle(plugin, value) {
125 + async updateToggle(plugin, enabled) {
126 if (!plugin?.name) return;
133 -
134 - if (value === 'advanced') {
135 - await this.openPluginAdvancedToggle(plugin);
136 - return;
137 - }
127 + if (plugin.always_enabled) return;
128
139 - const enabled = value === 'enabled';
140 - const clearOverrides = plugin.toggle_state === 'advanced';
141 - if (clearOverrides && !window.confirm(
142 - `"${plugin.display_name || plugin.name}" has per-scope activation rules that will be removed. Set globally to ${enabled ? 'ON' : 'OFF'}?`
143 - )) return;
129 + const nextEnabled = !!enabled;
130 + const previousState = plugin.toggle_state;
131 + plugin.toggle_state = nextEnabled ? "enabled" : "disabled";
132
133 this.loading = true;
134 try {
135 const response = await api.callJsonApi("plugins", {
136 action: "toggle_plugin",
137 plugin_name: plugin.name,
150 - enabled: enabled,
138 + enabled: nextEnabled,
139 project_name: "",
140 agent_profile: "",
153 - clear_overrides: clearOverrides,
141 + clear_overrides: false,
142 });
143 if (response?.error) throw new Error(response.error);
144 await this.refresh();
145 } catch (e) {
146 + plugin.toggle_state = previousState;
147 showErrorNotification(e, "Failed to toggle plugin");
148 this.loading = false;
149 }
webui/components/plugins/plugin-settings-store.js
+13
@@ -114,6 +114,19 @@ const model = {
114 await pluginToggleStore.loadToggleStatus();
115 },
116
117 + async setPluginEnabled(enabled) {
118 + if (!pluginToggleStore?.setEnabled) return;
119 + this.error = null;
120 + try {
121 + await pluginToggleStore.setEnabled(enabled, {
122 + projectName: this.projectName || "",
123 + agentProfileKey: this.agentProfileKey || "",
124 + });
125 + } catch (e) {
126 + this.error = e?.message || "Failed to save activation state";
127 + }
128 + },
129 +
130 async onScopeChanged() {
131 const nextProject = this.projectName || "";
132 const nextProfile = this.agentProfileKey || "";
webui/components/plugins/plugin-settings.html
+2 -2
@@ -49,8 +49,8 @@
49 <label class="toggle">
50 <input type="checkbox"
51 :checked="$store.pluginToggle?.status === 'enabled'"
52 - :disabled="$store.pluginToggle?.isSaving"
53 - @change="$store.pluginToggle.setEnabled($event.target.checked)">
52 + :disabled="$store.pluginToggle?.isSaving || context.isLoading"
53 + @change="context.setPluginEnabled($event.target.checked)">
54 <span class="toggler"></span>
55 </label>
56 <span class="plugin-toggle-status-text" x-text="$store.pluginToggle?.statusLabel"></span>
webui/components/plugins/toggle/plugin-toggle-advanced.html deleted
-261
@@ -1,261 +0,0 @@
1 -<html>
2 -<head>
3 - <title>Plugin Switch</title>
4 - <script type="module">
5 - import { store } from "/components/plugins/toggle/plugin-toggle-store.js";
6 - </script>
7 -</head>
8 -<body>
9 - <div x-data>
10 - <template x-if="$store.pluginToggle">
11 - <div x-create="$store.pluginToggle.open($store.pluginListStore?.selectedPlugin || '')">
12 -
13 - <!-- Error -->
14 - <div x-show="$store.pluginToggle.error" class="plugin-settings-error">
15 - <span class="material-symbols-outlined">error</span>
16 - <span x-text="$store.pluginToggle.error"></span>
17 - </div>
18 -
19 - <!-- Loading -->
20 - <div x-show="$store.pluginToggle.isLoading" class="plugin-settings-loading">
21 - <span class="material-symbols-outlined spinning">progress_activity</span>
22 - <span>Loading status...</span>
23 - </div>
24 -
25 - <!-- Main UI -->
26 - <div x-show="!$store.pluginToggle.isLoading">
27 -
28 - <!-- Scope selector + per-scope ON/OFF -->
29 - <div class="plugin-settings-scope-section">
30 - <div class="plugin-settings-scope-header">
31 - <div class="plugin-settings-scope-header-copy">
32 - <div class="plugin-settings-scope-title">Scope</div>
33 - <div class="plugin-settings-scope-desc">Select which project or agent profile to configure.</div>
34 - </div>
35 - <div class="plugin-settings-scope-header-toggle">
36 - <span class="plugin-settings-toolbar-label">Enabled</span>
37 - <label class="toggle" :class="{ 'disabled-appearance': $store.pluginToggle.alwaysEnabled || $store.pluginToggle.isSaving }">
38 - <input type="checkbox"
39 - :checked="$store.pluginToggle.status === 'enabled'"
40 - :disabled="$store.pluginToggle.alwaysEnabled || $store.pluginToggle.isSaving"
41 - @change="$store.pluginToggle.setEnabled($event.target.checked)">
42 - <span class="toggler"></span>
43 - </label>
44 - <span class="plugin-toggle-status-text" x-text="$store.pluginToggle.statusLabel"></span>
45 - </div>
46 - </div>
47 - <div class="plugin-settings-toolbar">
48 - <div class="plugin-settings-toolbar-row">
49 -
50 - <label class="plugin-settings-toolbar-item">
51 - <span class="plugin-settings-toolbar-label">Project</span>
52 - <select x-model="$store.pluginToggle.projectName"
53 - @change="$store.pluginToggle.onScopeChanged()"
54 - :disabled="!$store.pluginToggle.perProjectConfig">
55 - <option value="">Global</option>
56 - <template x-for="p in $store.pluginToggle.projects" :key="p.key">
57 - <option :value="p.key" x-text="p.label"></option>
58 - </template>
59 - </select>
60 - </label>
61 -
62 - <label class="plugin-settings-toolbar-item">
63 - <span class="plugin-settings-toolbar-label">Agent profile</span>
64 - <select x-model="$store.pluginToggle.agentProfileKey"
65 - @change="$store.pluginToggle.onScopeChanged()"
66 - :disabled="!$store.pluginToggle.perAgentConfig">
67 - <option value="">All profiles</option>
68 - <template x-for="a in $store.pluginToggle.agentProfiles" :key="a.key">
69 - <option :value="a.key" x-text="a.label"></option>
70 - </template>
71 - </select>
72 - </label>
73 -
74 - <button type="button" class="button plugin-settings-toolbar-button" title="View all scope configs" @click="$store.pluginToggle.openConfigListModal()">
75 - <span class="icon material-symbols-outlined">list</span>
76 - </button>
77 -
78 - </div>
79 - </div>
80 -
81 - <div x-show="$store.pluginToggle.noScopeRuleMessage" class="plugin-settings-scope-info">
82 - <span class="material-symbols-outlined">info</span>
83 - <span x-text="$store.pluginToggle.noScopeRuleMessage"></span>
84 - </div>
85 - </div>
86 -
87 - </div>
88 -
89 - </div>
90 - </template>
91 - </div>
92 -
93 - <!-- Footer -->
94 - <div class="modal-footer" data-modal-footer>
95 - <template x-if="$store.pluginToggle.hasConfigScreen">
96 - <button type="button" class="btn btn-ok footer-btn-left" @click="$store.pluginToggle.openConfigWithScope()">
97 - <span class="icon material-symbols-outlined">settings</span>
98 - Configure Plugin
99 - </button>
100 - </template>
101 - <button class="btn btn-cancel" @click="window.closeModal?.()">
102 - Close
103 - </button>
104 - </div>
105 -
106 - <style>
107 - .footer-btn-left {
108 - margin-left: var(--spacing-lg);
109 - margin-right: auto;
110 - }
111 -
112 - /* Shared scope layout — mirrors plugin-settings.html */
113 - .plugin-settings-scope-section {
114 - border: 1px solid var(--color-border);
115 - border-radius: 4px;
116 - margin: 1rem;
117 - padding: 0;
118 - overflow: hidden;
119 - }
120 -
121 - .plugin-settings-scope-header {
122 - display: flex;
123 - align-items: flex-start;
124 - justify-content: space-between;
125 - gap: 1rem;
126 - padding: 0.75rem 1rem;
127 - border-bottom: 1px solid var(--color-border);
128 - background: var(--color-bg-secondary);
129 - }
130 -
131 - .plugin-settings-scope-header-copy {
132 - min-width: 0;
133 - }
134 -
135 - .plugin-settings-scope-header-toggle {
136 - display: flex;
137 - align-items: center;
138 - gap: 0.75rem;
139 - margin-left: auto;
140 - flex: 0 0 auto;
141 - white-space: nowrap;
142 - }
143 -
144 - .plugin-settings-scope-title {
145 - font-weight: 600;
146 - font-size: var(--font-size-normal);
147 - color: var(--color-text-primary);
148 - }
149 -
150 - .plugin-settings-scope-desc {
151 - font-size: var(--font-size-small);
152 - color: var(--color-text-secondary);
153 - margin-top: 0.25rem;
154 - }
155 -
156 - .plugin-settings-toolbar {
157 - padding: 1rem;
158 - }
159 -
160 - .plugin-settings-toolbar-row {
161 - display: flex;
162 - align-items: center;
163 - gap: clamp(0.75rem, 2vw, 2rem);
164 - flex-wrap: wrap;
165 - }
166 -
167 - .plugin-settings-toolbar-item {
168 - display: flex;
169 - align-items: center;
170 - gap: 0.5rem;
171 - flex: 1 1 0;
172 - min-width: 12rem;
173 - margin: 0;
174 - }
175 -
176 - .plugin-settings-toolbar-item select {
177 - flex: 1 1 auto;
178 - min-width: 0;
179 - }
180 -
181 - .plugin-settings-toolbar-label {
182 - font-weight: 600;
183 - color: var(--color-text-secondary);
184 - white-space: nowrap;
185 - }
186 -
187 - .plugin-settings-toolbar-button {
188 - flex: 0 0 auto;
189 - padding: 0.5rem 0.75rem;
190 - height: 2.5rem;
191 - }
192 -
193 - .plugin-settings-scope-info {
194 - display: flex;
195 - align-items: flex-start;
196 - gap: 0.5rem;
197 - padding: 0 1rem 1rem 1rem;
198 - color: var(--color-text-secondary);
199 - font-size: var(--font-size-small);
200 - }
201 -
202 - @media (max-width: 640px) {
203 - .plugin-settings-scope-header {
204 - flex-direction: column;
205 - align-items: stretch;
206 - }
207 -
208 - .plugin-settings-scope-header-toggle {
209 - margin-left: 0;
210 - justify-content: flex-start;
211 - white-space: normal;
212 - }
213 -
214 - .plugin-settings-toolbar-item {
215 - flex-basis: 100%;
216 - min-width: 0;
217 - }
218 - }
219 -
220 - .plugin-settings-loading {
221 - display: flex;
222 - align-items: center;
223 - justify-content: center;
224 - gap: 0.5rem;
225 - padding: 2rem;
226 - color: var(--color-text-secondary);
227 - }
228 -
229 - .plugin-settings-error {
230 - display: flex;
231 - align-items: center;
232 - gap: 0.5rem;
233 - color: var(--color-error, #e74c3c);
234 - background: var(--color-error-bg, #fdecea);
235 - border-radius: 4px;
236 - padding: 0.5rem 0.75rem;
237 - margin-bottom: 0.75rem;
238 - font-size: var(--font-size-small);
239 - }
240 -
241 - .spinning {
242 - animation: spin 1s linear infinite;
243 - }
244 -
245 - @keyframes spin {
246 - from { transform: rotate(0deg); }
247 - to { transform: rotate(360deg); }
248 - }
249 -
250 - .disabled-appearance {
251 - opacity: 0.6;
252 - }
253 -
254 - .plugin-toggle-status-text {
255 - font-weight: 600;
256 - color: var(--color-text-primary);
257 - min-width: 2rem;
258 - }
259 - </style>
260 -</body>
261 -</html>
webui/components/plugins/toggle/plugin-toggle-store.js
+11 -19
@@ -169,24 +169,6 @@ const model = {
169 }
170 },
171
172 - async openConfigWithScope() {
173 - if (!this.pluginName) return;
174 - this.error = null;
175 - try {
176 - await settingsStore.openConfig(
177 - this.pluginName,
178 - this.projectName || "",
179 - this.agentProfileKey || ""
180 - );
181 - } catch (e) {
182 - this.error = e?.message || "Failed to open plugin config";
183 - }
184 - },
185 -
186 - async openConfigListModal() {
187 - await window.openModal?.("/components/plugins/toggle/plugin-toggles.html");
188 - },
189 -
172 async switchToConfig(projectName, agentProfile) {
173 this.projectName = projectName || "";
174 this.agentProfileKey = agentProfile || "";
@@ -217,8 +199,14 @@ const model = {
199 }
200 },
201
220 - async setEnabled(enabled) {
202 + async setEnabled(enabled, { projectName = this.projectName, agentProfileKey = this.agentProfileKey } = {}) {
203 if (!this.pluginName || this.alwaysEnabled) return;
204 + const previousStatus = this.status;
205 + const previousProjectName = this.projectName;
206 + const previousAgentProfileKey = this.agentProfileKey;
207 + this.projectName = projectName || "";
208 + this.agentProfileKey = agentProfileKey || "";
209 + this.status = enabled ? 'enabled' : 'disabled';
210 this.isSaving = true;
211 try {
212 const response = await fetchApi("/plugins", {
@@ -237,7 +225,11 @@ const model = {
225 await new Promise(r => setTimeout(r, 100));
226 await this.loadConfigs();
227 } catch (e) {
228 + this.status = previousStatus;
229 + this.projectName = previousProjectName;
230 + this.agentProfileKey = previousAgentProfileKey;
231 this.error = e.message || "Failed to save";
232 + throw e;
233 } finally {
234 this.isSaving = false;
235 }