Plugin UX Consolidation (pre-upstream-merge snapshot)

Co-authored-by: Cursor <cursoragent@cursor.com>

Alessandro committed Feb 24, 2026 at 11:25 UTC f78849065745542e1d0dd024d5a5c660bd356431
12 files changed +529 -377
plugins/example_agent/plugin.json
+3 -2
@@ -1,8 +1,9 @@
1 {
2 - "name": "Example Agent",
2 + "title": "Example Agent",
3 "description": "Example agent plugin demonstrating the Agent Zero plugin system.",
4 "version": "1.0.0",
5 "settings_sections": [],
6 "per_project_config": true,
7 - "per_agent_config": false
7 + "per_agent_config": false,
8 + "always_enabled": true
9 }
plugins/memory/plugin.json
+1 -1
@@ -1,5 +1,5 @@
1 {
2 - "name": "Memory",
2 + "title": "Memory",
3 "description": "Provides persistent memory capabilities to Agent Zero agents.",
4 "version": "1.0.0",
5 "settings_sections": ["agent"],
python/api/plugins.py
+24 -1
@@ -82,7 +82,14 @@ class Plugins(ApiHandler):
82 agent_profile="*",
83 only_first=False,
84 )
85 - allowed_paths = {c.get("path", "") for c in configs}
85 + toggles = plugins.find_plugin_assets(
86 + plugins.TOGGLE_FILE_PATTERN,
87 + plugin_name=plugin_name,
88 + project_name="*",
89 + agent_profile="*",
90 + only_first=False,
91 + )
92 + allowed_paths = {c.get("path", "") for c in configs + toggles}
93 if path not in allowed_paths:
94 return Response(status=400, response="Invalid path")
95
@@ -108,4 +115,20 @@ class Plugins(ApiHandler):
115 plugins.save_plugin_config(plugin_name, project_name, agent_profile, settings)
116 return {"ok": True}
117
118 + if action == "toggle_plugin":
119 + plugin_name = input.get("plugin_name", "")
120 + enabled = input.get("enabled")
121 + project_name = input.get("project_name", "")
122 + agent_profile = input.get("agent_profile", "")
123 +
124 + if not plugin_name:
125 + return Response(status=400, response="Missing plugin_name")
126 + if enabled is None:
127 + return Response(status=400, response="Missing enabled state")
128 +
129 + plugins.toggle_plugin(
130 + plugin_name, bool(enabled), project_name, agent_profile
131 + )
132 + return {"ok": True}
133 +
134 return Response(status=400, response=f"Unknown action: {action}")
python/helpers/files.py
+5 -1
@@ -425,7 +425,11 @@ def write_file(relative_path: str, content: str, encoding: str = "utf-8"):
425
426 def delete_file(relative_path: str):
427 abs_path = get_abs_path(relative_path)
428 - os.remove(abs_path)
428 + if os.path.exists(abs_path):
429 + try:
430 + os.remove(abs_path)
431 + except OSError:
432 + pass
433
434 def write_file_bin(relative_path: str, content: bytes):
435 abs_path = get_abs_path(relative_path)
python/helpers/plugins.py
+34 -15
@@ -1,6 +1,6 @@
1 from __future__ import annotations
2
3 -import re, json
3 +import re, json, glob
4 from pathlib import Path
5 from typing import Any, Dict, List, Literal, Optional, TYPE_CHECKING, TypedDict
6
@@ -27,11 +27,11 @@ CONFIG_FILE_NAME = "config.json"
27 CONFIG_DEFAULT_FILE_NAME = "config.default.json"
28 DISABLED_FILE_NAME = ".toggle-0"
29 ENABLED_FILE_NAME = ".toggle-1"
30 -TOGGLE_FILE_PATTERN = "*.toggle-[01]"
30 +TOGGLE_FILE_PATTERN = ".toggle-[01]"
31
32
33 class PluginMetadata(BaseModel):
34 - name: str = ""
34 + title: str = ""
35 description: str = ""
36 version: str = ""
37 settings_sections: List[str] = Field(default_factory=list)
@@ -49,6 +49,7 @@ class PluginListItem(BaseModel):
49 settings_sections: List[str] = Field(default_factory=list)
50 per_project_config: bool = False
51 per_agent_config: bool = False
52 + always_enabled: bool = False
53 is_custom: bool = False
54 has_main_screen: bool = False
55 has_config_screen: bool = False
@@ -95,17 +96,18 @@ def get_enhanced_plugins_list(
96 )
97 has_main_screen = files.exists(str(d / "webui" / "main.html"))
98 has_config_screen = files.exists(str(d / "webui" / "config.html"))
98 - toggle_state = get_toggle_state(meta.name)
99 + toggle_state = get_toggle_state(d.name)
100 results.append(
101 PluginListItem(
102 name=d.name,
103 path=str(d),
103 - display_name=meta.name or d.name,
104 + display_name=meta.title or d.name,
105 description=meta.description,
106 version=meta.version,
107 settings_sections=meta.settings_sections,
108 per_project_config=meta.per_project_config,
109 per_agent_config=meta.per_agent_config,
110 + always_enabled=meta.always_enabled,
111 is_custom=is_custom,
112 has_main_screen=has_main_screen,
113 has_config_screen=has_config_screen,
@@ -206,8 +208,10 @@ def get_enabled_plugins(agent: Agent | None):
208 include_project=True,
209 )
210
209 - # go through agent paths in reverse order and determine the state
210 - for agent_path in reversed(agent_paths):
211 + # go through agent paths in forward order and determine the state
212 + # subagents.get_paths returns [default, user, project] (priority low to high)
213 + # we want high priority to override low priority.
214 + for agent_path in agent_paths:
215 if enabled:
216 enabled = not files.exists(
217 files.get_abs_path(agent_path, DISABLED_FILE_NAME)
@@ -227,13 +231,15 @@ def get_toggle_state(plugin_name: str) -> ToggleState:
231 meta = get_plugin_meta(plugin_name)
232 if not meta:
233 return "disabled"
230 - if meta.always_enabled:
231 - return "enabled"
234
235 state = "enabled"
236
237 # toggles inside of user directory (there should be only one, but let's make it work in any case)
236 - usr_toggles = files.find_existing_paths_by_pattern(files.get_abs_path(files.USER_DIR, files.PLUGINS_DIR, plugin_name, TOGGLE_FILE_PATTERN))
238 + usr_toggles = files.find_existing_paths_by_pattern(
239 + files.get_abs_path(
240 + files.USER_DIR, files.PLUGINS_DIR, plugin_name, TOGGLE_FILE_PATTERN
241 + )
242 + )
243 for toggle in usr_toggles:
244 if toggle.endswith(ENABLED_FILE_NAME):
245 state = "enabled"
@@ -249,9 +255,19 @@ def get_toggle_state(plugin_name: str) -> ToggleState:
255 agent_profile="*" if meta.per_agent_config else "",
256 only_first=False,
257 )
252 - if len(configs) > len(usr_toggles):
253 - state = "advanced"
258
259 + # Advanced if there are specific overrides (project or agent specific)
260 + specific_overrides = [
261 + c for c in configs
262 + if c.get("project_name") or c.get("agent_profile")
263 + ]
264 +
265 + if len(specific_overrides) > 0:
266 + state = "advanced"
267 +
268 + if state != "advanced" and meta.always_enabled:
269 + return "enabled"
270 +
271 return state
272
273
@@ -261,11 +277,13 @@ def toggle_plugin(
277 enabled_file = determine_plugin_asset_path(plugin_name, project_name, agent_profile, ENABLED_FILE_NAME)
278 disabled_file = determine_plugin_asset_path(plugin_name, project_name, agent_profile, DISABLED_FILE_NAME)
279
280 + # ensure clean state by deleting both potential files first
281 + files.delete_file(enabled_file)
282 + files.delete_file(disabled_file)
283 +
284 if enabled:
265 - files.delete_file(disabled_file)
285 files.write_file(enabled_file, "")
286 else:
268 - files.delete_file(enabled_file)
287 files.write_file(disabled_file, "")
288
289
@@ -350,9 +368,10 @@ def find_plugin_assets(
368 results: list[PluginAssetFile] = []
369
370 def _collect(path: str, proj: str, profile: str) -> bool:
371 + is_glob = glob.has_magic(path)
372 matched_paths = (
373 files.find_existing_paths_by_pattern(path)
355 - if "*" in path
374 + if is_glob
375 else ([path] if files.exists(path) else [])
376 )
377
webui/components/plugins/list/plugin-list.html
+38
@@ -95,6 +95,20 @@
95 </template>
96 </div>
97 </div>
98 +
99 + <div class="plugin-controls-row">
100 + <select class="plugin-status-select"
101 + @change="$store.pluginListStore.updateToggle(plugin, $event.target.value)"
102 + @click.stop
103 + :disabled="plugin.always_enabled">
104 + <option value="enabled" :selected="plugin.toggle_state === 'enabled'">ON</option>
105 + <option value="disabled" :selected="plugin.toggle_state === 'disabled'">OFF</option>
106 + <template x-if="plugin.per_project_config || plugin.per_agent_config">
107 + <option value="advanced" :selected="plugin.toggle_state === 'advanced'">Advanced</option>
108 + </template>
109 + </select>
110 + </div>
111 +
112 <div class="plugin-description" x-text="plugin.description || 'No description provided.'"></div>
113 </div>
114 </template>
@@ -201,6 +215,30 @@
215 font-size: 1.1rem;
216 }
217
218 + .plugin-controls-row {
219 + margin-top: 0.5rem;
220 + display: flex;
221 + align-items: center;
222 + justify-content: flex-end;
223 + }
224 +
225 + .plugin-status-select {
226 + padding: 0.3rem 2rem 0.3rem 0.6rem;
227 + font-size: 0.9rem;
228 + border: 1px solid var(--color-border);
229 + border-radius: 4px;
230 + background: var(--color-bg-primary);
231 + color: var(--color-text-primary);
232 + cursor: pointer;
233 + height: 2.2rem;
234 + }
235 +
236 + .plugin-status-select:disabled {
237 + opacity: 0.7;
238 + cursor: default;
239 + background: var(--color-bg-secondary);
240 + }
241 +
242 .plugin-description {
243 margin-top: 0.35rem;
244 color: var(--color-text-secondary);
webui/components/plugins/list/pluginListStore.js
+50
@@ -1,6 +1,7 @@
1 import { createStore } from "/js/AlpineStore.js";
2 import * as api from "/js/api.js";
3 import "/components/plugins/plugin-settings-store.js";
4 +import "/components/plugins/toggle/plugin-toggle-store.js";
5 import {
6 store as notificationStore,
7 defaultPriority,
@@ -56,10 +57,18 @@ const model = {
57 async openPluginConfig(plugin) {
58 if (!plugin?.name || !plugin?.has_config_screen) return;
59 try {
60 + // Initialize toggle store for activation state UI in settings modal
61 + const pluginToggleStore = Alpine.store("pluginToggle");
62 + if (pluginToggleStore?.open) await pluginToggleStore.open(plugin);
63 +
64 const pluginSettingsStore = Alpine.store("pluginSettings");
65 if (!pluginSettingsStore?.open) {
66 throw new Error("Plugin settings store is unavailable.");
67 }
68 + // Set saveMode before open() so loadSettings picks up the right mode
69 + if (plugin.settings_sections?.includes('core')) {
70 + pluginSettingsStore.saveMode = 'core';
71 + }
72 await pluginSettingsStore.open(plugin.name);
73 window.openModal?.("components/plugins/plugin-settings.html");
74 } catch (e) {
@@ -67,6 +76,47 @@ const model = {
76 }
77 },
78
79 + async openPluginAdvancedToggle(plugin) {
80 + if (!plugin?.name) return;
81 + this.selectedPlugin = plugin;
82 + try {
83 + const pluginToggleStore = Alpine.store("pluginToggle");
84 + if (!pluginToggleStore?.open) {
85 + throw new Error("Plugin toggle store is unavailable.");
86 + }
87 + await pluginToggleStore.open(plugin);
88 + window.openModal?.("components/plugins/toggle/plugin-toggle-advanced.html");
89 + } catch (e) {
90 + showErrorNotification(e, "Failed to open plugin switch");
91 + }
92 + },
93 +
94 + async updateToggle(plugin, value) {
95 + if (!plugin?.name) return;
96 +
97 + if (value === 'advanced') {
98 + await this.openPluginAdvancedToggle(plugin);
99 + return;
100 + }
101 +
102 + const enabled = value === 'enabled';
103 + this.loading = true; // Show loading state
104 + try {
105 + const response = await api.callJsonApi("plugins", {
106 + action: "toggle_plugin",
107 + plugin_name: plugin.name,
108 + enabled: enabled,
109 + project_name: "", // Global
110 + agent_profile: "" // Global
111 + });
112 + if (response?.error) throw new Error(response.error);
113 + await this.refresh();
114 + } catch (e) {
115 + showErrorNotification(e, "Failed to toggle plugin");
116 + this.loading = false;
117 + }
118 + },
119 +
120 openPluginInfo(plugin) {
121 if (!plugin) return;
122 this.selectedPlugin = plugin;
webui/components/plugins/plugin-settings-store.js
+15 -5
@@ -52,6 +52,14 @@ const model = {
52 }
53
54 await this.loadSettings();
55 +
56 + // Mirror scope change to pluginToggle so activation state stays in sync
57 + const toggleStore = Alpine.store('pluginToggle');
58 + if (toggleStore) {
59 + toggleStore.projectName = nextProject;
60 + toggleStore.agentProfileKey = nextProfile;
61 + toggleStore.calculateStatus();
62 + }
63 },
64
65 // where the settings were actually loaded from
@@ -166,17 +174,19 @@ const model = {
174 error: null,
175
176 // Called by the subsection button before openModal()
169 - async open(pluginName) {
177 + // Optional scope: { projectName, agentProfileKey } — skips redundant global loadSettings()
178 + // when the caller already knows which scope to open at.
179 + async open(pluginName, { projectName = "", agentProfileKey = "" } = {}) {
180 this.pluginName = pluginName;
181 this.pluginMeta = null;
182 this.settings = {};
183 this.settingsSnapshotJson = "";
184 this.error = null;
185 this.saveMode = 'plugin';
176 - this.projectName = "";
177 - this.agentProfileKey = "";
178 - this.previousProjectName = "";
179 - this.previousAgentProfileKey = "";
186 + this.projectName = projectName;
187 + this.agentProfileKey = agentProfileKey;
188 + this.previousProjectName = projectName;
189 + this.previousAgentProfileKey = agentProfileKey;
190 this.loadedPath = "";
191 this.loadedProjectName = "";
192 this.loadedAgentProfile = "";
webui/components/plugins/plugin-settings.html
+40
@@ -23,6 +23,7 @@
23 <label class="plugin-settings-toolbar-item">
24 <span class="plugin-settings-toolbar-label">Project</span>
25 <select x-model="$store.pluginSettings.projectName"
26 + x-init="$nextTick(() => $el.value = $store.pluginSettings.projectName)"
27 @change="$store.pluginSettings.onScopeChanged()">
28 <option value="">Global</option>
29 <template x-for="project in $store.pluginSettings.projects" :key="project.key">
@@ -34,6 +35,7 @@
35 <label class="plugin-settings-toolbar-item">
36 <span class="plugin-settings-toolbar-label">Agent profile</span>
37 <select x-model="$store.pluginSettings.agentProfileKey"
38 + x-init="$nextTick(() => $el.value = $store.pluginSettings.agentProfileKey)"
39 @change="$store.pluginSettings.onScopeChanged()">
40 <option value="">All profiles</option>
41 <template x-for="profile in $store.pluginSettings.agentProfiles" :key="profile.key">
@@ -47,6 +49,27 @@
49 </button>
50
51 </div>
52 +
53 + <!-- Activation row: ON/OFF toggle + link to Advanced per-scope modal -->
54 + <div class="plugin-settings-toolbar plugin-activation-toolbar" x-show="$store.pluginToggle">
55 + <div class="plugin-settings-toolbar-row">
56 + <div class="plugin-activation-toggle-group">
57 + <label class="toggle">
58 + <input type="checkbox"
59 + :checked="$store.pluginToggle?.status === 'enabled'"
60 + :disabled="$store.pluginToggle?.alwaysEnabled || $store.pluginToggle?.isSaving"
61 + @change="$store.pluginToggle.setEnabled($event.target.checked)">
62 + <span class="toggler"></span>
63 + </label>
64 + <span class="plugin-toggle-status-text" x-text="$store.pluginToggle?.statusLabel"></span>
65 + </div>
66 + <button type="button" class="button plugin-settings-toolbar-button"
67 + @click="window.openModal?.('components/plugins/toggle/plugin-toggle-advanced.html')">
68 + <span class="icon material-symbols-outlined">toggle_on</span>
69 + Switch
70 + </button>
71 + </div>
72 + </div>
73 </div>
74
75 <div x-show="$store.pluginSettings.scopeMismatchMessage" class="plugin-settings-scope-info">
@@ -203,6 +226,23 @@
226 color: var(--color-text-secondary);
227 margin-top: 0.25rem;
228 }
229 +
230 + .plugin-activation-toolbar {
231 + padding-top: 0.75rem;
232 + padding-bottom: 0.75rem;
233 + }
234 +
235 + .plugin-activation-toggle-group {
236 + display: flex;
237 + align-items: center;
238 + gap: 0.75rem;
239 + }
240 +
241 + .plugin-toggle-status-text {
242 + font-weight: 600;
243 + color: var(--color-text-primary);
244 + min-width: 2rem;
245 + }
246 </style>
247 </body>
248 </html>
webui/components/plugins/toggle/plugin-toggle-advanced.html
+134 -106
@@ -1,97 +1,137 @@
1 <html>
2 <head>
3 - <title>Plugin Settings</title>
3 + <title>Plugin Switch</title>
4 <script type="module">
5 - import { store } from "/components/plugins/plugin-settings-store.js";
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.pluginSettings">
11 - <div x-create="$store.pluginSettings.onModalOpen()"
12 - x-destroy="$store.pluginSettings.cleanup()">
13 -
14 - <!-- Context toolbar: Project + Agent profile (mirrors skills list) -->
15 - <div class="plugin-settings-scope-section">
16 - <div class="plugin-settings-scope-header">
17 - <div class="plugin-settings-scope-title">Settings scope</div>
18 - <div class="plugin-settings-scope-desc">This plugin supports settings per project or agent profile.</div>
19 - </div>
20 - <div class="plugin-settings-toolbar">
21 - <div class="plugin-settings-toolbar-row">
22 -
23 - <label class="plugin-settings-toolbar-item">
24 - <span class="plugin-settings-toolbar-label">Project</span>
25 - <select x-model="$store.pluginSettings.projectName"
26 - @change="$store.pluginSettings.onScopeChanged()">
27 - <option value="">Global</option>
28 - <template x-for="project in $store.pluginSettings.projects" :key="project.key">
29 - <option :value="project.key" x-text="project.label"></option>
30 - </template>
31 - </select>
32 - </label>
33 -
34 - <label class="plugin-settings-toolbar-item">
35 - <span class="plugin-settings-toolbar-label">Agent profile</span>
36 - <select x-model="$store.pluginSettings.agentProfileKey"
37 - @change="$store.pluginSettings.onScopeChanged()">
38 - <option value="">All profiles</option>
39 - <template x-for="profile in $store.pluginSettings.agentProfiles" :key="profile.key">
40 - <option :value="profile.key" x-text="profile.label"></option>
41 - </template>
42 - </select>
43 - </label>
44 -
45 - <button type="button" class="button plugin-settings-toolbar-button" title="Show existing configurations" @click="$store.pluginSettings.openConfigListModal()">
46 - <span class="icon material-symbols-outlined">list</span>
47 - </button>
48 -
49 - </div>
50 - </div>
51 -
52 - <div x-show="$store.pluginSettings.scopeMismatchMessage" class="plugin-settings-scope-info">
53 - <span class="material-symbols-outlined">info</span>
54 - <span x-text="$store.pluginSettings.scopeMismatchMessage"></span>
55 - </div>
56 - </div>
10 + <template x-if="$store.pluginToggle">
11 + <div x-create="$store.pluginToggle.open($store.pluginListStore?.selectedPlugin || '')"
12 + x-destroy="$store.pluginToggle.cleanup()">
13
14 <!-- Error -->
59 - <div x-show="$store.pluginSettings.error" class="plugin-settings-error">
15 + <div x-show="$store.pluginToggle.error" class="plugin-settings-error">
16 <span class="material-symbols-outlined">error</span>
61 - <span x-text="$store.pluginSettings.error"></span>
17 + <span x-text="$store.pluginToggle.error"></span>
18 </div>
19
20 <!-- Loading -->
65 - <div x-show="$store.pluginSettings.isLoading" class="plugin-settings-loading">
21 + <div x-show="$store.pluginToggle.isLoading" class="plugin-settings-loading">
22 <span class="material-symbols-outlined spinning">progress_activity</span>
67 - <span>Loading settings...</span>
23 + <span>Loading status...</span>
24 </div>
25
70 - <!-- Plugin settings body: plugin provides /plugins/<name>/webui/config.html -->
71 - <div x-show="!$store.pluginSettings.isLoading"
72 - class="plugin-settings-body"
73 - x-html="$store.pluginSettings.settingsComponentHtml">
26 + <!-- Main UI -->
27 + <div x-show="!$store.pluginToggle.isLoading">
28 +
29 + <!-- Scope selector + per-scope ON/OFF -->
30 + <div class="plugin-settings-scope-section">
31 + <div class="plugin-settings-scope-header">
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-toolbar">
36 + <div class="plugin-settings-toolbar-row">
37 +
38 + <label class="plugin-settings-toolbar-item">
39 + <span class="plugin-settings-toolbar-label">Project</span>
40 + <select x-model="$store.pluginToggle.projectName"
41 + @change="$store.pluginToggle.onScopeChanged()">
42 + <option value="">Global</option>
43 + <template x-for="p in $store.pluginToggle.projects" :key="p.key">
44 + <option :value="p.key" x-text="p.label"></option>
45 + </template>
46 + </select>
47 + </label>
48 +
49 + <label class="plugin-settings-toolbar-item">
50 + <span class="plugin-settings-toolbar-label">Agent profile</span>
51 + <select x-model="$store.pluginToggle.agentProfileKey"
52 + @change="$store.pluginToggle.onScopeChanged()">
53 + <option value="">All profiles</option>
54 + <template x-for="a in $store.pluginToggle.agentProfiles" :key="a.key">
55 + <option :value="a.key" x-text="a.label"></option>
56 + </template>
57 + </select>
58 + </label>
59 +
60 + <div class="plugin-settings-toolbar-item plugin-status-toggle-item">
61 + <label class="toggle">
62 + <input type="checkbox"
63 + :checked="$store.pluginToggle.status === 'enabled'"
64 + :disabled="$store.pluginToggle.alwaysEnabled || $store.pluginToggle.isSaving"
65 + @change="$store.pluginToggle.setEnabled($event.target.checked)">
66 + <span class="toggler"></span>
67 + </label>
68 + <span class="plugin-toggle-status-text" x-text="$store.pluginToggle.statusLabel"></span>
69 + </div>
70 +
71 + <button type="button" class="button plugin-settings-toolbar-button" title="View all scope configs" @click="$store.pluginToggle.openConfigListModal()">
72 + <span class="icon material-symbols-outlined">list</span>
73 + </button>
74 +
75 + </div>
76 + </div>
77 +
78 + <div x-show="$store.pluginToggle.noScopeRuleMessage" class="plugin-settings-scope-info">
79 + <span class="material-symbols-outlined">info</span>
80 + <span x-text="$store.pluginToggle.noScopeRuleMessage"></span>
81 + </div>
82 + </div>
83 +
84 </div>
85
86 </div>
87 </template>
88 </div>
89
80 - <!-- Footer (pinned outside scroll area) -->
90 + <!-- Footer -->
91 <div class="modal-footer" data-modal-footer>
82 - <button class="btn btn-ok"
83 - @click="$store.pluginSettings.save()"
84 - :disabled="$store.pluginSettings?.isSaving || $store.pluginSettings?.isLoading">
85 - Save
92 + <button type="button" class="btn btn-ok footer-btn-left" @click="$store.pluginToggle.openConfigWithScope()">
93 + <span class="icon material-symbols-outlined">settings</span>
94 + Configure Plugin
95 </button>
87 - <button class="btn btn-cancel"
88 - @click="window.closeModal?.()">
89 - Cancel
96 + <button class="btn btn-cancel" @click="window.closeModal?.()">
97 + Close
98 </button>
99 </div>
100
101 <style>
94 - .plugin-settings-toolbar, .plugin-settings-body {
102 + .footer-btn-left {
103 + margin-left: var(--spacing-lg);
104 + margin-right: auto;
105 + }
106 +
107 + /* Shared scope layout — mirrors plugin-settings.html */
108 + .plugin-settings-scope-section {
109 + border: 1px solid var(--color-border);
110 + border-radius: 4px;
111 + margin: 1rem;
112 + padding: 0;
113 + overflow: hidden;
114 + }
115 +
116 + .plugin-settings-scope-header {
117 + padding: 0.75rem 1rem;
118 + border-bottom: 1px solid var(--color-border);
119 + background: var(--color-bg-secondary);
120 + }
121 +
122 + .plugin-settings-scope-title {
123 + font-weight: 600;
124 + font-size: var(--font-size-normal);
125 + color: var(--color-text-primary);
126 + }
127 +
128 + .plugin-settings-scope-desc {
129 + font-size: var(--font-size-small);
130 + color: var(--color-text-secondary);
131 + margin-top: 0.25rem;
132 + }
133 +
134 + .plugin-settings-toolbar {
135 padding: 1rem;
136 }
137
@@ -111,10 +151,9 @@
151 margin: 0;
152 }
153
114 - .plugin-settings-toolbar-button {
115 - flex: 0 0 auto;
116 - padding: 0.5rem 0.75rem;
117 - height: 2.5rem;
154 + .plugin-settings-toolbar-item select {
155 + flex: 1 1 auto;
156 + min-width: 0;
157 }
158
159 .plugin-settings-toolbar-label {
@@ -123,9 +162,10 @@
162 white-space: nowrap;
163 }
164
126 - .plugin-settings-toolbar-item select {
127 - flex: 1 1 auto;
128 - min-width: 0;
165 + .plugin-settings-toolbar-button {
166 + flex: 0 0 auto;
167 + padding: 0.5rem 0.75rem;
168 + height: 2.5rem;
169 }
170
171 .plugin-settings-scope-info {
@@ -137,6 +177,11 @@
177 font-size: var(--font-size-small);
178 }
179
180 + .plugin-settings-body {
181 + padding: 1rem;
182 + min-height: 4rem;
183 + }
184 +
185 @media (max-width: 640px) {
186 .plugin-settings-toolbar-item {
187 flex-basis: 100%;
@@ -144,6 +189,15 @@
189 }
190 }
191
192 + .plugin-settings-loading {
193 + display: flex;
194 + align-items: center;
195 + justify-content: center;
196 + gap: 0.5rem;
197 + padding: 2rem;
198 + color: var(--color-text-secondary);
199 + }
200 +
201 .plugin-settings-error {
202 display: flex;
203 align-items: center;
@@ -156,19 +210,6 @@
210 font-size: var(--font-size-small);
211 }
212
159 - .plugin-settings-loading {
160 - display: flex;
161 - align-items: center;
162 - justify-content: center;
163 - gap: 0.5rem;
164 - padding: 2rem;
165 - color: var(--color-text-secondary);
166 - }
167 -
168 - .plugin-settings-body {
169 - min-height: 4rem;
170 - }
171 -
213 .spinning {
214 animation: spin 1s linear infinite;
215 }
@@ -178,30 +219,17 @@
219 to { transform: rotate(360deg); }
220 }
221
181 - .plugin-settings-scope-section {
182 - border: 1px solid var(--color-border);
183 - border-radius: 4px;
184 - margin: 1rem;
185 - padding: 0;
186 - overflow: hidden;
187 - }
188 -
189 - .plugin-settings-scope-header {
190 - padding: 0.75rem 1rem;
191 - border-bottom: 1px solid var(--color-border);
192 - background: var(--color-bg-secondary);
222 + .plugin-status-toggle-item {
223 + display: flex;
224 + align-items: center;
225 + gap: 0.75rem;
226 + flex: 0 0 auto;
227 }
228
195 - .plugin-settings-scope-title {
229 + .plugin-toggle-status-text {
230 font-weight: 600;
197 - font-size: var(--font-size-normal);
231 color: var(--color-text-primary);
199 - }
200 -
201 - .plugin-settings-scope-desc {
202 - font-size: var(--font-size-small);
203 - color: var(--color-text-secondary);
204 - margin-top: 0.25rem;
232 + min-width: 2rem;
233 }
234 </style>
235 </body>
webui/components/plugins/toggle/plugin-toggle-store.js
+152 -225
@@ -1,100 +1,88 @@
1 import { createStore } from "/js/AlpineStore.js";
2 +import { store as settingsStore } from "/components/plugins/plugin-settings-store.js";
3
4 const fetchApi = globalThis.fetchApi;
5
6 const model = {
6 - // which plugin this modal is showing
7 pluginName: null,
8 - pluginMeta: null,
9 -
10 - // context selectors (mirrors skills list pattern)
8 +
9 + // Context selectors
10 projects: [],
11 agentProfiles: [],
12 projectName: "",
13 agentProfileKey: "",
14
16 - // plugin settings data (plugins bind their fields here)
17 - settings: {},
15 + // State
16 + isLoading: false,
17 + isSaving: false,
18 + error: null,
19 +
20 + // Status: 'enabled' | 'disabled'
21 + status: 'enabled',
22 + alwaysEnabled: false,
23 + explicitPath: null,
24 + configs: [],
25 +
26 + async open(plugin) {
27 + this.isLoading = true;
28 + this.error = null;
29 + this.projects = [];
30 + this.agentProfiles = [];
31 + this.projectName = "";
32 + this.agentProfileKey = "";
33 + this.configs = [];
34
19 - settingsSnapshotJson: "",
20 - previousProjectName: "",
21 - previousAgentProfileKey: "",
35 + const pluginName = typeof plugin === 'string' ? plugin : plugin?.name;
36 + this.pluginName = pluginName;
37 + this.alwaysEnabled = typeof plugin === 'object' ? !!plugin.always_enabled : false;
38
23 - _toComparableJson(value) {
39 try {
25 - return JSON.stringify(value ?? {});
26 - } catch {
27 - return "";
40 + await Promise.all([this.loadProjects(), this.loadAgentProfiles()]);
41 + await this.loadConfigs();
42 + } finally {
43 + this.isLoading = false;
44 }
45 },
46
31 - get hasUnsavedChanges() {
32 - return this._toComparableJson(this.settings) !== (this.settingsSnapshotJson || "");
33 - },
34 -
35 - confirmDiscardUnsavedChanges() {
36 - if (!this.hasUnsavedChanges) return true;
37 - return window.confirm("You have unsaved changes that will be lost. Continue?");
47 + cleanup() {
48 + this.pluginName = null;
49 + this.projectName = "";
50 + this.agentProfileKey = "";
51 + this.error = null;
52 + this.configs = [];
53 },
54
40 - async onScopeChanged() {
41 - const nextProject = this.projectName || "";
42 - const nextProfile = this.agentProfileKey || "";
43 - const prevProject = this.previousProjectName || "";
44 - const prevProfile = this.previousAgentProfileKey || "";
45 -
46 - if (nextProject === prevProject && nextProfile === prevProfile) return;
47 -
48 - if (!this.confirmDiscardUnsavedChanges()) {
49 - this.projectName = prevProject;
50 - this.agentProfileKey = prevProfile;
51 - return;
55 + async loadProjects() {
56 + try {
57 + const response = await fetchApi("/projects", {
58 + method: "POST",
59 + headers: { "Content-Type": "application/json" },
60 + body: JSON.stringify({ action: "list_options" }),
61 + });
62 + const data = await response.json().catch(() => ({}));
63 + this.projects = data.ok ? (data.data || []) : [];
64 + } catch {
65 + this.projects = [];
66 }
53 -
54 - await this.loadSettings();
55 - },
56 -
57 - // where the settings were actually loaded from
58 - loadedPath: "",
59 - loadedProjectName: "",
60 - loadedAgentProfile: "",
61 -
62 - projectLabel(key) {
63 - if (!key) return "Global";
64 - const found = (this.projects || []).find((p) => p.key === key);
65 - return found?.label || key;
66 - },
67 -
68 - agentProfileLabel(key) {
69 - if (!key) return "All profiles";
70 - const found = (this.agentProfiles || []).find((p) => p.key === key);
71 - return found?.label || key;
67 },
68
74 - get scopeMismatchMessage() {
75 - const selectedProject = this.projectName || "";
76 - const selectedProfile = this.agentProfileKey || "";
77 - const loadedProject = this.loadedProjectName || "";
78 - const loadedProfile = this.loadedAgentProfile || "";
79 -
80 - if (!this.loadedPath) return "";
81 - if (selectedProject === loadedProject && selectedProfile === loadedProfile) return "";
82 -
83 - return `Settings do not yet exist for this combination, settings from ${this.projectLabel(loadedProject)}, ${this.agentProfileLabel(loadedProfile)} (${this.loadedPath}) will apply.`;
84 - },
85 -
86 - configs: [],
87 - isListingConfigs: false,
88 - configsError: null,
89 -
90 - async openConfigListModal() {
91 - await window.openModal?.("/components/plugins/plugin-configs.html");
69 + async loadAgentProfiles() {
70 + try {
71 + const response = await fetchApi("/agents", {
72 + method: "POST",
73 + headers: { "Content-Type": "application/json" },
74 + body: JSON.stringify({ action: "list" }),
75 + });
76 + const data = await response.json().catch(() => ({}));
77 + this.agentProfiles = data.ok ? (data.data || []) : [];
78 + } catch {
79 + this.agentProfiles = [];
80 + }
81 },
82
94 - async loadConfigList() {
83 + async loadConfigs() {
84 if (!this.pluginName) return;
96 - this.isListingConfigs = true;
97 - this.configsError = null;
85 + this.isLoading = true;
86 try {
87 const response = await fetchApi("/plugins", {
88 method: "POST",
@@ -102,217 +90,156 @@ const model = {
90 body: JSON.stringify({
91 action: "list_configs",
92 plugin_name: this.pluginName,
93 + asset_type: "toggle"
94 }),
95 });
96 const result = await response.json().catch(() => ({}));
97 this.configs = result.ok ? (result.data || []) : [];
109 - if (!result.ok) this.configsError = result.error || "Failed to load configurations";
98 + this.calculateStatus();
99 } catch (e) {
111 - this.configsError = e?.message || "Failed to load configurations";
112 - this.configs = [];
100 + this.error = e?.message || "Failed to load configurations";
101 } finally {
114 - this.isListingConfigs = false;
102 + this.isLoading = false;
103 }
104 },
105
118 - async switchToConfig(projectName, agentProfile) {
119 - if (!this.confirmDiscardUnsavedChanges()) return;
120 - this.projectName = projectName || "";
121 - this.agentProfileKey = agentProfile || "";
122 - await this.loadSettings();
123 - await window.closeModal?.();
124 - },
125 -
126 - async deleteConfig(projectName, agentProfile) {
106 + async openConfigWithScope() {
107 if (!this.pluginName) return;
128 - try {
129 - const cfg = (this.configs || []).find(
130 - (c) => (c?.project_name || "") === (projectName || "") && (c?.agent_profile || "") === (agentProfile || "")
131 - );
132 - const path = cfg?.path || "";
133 - if (!path) {
134 - this.configsError = "Configuration path not found";
135 - return;
136 - }
108
138 - const response = await fetchApi("/plugins", {
139 - method: "POST",
140 - headers: { "Content-Type": "application/json" },
141 - body: JSON.stringify({
142 - action: "delete_config",
143 - plugin_name: this.pluginName,
144 - path,
145 - }),
109 + if (settingsStore.pluginName !== this.pluginName) {
110 + // Different plugin — full init with current scope
111 + await settingsStore.open(this.pluginName, {
112 + projectName: this.projectName || "",
113 + agentProfileKey: this.agentProfileKey || "",
114 });
147 - const result = await response.json().catch(() => ({}));
148 - if (!result.ok) {
149 - this.configsError = result.error || "Delete failed";
150 - return;
151 - }
152 -
153 - this.configsError = null;
154 - await this.loadConfigList();
155 - } catch (e) {
156 - this.configsError = e?.message || "Delete failed";
115 + } else {
116 + // Same plugin — push current scope explicitly.
117 + // onScopeChanged() syncs on @change events, but if the user never touched
118 + // the scope selectors after the modal opened the settings store may still
119 + // hold a stale scope from a prior session.
120 + settingsStore.projectName = this.projectName || "";
121 + settingsStore.agentProfileKey = this.agentProfileKey || "";
122 }
123 + await window.openModal?.("components/plugins/plugin-settings.html");
124 },
125
160 - // 'plugin' = save to plugin settings API
161 - // 'core' = save via $store.settings.saveSettings() (for plugins that surface core settings)
162 - saveMode: 'plugin',
163 -
164 - isLoading: false,
165 - isSaving: false,
166 - error: null,
167 -
168 - // Called by the subsection button before openModal()
169 - async open(pluginName) {
170 - this.pluginName = pluginName;
171 - this.pluginMeta = null;
172 - this.settings = {};
173 - this.settingsSnapshotJson = "";
174 - this.error = null;
175 - this.saveMode = 'plugin';
176 - this.projectName = "";
177 - this.agentProfileKey = "";
178 - this.previousProjectName = "";
179 - this.previousAgentProfileKey = "";
180 - this.loadedPath = "";
181 - this.loadedProjectName = "";
182 - this.loadedAgentProfile = "";
183 - await Promise.all([this.loadProjects(), this.loadAgentProfiles()]);
184 - await this.loadSettings();
185 - },
186 -
187 - // Called by x-create inside the modal on every open
188 - async onModalOpen() {
189 - if (this.pluginName) await this.loadSettings();
126 + async openConfigListModal() {
127 + await window.openModal?.("/components/plugins/toggle/plugin-toggles.html");
128 },
129
192 - async loadAgentProfiles() {
193 - try {
194 - const response = await fetchApi("/agents", {
195 - method: "POST",
196 - headers: { "Content-Type": "application/json" },
197 - body: JSON.stringify({ action: "list" }),
198 - });
199 - const data = await response.json().catch(() => ({}));
200 - this.agentProfiles = data.ok ? (data.data || []) : [];
201 - } catch {
202 - this.agentProfiles = [];
203 - }
130 + async loadConfigList() {
131 + await this.loadConfigs();
132 },
133
206 - async loadProjects() {
207 - try {
208 - const response = await fetchApi("/projects", {
209 - method: "POST",
210 - headers: { "Content-Type": "application/json" },
211 - body: JSON.stringify({ action: "list_options" }),
212 - });
213 - const data = await response.json().catch(() => ({}));
214 - this.projects = data.ok ? (data.data || []) : [];
215 - } catch {
216 - this.projects = [];
217 - }
134 + async switchToConfig(projectName, agentProfile) {
135 + this.projectName = projectName || "";
136 + this.agentProfileKey = agentProfile || "";
137 + this.onScopeChanged();
138 + await window.closeModal?.();
139 },
140
220 - async loadSettings() {
221 - if (!this.pluginName) return;
141 + async deleteConfig(path) {
142 + if (!this.pluginName || !path) return;
143 this.isLoading = true;
223 - this.error = null;
144 try {
145 const response = await fetchApi("/plugins", {
146 method: "POST",
147 headers: { "Content-Type": "application/json" },
148 body: JSON.stringify({
229 - action: "get_config",
149 + action: "delete_config",
150 plugin_name: this.pluginName,
231 - project_name: this.projectName || "",
232 - agent_profile: this.agentProfileKey || "",
151 + path: path
152 }),
153 });
235 - const result = await response.json().catch(() => ({}));
236 - this.settings = result.ok ? (result.data || {}) : {};
237 - this.loadedPath = result.loaded_path || "";
238 - this.loadedProjectName = result.loaded_project_name || "";
239 - this.loadedAgentProfile = result.loaded_agent_profile || "";
240 - if (!result.ok) this.error = result.error || "Failed to load settings";
154 + const result = await response.json();
155 + if (!result.ok) throw new Error(result.error);
156 + await this.loadConfigs();
157 } catch (e) {
242 - this.error = e?.message || "Failed to load settings";
243 - this.settings = {};
158 + this.error = e.message || "Delete failed";
159 } finally {
245 - this.settingsSnapshotJson = this._toComparableJson(this.settings);
246 - this.previousProjectName = this.projectName || "";
247 - this.previousAgentProfileKey = this.agentProfileKey || "";
160 this.isLoading = false;
161 }
162 },
163
252 - async save() {
253 - if (!this.pluginName) return;
254 -
255 - // Core-backed plugins (e.g. memory) delegate to the settings store
256 - if (this.saveMode === 'core') {
257 - const coreStore = Alpine.store('settings');
258 - if (coreStore?.saveSettings) {
259 - const ok = await coreStore.saveSettings();
260 - if (ok) window.closeModal?.();
261 - }
164 + calculateStatus() {
165 + if (this.alwaysEnabled) {
166 + this.explicitPath = null;
167 + this.status = 'enabled';
168 return;
169 }
170
265 - // Plugin-specific settings: persist to plugin settings API
171 + const p = this.projectName || "";
172 + const a = this.agentProfileKey || "";
173 + const explicit = this.configs.find(
174 + c => (c.project_name||"") === p && (c.agent_profile||"") === a
175 + );
176 +
177 + if (explicit) {
178 + this.explicitPath = explicit.path;
179 + this.status = explicit.path.endsWith(".toggle-1") ? 'enabled' : 'disabled';
180 + } else {
181 + this.explicitPath = null;
182 + this.status = 'enabled'; // default when no explicit config
183 + }
184 + },
185 +
186 + async setEnabled(enabled) {
187 + if (!this.pluginName || this.alwaysEnabled) return;
188 this.isSaving = true;
267 - this.error = null;
189 try {
190 const response = await fetchApi("/plugins", {
191 method: "POST",
192 headers: { "Content-Type": "application/json" },
193 body: JSON.stringify({
273 - action: "save_config",
194 + action: "toggle_plugin",
195 plugin_name: this.pluginName,
196 project_name: this.projectName || "",
197 agent_profile: this.agentProfileKey || "",
277 - settings: this.settings,
198 + enabled: enabled
199 }),
200 });
280 - const result = await response.json().catch(() => ({}));
281 - if (!result.ok) this.error = result.error || "Save failed";
282 - else {
283 - this.settingsSnapshotJson = this._toComparableJson(this.settings);
284 - window.closeModal?.();
285 - }
201 + const result = await response.json();
202 + if (!result.ok) throw new Error(result.error);
203 + await new Promise(r => setTimeout(r, 100));
204 + await this.loadConfigs();
205 } catch (e) {
287 - this.error = e?.message || "Save failed";
206 + this.error = e.message || "Failed to save";
207 } finally {
208 this.isSaving = false;
209 }
210 },
211
293 - cleanup() {
294 - this.pluginName = null;
295 - this.pluginMeta = null;
296 - this.settings = {};
297 - this.settingsSnapshotJson = "";
298 - this.previousProjectName = "";
299 - this.previousAgentProfileKey = "";
300 - this.loadedPath = "";
301 - this.loadedProjectName = "";
302 - this.loadedAgentProfile = "";
303 - this.error = null;
304 - this.isLoading = false;
305 - this.isSaving = false;
306 - this.isListingConfigs = false;
307 - this.configsError = null;
308 - this.configs = [];
212 + async onScopeChanged() {
213 + this.calculateStatus();
214 +
215 + // Sync scope with settings store so its loadSettings picks up the right context
216 + settingsStore.projectName = this.projectName || "";
217 + settingsStore.agentProfileKey = this.agentProfileKey || "";
218 + await settingsStore.loadSettings();
219 + },
220 +
221 + projectLabel(key) {
222 + if (!key) return "Global";
223 + const found = (this.projects || []).find(p => p.key === key);
224 + return found?.label || key;
225 + },
226 +
227 + agentProfileLabel(key) {
228 + if (!key) return "All profiles";
229 + const found = (this.agentProfiles || []).find(p => p.key === key);
230 + return found?.label || key;
231 },
232
311 - // Reactive URL for the plugin's settings component (used with x-html injection)
312 - get settingsComponentHtml() {
313 - if (!this.pluginName) return "";
314 - return `<x-component path="/plugins/${this.pluginName}/webui/config.html"></x-component>`;
233 + get statusLabel() {
234 + return this.status === 'enabled' ? 'ON' : 'OFF';
235 },
236 +
237 + get noScopeRuleMessage() {
238 + if (this.alwaysEnabled || this.isLoading) return "";
239 + if (this.configs.length === 0) return "No activation rules configured. Plugin defaults to ON.";
240 + if (!this.explicitPath) return "No rule set for this scope. Status defaults to ON.";
241 + return "";
242 + }
243 };
244
318 -export const store = createStore("pluginSettings", model);
245 +export const store = createStore("pluginToggle", model);
webui/components/plugins/toggle/plugin-toggles.html
+33 -21
@@ -1,52 +1,56 @@
1 <html>
2 <head>
3 - <title>Existing plugin configurations</title>
3 + <title>Plugin Switch Configuration</title>
4 <script type="module">
5 - import { store } from "/components/plugins/plugin-settings-store.js";
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.pluginSettings">
11 - <div x-create="$store.pluginSettings.loadConfigList()" class="plugin-configs-container">
10 + <template x-if="$store.pluginToggle">
11 + <div x-create="$store.pluginToggle.loadConfigs()" class="plugin-configs-container">
12
13 - <div x-show="$store.pluginSettings.configsError" class="plugin-configs-error">
13 + <div x-show="$store.pluginToggle.error" class="plugin-configs-error">
14 <span class="material-symbols-outlined">error</span>
15 - <span x-text="$store.pluginSettings.configsError"></span>
15 + <span x-text="$store.pluginToggle.error"></span>
16 </div>
17
18 - <div x-show="$store.pluginSettings.isListingConfigs" class="plugin-configs-loading">
18 + <div x-show="$store.pluginToggle.isLoading" class="plugin-configs-loading">
19 <span class="material-symbols-outlined spinning">progress_activity</span>
20 <span>Loading configurations...</span>
21 </div>
22
23 - <div x-show="!$store.pluginSettings.isListingConfigs" class="plugin-configs-list">
24 - <template x-if="($store.pluginSettings.configs || []).length === 0">
25 - <div class="plugin-configs-empty">No configurations found.</div>
23 + <div x-show="!$store.pluginToggle.isLoading" class="plugin-configs-list">
24 + <template x-if="($store.pluginToggle.configs || []).length === 0">
25 + <div class="plugin-configs-empty">No status configurations found.</div>
26 </template>
27
28 - <template x-for="cfg in ($store.pluginSettings.configs || [])" :key="(cfg.project_name || '') + '|' + (cfg.agent_profile || '')">
28 + <template x-for="cfg in ($store.pluginToggle.configs || [])" :key="(cfg.project_name || '') + '|' + (cfg.agent_profile || '')">
29 <div class="plugin-configs-row">
30 <div class="plugin-configs-scope">
31 <div class="plugin-configs-scope-top">
32 <div class="plugin-configs-scope-line">
33 <span class="plugin-configs-scope-key">Project:</span>
34 - <span x-text="$store.pluginSettings.projectLabel(cfg.project_name || '')"></span>
34 + <span x-text="$store.pluginToggle.projectLabel(cfg.project_name || '')"></span>
35 </div>
36 <div class="plugin-configs-scope-line">
37 <span class="plugin-configs-scope-key">Agent profile:</span>
38 - <span x-text="$store.pluginSettings.agentProfileLabel(cfg.agent_profile || '')"></span>
38 + <span x-text="$store.pluginToggle.agentProfileLabel(cfg.agent_profile || '')"></span>
39 </div>
40 </div>
41 - <div class="plugin-configs-scope-sub" x-text="cfg.path || ''"></div>
41 + <div class="plugin-configs-scope-sub">
42 + <span x-text="cfg.path.endsWith('.toggle-1') ? 'ON' : 'OFF'"
43 + :class="cfg.path.endsWith('.toggle-1') ? 'text-success' : 'text-error'"></span>
44 + <span class="path-detail" x-text="cfg.path"></span>
45 + </div>
46 </div>
47
48 <div class="plugin-configs-actions">
45 - <button type="button" class="button" @click="$store.pluginSettings.switchToConfig(cfg.project_name || '', cfg.agent_profile || '')">
46 - <span class="icon material-symbols-outlined">list</span>
47 - Show
49 + <button type="button" class="button" @click="$store.pluginToggle.switchToConfig(cfg.project_name || '', cfg.agent_profile || '')">
50 + <span class="icon material-symbols-outlined">edit</span>
51 + Edit
52 </button>
49 - <button type="button" class="button cancel icon-button" title="Delete" @click="$confirmClick($event, () => $store.pluginSettings.deleteConfig(cfg.project_name || '', cfg.agent_profile || ''))">
53 + <button type="button" class="button cancel icon-button" title="Delete" @click="$confirmClick($event, () => $store.pluginToggle.deleteConfig(cfg.path))">
54 <span class="icon material-symbols-outlined">delete</span>
55 </button>
56 </div>
@@ -125,10 +129,18 @@
129 font-size: var(--font-size-small);
130 color: var(--color-text-secondary);
131 margin-top: 0.2rem;
128 - white-space: normal;
129 - overflow-wrap: break-word;
130 - word-break: normal;
132 + display: flex;
133 + gap: 0.5rem;
134 + align-items: center;
135 + }
136 +
137 + .path-detail {
138 + opacity: 0.5;
139 + font-size: 0.8em;
140 }
141 +
142 + .text-success { color: var(--color-success, #2ecc71); font-weight: 600; }
143 + .text-error { color: var(--color-error, #e74c3c); font-weight: 600; }
144
145 .plugin-configs-actions {
146 display: flex;