plugins: UI polish, intentional scope rules

- Added a shortcut "Open Advanced" icon next to the dropdown (visible only in Advanced mode) to quickly open the Switch modal. - Fixed the Switch modal so the "Configure Plugin" button only appears if the plugin actually has settings. - Redesigned scope rule creation in the Switch modal: switching project/agent profiles no longer auto-creates .toggle-1 files silently. Users must now intentionally click a + button to create a rule for that scope before the switch becomes active.

Alessandro committed Feb 25, 2026 at 19:45 UTC 1e4f170da169764387921fa224da8bd16f54a6d3
3 files changed +81 -24
webui/components/plugins/list/plugin-list.html
+38 -9
@@ -116,16 +116,26 @@
116
117 <div class="plugin-footer-row">
118 <div class="plugin-description" x-text="plugin.description || 'No description provided.'"></div>
119 - <select class="plugin-status-select"
120 - @change="$store.pluginListStore.updateToggle(plugin, $event.target.value)"
121 - @click.stop
122 - :disabled="plugin.always_enabled">
123 - <option value="enabled" :selected="plugin.toggle_state === 'enabled'">ON</option>
124 - <option value="disabled" :selected="plugin.toggle_state === 'disabled'">OFF</option>
125 - <template x-if="plugin.per_project_config || plugin.per_agent_config">
126 - <option value="advanced" :selected="plugin.toggle_state === 'advanced'">Advanced</option>
119 + <div class="plugin-status-group">
120 + <template x-if="plugin.toggle_state === 'advanced'">
121 + <button type="button"
122 + class="button icon-button"
123 + title="Open Advanced"
124 + @click="$store.pluginListStore.openPluginAdvancedToggle(plugin)">
125 + <span class="icon material-symbols-outlined">rule_settings</span>
126 + </button>
127 </template>
128 - </select>
128 + <select class="plugin-status-select"
129 + @change="$store.pluginListStore.updateToggle(plugin, $event.target.value)"
130 + @click.stop
131 + :disabled="plugin.always_enabled">
132 + <option value="enabled" :selected="plugin.toggle_state === 'enabled'">ON</option>
133 + <option value="disabled" :selected="plugin.toggle_state === 'disabled'">OFF</option>
134 + <template x-if="plugin.per_project_config || plugin.per_agent_config">
135 + <option value="advanced" :selected="plugin.toggle_state === 'advanced'">Advanced</option>
136 + </template>
137 + </select>
138 + </div>
139 </div>
140 </div>
141 </template>
@@ -240,6 +250,25 @@
250 margin-top: 0.5rem;
251 }
252
253 + .plugin-status-group {
254 + display: flex;
255 + align-items: center;
256 + gap: 0.5rem;
257 + flex: 0 0 auto;
258 + }
259 +
260 + .plugin-status-group .button {
261 + padding: 0.3rem 0.5rem;
262 + height: 2.2rem;
263 + display: flex;
264 + align-items: center;
265 + justify-content: center;
266 + }
267 +
268 + .plugin-status-group .button .icon {
269 + font-size: 1.1rem;
270 + }
271 +
272 .plugin-status-select {
273 padding: 0.3rem 2rem 0.3rem 0.6rem;
274 font-size: 0.9rem;
webui/components/plugins/toggle/plugin-toggle-advanced.html
+32 -6
@@ -60,10 +60,18 @@
60 </label>
61
62 <div class="plugin-settings-toolbar-item plugin-status-toggle-item">
63 - <label class="toggle">
63 + <template x-if="!$store.pluginToggle.hasExplicitRuleForScope && !$store.pluginToggle.alwaysEnabled">
64 + <button type="button"
65 + class="button icon-button"
66 + title="Add explicit rule for this scope"
67 + @click="$store.pluginToggle.addRule()">
68 + <span class="icon material-symbols-outlined">add</span>
69 + </button>
70 + </template>
71 + <label class="toggle" :class="{ 'disabled-appearance': !$store.pluginToggle.hasExplicitRuleForScope && !$store.pluginToggle.alwaysEnabled }">
72 <input type="checkbox"
73 :checked="$store.pluginToggle.status === 'enabled'"
66 - :disabled="$store.pluginToggle.alwaysEnabled || $store.pluginToggle.isSaving"
74 + :disabled="$store.pluginToggle.alwaysEnabled || $store.pluginToggle.isSaving || !$store.pluginToggle.hasExplicitRuleForScope"
75 @change="$store.pluginToggle.setEnabled($event.target.checked)">
76 <span class="toggler"></span>
77 </label>
@@ -91,10 +99,12 @@
99
100 <!-- Footer -->
101 <div class="modal-footer" data-modal-footer>
94 - <button type="button" class="btn btn-ok footer-btn-left" @click="$store.pluginToggle.openConfigWithScope()">
95 - <span class="icon material-symbols-outlined">settings</span>
96 - Configure Plugin
97 - </button>
102 + <template x-if="$store.pluginToggle.hasConfigScreen">
103 + <button type="button" class="btn btn-ok footer-btn-left" @click="$store.pluginToggle.openConfigWithScope()">
104 + <span class="icon material-symbols-outlined">settings</span>
105 + Configure Plugin
106 + </button>
107 + </template>
108 <button class="btn btn-cancel" @click="window.closeModal?.()">
109 Close
110 </button>
@@ -228,6 +238,22 @@
238 flex: 0 0 auto;
239 }
240
241 + .plugin-status-toggle-item .button {
242 + padding: 0.3rem 0.5rem;
243 + height: 2.2rem;
244 + display: flex;
245 + align-items: center;
246 + justify-content: center;
247 + }
248 +
249 + .plugin-status-toggle-item .button .icon {
250 + font-size: 1.1rem;
251 + }
252 +
253 + .disabled-appearance {
254 + opacity: 0.6;
255 + }
256 +
257 .plugin-toggle-status-text {
258 font-weight: 600;
259 color: var(--color-text-primary);
webui/components/plugins/toggle/plugin-toggle-store.js
+11 -9
@@ -23,6 +23,7 @@ const model = {
23 perProjectConfig: true,
24 perAgentConfig: true,
25 explicitPath: null,
26 + hasExplicitRuleForScope: false,
27 configs: [],
28
29 async open(plugin) {
@@ -39,14 +40,11 @@ const model = {
40 this.alwaysEnabled = typeof plugin === 'object' ? !!plugin.always_enabled : false;
41 this.perProjectConfig = typeof plugin === 'object' ? !!plugin.per_project_config : true;
42 this.perAgentConfig = typeof plugin === 'object' ? !!plugin.per_agent_config : true;
43 + this.hasConfigScreen = typeof plugin === 'object' ? !!plugin.has_config_screen : false;
44
45 try {
46 await Promise.all([this.loadProjects(), this.loadAgentProfiles()]);
47 await this.loadConfigs();
46 - // Auto-save ON for the default scope (Global + All profiles)
47 - if (!this.explicitPath && !this.alwaysEnabled && this.pluginName) {
48 - await this.setEnabled(true);
49 - }
48 } finally {
49 this.isLoading = false;
50 }
@@ -61,6 +59,8 @@ const model = {
59 this.perProjectConfig = true;
60 this.perAgentConfig = true;
61 this.alwaysEnabled = false;
62 + this.hasConfigScreen = false;
63 + this.hasExplicitRuleForScope = false;
64 },
65
66 async loadProjects() {
@@ -175,6 +175,7 @@ const model = {
175 calculateStatus() {
176 if (this.alwaysEnabled) {
177 this.explicitPath = null;
178 + this.hasExplicitRuleForScope = true;
179 this.status = 'enabled';
180 return;
181 }
@@ -187,9 +188,11 @@ const model = {
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 },
@@ -223,17 +226,16 @@ const model = {
226 async onScopeChanged() {
227 this.calculateStatus();
228
226 - // Auto-save immediately so the displayed default state is persisted
227 - if (!this.explicitPath && !this.alwaysEnabled && this.pluginName) {
228 - await this.setEnabled(true);
229 - }
230 -
229 // Sync scope with settings store so its loadSettings picks up the right context
230 settingsStore.projectName = this.projectName || "";
231 settingsStore.agentProfileKey = this.agentProfileKey || "";
232 await settingsStore.loadSettings();
233 },
234
235 + async addRule() {
236 + await this.setEnabled(this.status === 'enabled');
237 + },
238 +
239 projectLabel(key) {
240 if (!key) return "Global";
241 const found = (this.projects || []).find(p => p.key === key);