WIP: Add plugin toggle UI and helper stubs

Remove a temporary test extension and add UI and Python support for plugin toggles/settings. Highlights: - Deleted plugins/memory/extensions/webui/.../testingext.js (temporary test code removed). - API: python/api/plugins.py: add asset_type input to list_configs and route to toggle asset pattern when requested. - Helpers: python/helpers/plugins.py: add typing for toggle state, TOGGLE_FILE_PATTERN, always_enabled metadata flag, stub get_toggle_state/toggle_plugin, and small refactor in get_enabled_plugins to import subagents only when needed. - Subagents: python/helpers/subagents.py: iterate over get_plugins_list() and adjust path usage. - Web UI: add new components under webui/components/plugins/toggle: plugin-toggle-advanced.html (plugin settings modal), plugin-toggle-store.js (Alpine store for plugin settings), and plugin-toggles.html (list of existing plugin configurations). These changes add the front-end modal and store for per-project/agent plugin settings and introduce backend patterns/constants and stubs to support toggles (get_toggle_state/toggle_plugin) and listing toggle assets. Further implementation is needed to persist toggle state and fully integrate TOGGLE_FILE_PATTERN handling.

frdel committed Feb 23, 2026 at 09:39 UTC c41dfbaa66c8c72636e238b525995a590542b57d
7 files changed +694 -11
plugins/memory/extensions/webui/set_messages_before_loop/testingext.js deleted
-4
@@ -1,4 +0,0 @@
1 -
2 -export default function extension(context){
3 - console.log("set_messages_before_loop extension called - textingext.js - REMOVE ME", context);
4 -}
\ No newline at end of file
python/api/plugins.py
+2 -1
@@ -53,11 +53,12 @@ class Plugins(ApiHandler):
53
54 if action == "list_configs":
55 plugin_name = input.get("plugin_name", "")
56 + asset_type = input.get("asset_type", "config")
57 if not plugin_name:
58 return Response(status=400, response="Missing plugin_name")
59
60 configs = plugins.find_plugin_assets(
60 - plugins.CONFIG_FILE_NAME,
61 + plugins.CONFIG_FILE_NAME if asset_type == "config" else plugins.TOGGLE_FILE_PATTERN,
62 plugin_name=plugin_name,
63 project_name="*",
64 agent_profile="*",
python/helpers/plugins.py
+16 -4
@@ -2,7 +2,7 @@ from __future__ import annotations
2
3 import re, json
4 from pathlib import Path
5 -from typing import Any, Dict, List, Optional, TYPE_CHECKING
5 +from typing import Any, Dict, List, Literal, Optional, TYPE_CHECKING
6
7 from python.helpers import files, print_style
8 from pydantic import BaseModel, Field
@@ -16,11 +16,15 @@ _META_TARGET_RE = re.compile(
16 re.IGNORECASE,
17 )
18
19 +type ToggleState = Literal["enabled", "disabled", "advanced"]
20 +
21 +
22 META_FILE_NAME = "plugin.json"
23 CONFIG_FILE_NAME = "config.json"
24 CONFIG_DEFAULT_FILE_NAME = "config.default.json"
25 DISABLED_FILE_NAME = ".disabled"
26 ENABLED_FILE_NAME = ".enabled"
27 +TOGGLE_FILE_PATTERN = ".*abled"
28
29
30 class PluginMetadata(BaseModel):
@@ -30,6 +34,7 @@ class PluginMetadata(BaseModel):
34 settings_sections: List[str] = Field(default_factory=list)
35 per_project_config: bool = False
36 per_agent_config: bool = False
37 + always_enabled: bool = False
38
39
40 class PluginListItem(BaseModel):
@@ -86,6 +91,7 @@ def get_enhanced_plugins_list(
91 )
92 has_main_screen = files.exists(str(d / "webui" / "main.html"))
93 has_config_screen = files.exists(str(d / "webui" / "config.html"))
94 + toggle_state = get_toggle_state(meta.name)
95 results.append(
96 PluginListItem(
97 name=d.name,
@@ -167,15 +173,13 @@ def get_enabled_plugins(agent: Agent | None):
173 plugins = get_plugins_list()
174 active = []
175
170 - if agent:
171 - from python.helpers import subagents
172 -
176 for plugin in plugins:
177 # plugins are toggled via .enabled / .disabled files
178 # every plugin is on by default, unless disabled in usr dir
179 enabled = True
180
181 if agent:
182 + from python.helpers import subagents
183 agent_paths = subagents.get_paths(
184 agent,
185 files.PLUGINS_DIR,
@@ -204,6 +208,14 @@ def get_enabled_plugins(agent: Agent | None):
208 return active
209
210
211 +def get_toggle_state(plugin_name: str) -> ToggleState:
212 + return "enabled"
213 +
214 +
215 +def toggle_plugin(plugin_name: str, enabled: bool, project_name: str = "", agent_profile: str = ""):
216 + pass
217 +
218 +
219 def get_webui_extensions(extension_point: str, filters: List[str] | None = None):
220 entries: List[str] = []
221 effective_filters = filters or ["*"]
python/helpers/subagents.py
+2 -2
@@ -388,8 +388,8 @@ def get_paths(
388 # plugins/*/subpaths...
389 from python.helpers import plugins
390
391 - for plugin in plugins.get_enhanced_plugins_list():
392 - path = files.get_abs_path(str(plugin.path), *subpaths)
391 + for plugin in plugins.get_plugins_list():
392 + path = files.get_abs_path(plugin, *subpaths)
393 if (not must_exist_completely) or files.exists(path):
394 if path not in paths:
395 paths.append(path)
webui/components/plugins/toggle/plugin-toggle-advanced.html new
+208
@@ -0,0 +1,208 @@
1 +<html>
2 +<head>
3 + <title>Plugin Settings</title>
4 + <script type="module">
5 + import { store } from "/components/plugins/plugin-settings-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>
57 +
58 + <!-- Error -->
59 + <div x-show="$store.pluginSettings.error" class="plugin-settings-error">
60 + <span class="material-symbols-outlined">error</span>
61 + <span x-text="$store.pluginSettings.error"></span>
62 + </div>
63 +
64 + <!-- Loading -->
65 + <div x-show="$store.pluginSettings.isLoading" class="plugin-settings-loading">
66 + <span class="material-symbols-outlined spinning">progress_activity</span>
67 + <span>Loading settings...</span>
68 + </div>
69 +
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">
74 + </div>
75 +
76 + </div>
77 + </template>
78 + </div>
79 +
80 + <!-- Footer (pinned outside scroll area) -->
81 + <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
86 + </button>
87 + <button class="btn btn-cancel"
88 + @click="window.closeModal?.()">
89 + Cancel
90 + </button>
91 + </div>
92 +
93 + <style>
94 + .plugin-settings-toolbar, .plugin-settings-body {
95 + padding: 1rem;
96 + }
97 +
98 + .plugin-settings-toolbar-row {
99 + display: flex;
100 + align-items: center;
101 + gap: clamp(0.75rem, 2vw, 2rem);
102 + flex-wrap: wrap;
103 + }
104 +
105 + .plugin-settings-toolbar-item {
106 + display: flex;
107 + align-items: center;
108 + gap: 0.5rem;
109 + flex: 1 1 0;
110 + min-width: 12rem;
111 + margin: 0;
112 + }
113 +
114 + .plugin-settings-toolbar-button {
115 + flex: 0 0 auto;
116 + padding: 0.5rem 0.75rem;
117 + height: 2.5rem;
118 + }
119 +
120 + .plugin-settings-toolbar-label {
121 + font-weight: 600;
122 + color: var(--color-text-secondary);
123 + white-space: nowrap;
124 + }
125 +
126 + .plugin-settings-toolbar-item select {
127 + flex: 1 1 auto;
128 + min-width: 0;
129 + }
130 +
131 + .plugin-settings-scope-info {
132 + display: flex;
133 + align-items: flex-start;
134 + gap: 0.5rem;
135 + padding: 0 1rem 1rem 1rem;
136 + color: var(--color-text-secondary);
137 + font-size: var(--font-size-small);
138 + }
139 +
140 + @media (max-width: 640px) {
141 + .plugin-settings-toolbar-item {
142 + flex-basis: 100%;
143 + min-width: 0;
144 + }
145 + }
146 +
147 + .plugin-settings-error {
148 + display: flex;
149 + align-items: center;
150 + gap: 0.5rem;
151 + color: var(--color-error, #e74c3c);
152 + background: var(--color-error-bg, #fdecea);
153 + border-radius: 4px;
154 + padding: 0.5rem 0.75rem;
155 + margin-bottom: 0.75rem;
156 + font-size: var(--font-size-small);
157 + }
158 +
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 +
172 + .spinning {
173 + animation: spin 1s linear infinite;
174 + }
175 +
176 + @keyframes spin {
177 + from { transform: rotate(0deg); }
178 + to { transform: rotate(360deg); }
179 + }
180 +
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);
193 + }
194 +
195 + .plugin-settings-scope-title {
196 + font-weight: 600;
197 + font-size: var(--font-size-normal);
198 + 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;
205 + }
206 + </style>
207 +</body>
208 +</html>
webui/components/plugins/toggle/plugin-toggle-store.js new
+318
@@ -0,0 +1,318 @@
1 +import { createStore } from "/js/AlpineStore.js";
2 +
3 +const fetchApi = globalThis.fetchApi;
4 +
5 +const model = {
6 + // which plugin this modal is showing
7 + pluginName: null,
8 + pluginMeta: null,
9 +
10 + // context selectors (mirrors skills list pattern)
11 + projects: [],
12 + agentProfiles: [],
13 + projectName: "",
14 + agentProfileKey: "",
15 +
16 + // plugin settings data (plugins bind their fields here)
17 + settings: {},
18 +
19 + settingsSnapshotJson: "",
20 + previousProjectName: "",
21 + previousAgentProfileKey: "",
22 +
23 + _toComparableJson(value) {
24 + try {
25 + return JSON.stringify(value ?? {});
26 + } catch {
27 + return "";
28 + }
29 + },
30 +
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?");
38 + },
39 +
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;
52 + }
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;
72 + },
73 +
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");
92 + },
93 +
94 + async loadConfigList() {
95 + if (!this.pluginName) return;
96 + this.isListingConfigs = true;
97 + this.configsError = null;
98 + try {
99 + const response = await fetchApi("/plugins", {
100 + method: "POST",
101 + headers: { "Content-Type": "application/json" },
102 + body: JSON.stringify({
103 + action: "list_configs",
104 + plugin_name: this.pluginName,
105 + }),
106 + });
107 + const result = await response.json().catch(() => ({}));
108 + this.configs = result.ok ? (result.data || []) : [];
109 + if (!result.ok) this.configsError = result.error || "Failed to load configurations";
110 + } catch (e) {
111 + this.configsError = e?.message || "Failed to load configurations";
112 + this.configs = [];
113 + } finally {
114 + this.isListingConfigs = false;
115 + }
116 + },
117 +
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) {
127 + 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 + }
137 +
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 + }),
146 + });
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";
157 + }
158 + },
159 +
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();
190 + },
191 +
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 + }
204 + },
205 +
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 + }
218 + },
219 +
220 + async loadSettings() {
221 + if (!this.pluginName) return;
222 + this.isLoading = true;
223 + this.error = null;
224 + try {
225 + const response = await fetchApi("/plugins", {
226 + method: "POST",
227 + headers: { "Content-Type": "application/json" },
228 + body: JSON.stringify({
229 + action: "get_config",
230 + plugin_name: this.pluginName,
231 + project_name: this.projectName || "",
232 + agent_profile: this.agentProfileKey || "",
233 + }),
234 + });
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";
241 + } catch (e) {
242 + this.error = e?.message || "Failed to load settings";
243 + this.settings = {};
244 + } finally {
245 + this.settingsSnapshotJson = this._toComparableJson(this.settings);
246 + this.previousProjectName = this.projectName || "";
247 + this.previousAgentProfileKey = this.agentProfileKey || "";
248 + this.isLoading = false;
249 + }
250 + },
251 +
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 + }
262 + return;
263 + }
264 +
265 + // Plugin-specific settings: persist to plugin settings API
266 + this.isSaving = true;
267 + this.error = null;
268 + try {
269 + const response = await fetchApi("/plugins", {
270 + method: "POST",
271 + headers: { "Content-Type": "application/json" },
272 + body: JSON.stringify({
273 + action: "save_config",
274 + plugin_name: this.pluginName,
275 + project_name: this.projectName || "",
276 + agent_profile: this.agentProfileKey || "",
277 + settings: this.settings,
278 + }),
279 + });
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 + }
286 + } catch (e) {
287 + this.error = e?.message || "Save failed";
288 + } finally {
289 + this.isSaving = false;
290 + }
291 + },
292 +
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 = [];
309 + },
310 +
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>`;
315 + },
316 +};
317 +
318 +export const store = createStore("pluginSettings", model);
webui/components/plugins/toggle/plugin-toggles.html new
+148
@@ -0,0 +1,148 @@
1 +<html>
2 +<head>
3 + <title>Existing plugin configurations</title>
4 + <script type="module">
5 + import { store } from "/components/plugins/plugin-settings-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">
12 +
13 + <div x-show="$store.pluginSettings.configsError" class="plugin-configs-error">
14 + <span class="material-symbols-outlined">error</span>
15 + <span x-text="$store.pluginSettings.configsError"></span>
16 + </div>
17 +
18 + <div x-show="$store.pluginSettings.isListingConfigs" 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>
26 + </template>
27 +
28 + <template x-for="cfg in ($store.pluginSettings.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>
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>
39 + </div>
40 + </div>
41 + <div class="plugin-configs-scope-sub" x-text="cfg.path || ''"></div>
42 + </div>
43 +
44 + <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
48 + </button>
49 + <button type="button" class="button cancel icon-button" title="Delete" @click="$confirmClick($event, () => $store.pluginSettings.deleteConfig(cfg.project_name || '', cfg.agent_profile || ''))">
50 + <span class="icon material-symbols-outlined">delete</span>
51 + </button>
52 + </div>
53 + </div>
54 + </template>
55 + </div>
56 +
57 + </div>
58 + </template>
59 + </div>
60 +
61 + <style>
62 + .plugin-configs-loading,
63 + .plugin-configs-error {
64 + display: flex;
65 + align-items: center;
66 + gap: 0.5rem;
67 + padding: 0.5rem 0.75rem;
68 + margin-bottom: 0.75rem;
69 + font-size: var(--font-size-small);
70 + }
71 +
72 + .plugin-configs-container {
73 + padding: 0.75rem;
74 + }
75 +
76 + .plugin-configs-error {
77 + color: var(--color-error);
78 + background: rgba(255, 0, 0, 0.07);
79 + border: 1px solid rgba(255, 0, 0, 0.2);
80 + border-radius: 4px;
81 + }
82 +
83 + .plugin-configs-list {
84 + display: flex;
85 + flex-direction: column;
86 + gap: 0.5rem;
87 + }
88 +
89 + .plugin-configs-row {
90 + display: flex;
91 + align-items: center;
92 + justify-content: space-between;
93 + gap: 0.75rem;
94 + padding: 0.75rem;
95 + border: 1px solid var(--color-border);
96 + border-radius: 6px;
97 + background: var(--color-surface);
98 + }
99 +
100 + .plugin-configs-scope {
101 + flex: 1 1 auto;
102 + min-width: 0;
103 + }
104 +
105 + .plugin-configs-scope-top {
106 + display: flex;
107 + gap: 1rem;
108 + flex-wrap: wrap;
109 + align-items: baseline;
110 + }
111 +
112 + .plugin-configs-scope-line {
113 + display: flex;
114 + gap: 0.35rem;
115 + align-items: baseline;
116 + }
117 +
118 + .plugin-configs-scope-key {
119 + font-weight: 600;
120 + color: var(--color-text-secondary);
121 + white-space: nowrap;
122 + }
123 +
124 + .plugin-configs-scope-sub {
125 + font-size: var(--font-size-small);
126 + color: var(--color-text-secondary);
127 + margin-top: 0.2rem;
128 + white-space: normal;
129 + overflow-wrap: break-word;
130 + word-break: normal;
131 + }
132 +
133 + .plugin-configs-actions {
134 + display: flex;
135 + gap: 0.5rem;
136 + flex-wrap: nowrap;
137 + justify-content: flex-end;
138 + flex: 0 0 auto;
139 + }
140 +
141 + .plugin-configs-empty {
142 + padding: 0.75rem;
143 + color: var(--color-text-secondary);
144 + font-size: var(--font-size-small);
145 + }
146 + </style>
147 +</body>
148 +</html>