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;