cleanup plugin installer and store wiring

Align plugin installer and plugin settings UI with the frontend component/store patterns. - compose plugin_installer main.html from install-index/install-git/install-zip components instead of duplicating tab markup - restore shared plugin window CSS in main.html while keeping tab content delegated to dedicated components - replace remaining direct cross-store/template calls with store methods and module imports - route installer screenshot opening through pluginInstallStore and rewire plugin init opening through pluginListStore - extract settings/plugins subsection logic from inline x-data into a dedicated store-backed context - remove touched plugin/settings Alpine.store lookups in favor of explicit store imports - fix advanced toggle behavior so the switch can create a rule immediately for the selected scope - restore installer success/result state used by the git/zip tab UIs

Alessandro committed Mar 10, 2026 at 04:54 UTC 4515502cea29823728c929cdd791db68cd566899
12 files changed +158 -346
plugins/plugin_installer/webui/install-detail.html
+1 -1
@@ -97,7 +97,7 @@
97 <img class="pi-screenshot-thumb"
98 :src="url"
99 :alt="$store.pluginInstallStore.selectedPlugin.title + ' screenshot ' + (i + 1)"
100 - @click="$store.imageViewer.open(url, { name: $store.pluginInstallStore.selectedPlugin.title })">
100 + @click="$store.pluginInstallStore.openScreenshot(url)">
101 </template>
102 </div>
103 </div>
plugins/plugin_installer/webui/install-git.html
+1 -2
@@ -8,8 +8,7 @@
8 <body>
9 <div x-data>
10 <template x-if="$store.pluginInstallStore">
11 - <div x-init="$store.pluginInstallStore.resetGit()"
12 - x-destroy="$store.pluginInstallStore.refreshPluginList()">
11 + <div x-create="$store.pluginInstallStore.resetGit()">
12
13 <div class="pi-form-group">
14 <label class="pi-label">Git Repository URL</label>
plugins/plugin_installer/webui/install-index.html
+1 -2
@@ -9,8 +9,7 @@
9 <div x-data>
10 <template x-if="$store.pluginInstallStore">
11 <div class="pi-browse-shell"
12 - x-init="$store.pluginInstallStore.resetIndex(); $store.pluginInstallStore.fetchIndex()"
13 - x-destroy="$store.pluginInstallStore.refreshPluginList()">
12 + x-create="$store.pluginInstallStore.resetIndex(); $store.pluginInstallStore.fetchIndex()">
13
14 <section class="pi-browse-hero">
15 <div class="pi-browse-copy">
plugins/plugin_installer/webui/install-zip.html
+1 -2
@@ -8,8 +8,7 @@
8 <body>
9 <div x-data>
10 <template x-if="$store.pluginInstallStore">
11 - <div x-init="$store.pluginInstallStore.resetZip()"
12 - x-destroy="$store.pluginInstallStore.refreshPluginList()">
11 + <div x-create="$store.pluginInstallStore.resetZip()">
12
13 <div class="pi-upload-section">
14 <label for="plugin-zip-file" class="pi-upload-btn button confirm"
plugins/plugin_installer/webui/main.html
+13 -240
@@ -10,7 +10,7 @@
10 <body>
11 <div x-data>
12 <template x-if="$store.pluginInstallStore">
13 - <div x-init="$store.pluginInstallStore.setTab('store')"
13 + <div x-create="$store.pluginInstallStore.setTab('store')"
14 x-destroy="$store.pluginInstallStore.refreshPluginList()">
15
16 <ul class="nav nav-tabs" role="tablist">
@@ -34,244 +34,17 @@
34 </li>
35 </ul>
36
37 - <!-- ═══ Store Tab ═══ -->
38 - <div x-show="$store.pluginInstallStore.activeTab === 'store'"
39 - x-init="$store.pluginInstallStore.resetIndex(); $store.pluginInstallStore.fetchIndex()">
40 -
41 - <div class="pi-browse-shell">
42 - <section class="pi-browse-hero">
43 - <div class="pi-browse-copy">
44 - <div class="pi-browse-eyebrow">Community Marketplace</div>
45 - <h2 class="pi-browse-title">Discover community plugins for Agent Zero</h2>
46 - <p class="pi-browse-description">
47 - These plugins are provided by the community of Agent Zero. Contribute by publishing
48 - in the <a href="https://github.com/agent0ai/a0-plugins" target="_blank">index
49 - repository</a>.
50 - </p>
51 - </div>
52 - <div class="pi-browse-summary" x-text="$store.pluginInstallStore.browseResultsSummary">
53 - </div>
54 - </section>
55 -
56 - <div class="pi-index-toolbar">
57 - <label class="pi-search-field">
58 - <span class="material-symbols-outlined">search</span>
59 - <input type="text" class="pi-search-input" x-model="$store.pluginInstallStore.search"
60 - @input="$store.pluginInstallStore.page = 1"
61 - placeholder="Search plugins, authors, or tags...">
62 - </label>
63 -
64 - <label class="pi-sort-field">
65 - <span class="pi-field-label">Sort</span>
66 - <select class="pi-sort-select" x-model="$store.pluginInstallStore.sortBy">
67 - <option value="stars">Stars</option>
68 - <option value="name">Name</option>
69 - </select>
70 - </label>
71 - </div>
72 -
73 - <div class="pi-filter-row" x-show="$store.pluginInstallStore.browseFilters.length > 1">
74 - <template x-for="filter in $store.pluginInstallStore.browseFilters" :key="filter.key">
75 - <button type="button" class="pi-filter-chip"
76 - :class="{ active: $store.pluginInstallStore.browseFilter === filter.key }"
77 - @click="$store.pluginInstallStore.setBrowseFilter(filter.key)">
78 - <span x-text="filter.label"></span>
79 - <span class="pi-filter-count" x-text="filter.count"></span>
80 - </button>
81 - </template>
82 - </div>
83 -
84 - <div x-show="$store.pluginInstallStore.loading" class="pi-state-card pi-state-loading">
85 - <span class="material-symbols-outlined">progress_activity</span>
86 - <div>
87 - <div class="pi-state-title">Loading marketplace</div>
88 - <div class="pi-state-text"
89 - x-text="$store.pluginInstallStore.loadingMessage || 'Loading plugins...'"></div>
90 - </div>
91 - </div>
92 -
93 - <div x-show="$store.pluginInstallStore.error && !$store.pluginInstallStore.loading"
94 - class="pi-state-card pi-state-error">
95 - <span class="material-symbols-outlined">error</span>
96 - <div>
97 - <div class="pi-state-title">Marketplace unavailable</div>
98 - <div class="pi-state-text" x-text="$store.pluginInstallStore.error"></div>
99 - </div>
100 - </div>
101 -
102 - <div x-show="$store.pluginInstallStore.index && !$store.pluginInstallStore.loading && $store.pluginInstallStore.filteredPlugins.length > 0"
103 - class="pi-grid">
104 - <template x-for="plugin in $store.pluginInstallStore.paginatedPlugins" :key="plugin.key">
105 - <button type="button" class="pi-card"
106 - @click="$store.pluginInstallStore.openDetail(plugin)">
107 - <div class="pi-card-thumb">
108 - <template x-if="plugin.thumbnail">
109 - <img :src="plugin.thumbnail" :alt="plugin.title" loading="lazy">
110 - </template>
111 - <template x-if="!plugin.thumbnail">
112 - <span class="material-symbols-outlined pi-card-placeholder">extension</span>
113 - </template>
114 -
115 - <template x-if="plugin.installed">
116 - <span class="pi-card-installed-pill">Installed</span>
117 - </template>
118 - </div>
119 -
120 - <div class="pi-card-body">
121 - <div class="pi-card-head">
122 - <div class="pi-card-heading">
123 - <h3 class="pi-card-title" x-text="plugin.title || plugin.key"></h3>
124 - <div class="pi-card-subtitle"
125 - x-text="$store.pluginInstallStore.getBrowseSubtitle(plugin)">
126 - </div>
127 - </div>
128 - <span class="material-symbols-outlined pi-card-arrow">arrow_outward</span>
129 - </div>
130 -
131 - <div class="pi-card-desc"
132 - x-text="$store.pluginInstallStore.truncate(plugin.description, 110)">
133 - </div>
134 - </div>
135 -
136 - <div class="pi-card-footer">
137 - <span class="pi-card-stars">
138 - <span class="material-symbols-outlined">star</span>
139 - <span x-text="plugin.stars || 0"></span>
140 - </span>
141 -
142 - <template x-if="$store.pluginInstallStore.getBrowsePrimaryTag(plugin)">
143 - <span class="pi-card-tag"
144 - x-text="$store.pluginInstallStore.getBrowsePrimaryTag(plugin)">
145 - </span>
146 - </template>
147 - </div>
148 - </button>
149 - </template>
150 - </div>
151 -
152 - <div x-show="$store.pluginInstallStore.index && !$store.pluginInstallStore.loading && $store.pluginInstallStore.filteredPlugins.length === 0"
153 - class="pi-state-card pi-state-empty">
154 - <span class="material-symbols-outlined">search_off</span>
155 - <div>
156 - <div class="pi-state-title">No plugins match this view</div>
157 - <div class="pi-state-text">
158 - Try a different search or switch to another marketplace filter.
159 - </div>
160 - </div>
161 - </div>
162 -
163 - <div x-show="$store.pluginInstallStore.totalPages > 1" class="pi-pagination">
164 - <button class="button" :disabled="$store.pluginInstallStore.page <= 1"
165 - @click="$store.pluginInstallStore.setPage($store.pluginInstallStore.page - 1)">
166 - <span class="material-symbols-outlined">chevron_left</span>
167 - </button>
168 - <span class="pi-page-info"
169 - x-text="'Page ' + $store.pluginInstallStore.page + ' of ' + $store.pluginInstallStore.totalPages">
170 - </span>
171 - <button class="button"
172 - :disabled="$store.pluginInstallStore.page >= $store.pluginInstallStore.totalPages"
173 - @click="$store.pluginInstallStore.setPage($store.pluginInstallStore.page + 1)">
174 - <span class="material-symbols-outlined">chevron_right</span>
175 - </button>
176 - </div>
177 - </div>
178 - </div>
179 -
180 - <!-- ═══ Git Tab ═══ -->
181 - <div x-show="$store.pluginInstallStore.activeTab === 'git'"
182 - x-init="$store.pluginInstallStore.resetGit()">
183 -
184 - <div class="pi-form-group">
185 - <label class="pi-label">Git Repository URL</label>
186 - <input type="text" class="pi-input" x-model="$store.pluginInstallStore.gitUrl"
187 - :disabled="$store.pluginInstallStore.loading"
188 - placeholder="https://github.com/user/my-plugin.git">
189 - </div>
190 -
191 - <div class="pi-form-group">
192 - <label class="pi-label">Access Token <span class="pi-optional">(optional, for private
193 - repos)</span></label>
194 - <input type="password" class="pi-input" x-model="$store.pluginInstallStore.gitToken"
195 - :disabled="$store.pluginInstallStore.loading" placeholder="ghp_xxxx or glpat-xxxx">
196 - <div class="pi-hint">Token is used only for cloning and is not stored.</div>
197 - </div>
198 -
199 - <div class="pi-actions">
200 - <button class="button confirm" @click="$store.pluginInstallStore.installGit()"
201 - :disabled="$store.pluginInstallStore.loading || !$store.pluginInstallStore.gitUrl.trim()">
202 - <template x-if="$store.pluginInstallStore.loading">
203 - <span class="pi-button-loading">
204 - <span class="spinner"></span>
205 - <span x-text="$store.pluginInstallStore.loadingMessage || 'Cloning...'"></span>
206 - </span>
207 - </template>
208 - <template x-if="!$store.pluginInstallStore.loading">
209 - <span>
210 - <span class="icon material-symbols-outlined">terminal</span> Clone & Install
211 - </span>
212 - </template>
213 - </button>
214 - </div>
215 -
216 - <div x-show="$store.pluginInstallStore.error" class="pi-error">
217 - <span x-text="$store.pluginInstallStore.error"></span>
218 - </div>
219 -
220 - <div x-show="$store.pluginInstallStore.result" class="pi-result">
221 - <div class="pi-result-icon">
222 - <span class="material-symbols-outlined">check_circle</span>
223 - </div>
224 - <div class="pi-result-text">
225 - <strong
226 - x-text="'Plugin installed: ' + ($store.pluginInstallStore.result?.title || $store.pluginInstallStore.result?.plugin_name || '')"></strong>
227 - <div class="pi-result-path" x-text="$store.pluginInstallStore.result?.path || ''"></div>
228 - </div>
229 - </div>
230 - </div>
231 -
232 - <!-- ═══ ZIP Tab ═══ -->
233 - <div x-show="$store.pluginInstallStore.activeTab === 'zip'"
234 - x-init="$store.pluginInstallStore.resetZip()">
235 -
236 - <div class="pi-upload-section">
237 - <label for="plugin-zip-file" class="pi-upload-btn button confirm"
238 - :class="{ 'pi-has-file': $store.pluginInstallStore.zipFile }">
239 - <span class="icon material-symbols-outlined">upload_file</span>
240 - <span x-text="$store.pluginInstallStore.zipFileName || 'Select Plugin ZIP File'"></span>
241 - </label>
242 - <input type="file" id="plugin-zip-file" accept=".zip" style="display:none"
243 - @change="$store.pluginInstallStore.handleFileUpload($event)">
244 - <div class="pi-hint">
245 - The ZIP should contain a folder with a plugin.yaml file.
246 - </div>
247 - </div>
248 -
249 - <div x-show="$store.pluginInstallStore.zipFile" class="pi-actions-center">
250 - <button class="button confirm" @click="$store.pluginInstallStore.installZip()"
251 - :disabled="$store.pluginInstallStore.loading">
252 - <span class="icon material-symbols-outlined">download</span> Install Plugin
253 - </button>
254 - </div>
255 -
256 - <div x-show="$store.pluginInstallStore.loading" class="pi-loading">
257 - <span x-text="$store.pluginInstallStore.loadingMessage || 'Processing...'"></span>
258 - </div>
259 -
260 - <div x-show="$store.pluginInstallStore.error" class="pi-error">
261 - <span x-text="$store.pluginInstallStore.error"></span>
262 - </div>
263 -
264 - <div x-show="$store.pluginInstallStore.result" class="pi-result">
265 - <div class="pi-result-icon">
266 - <span class="material-symbols-outlined">check_circle</span>
267 - </div>
268 - <div class="pi-result-text">
269 - <strong
270 - x-text="'Plugin installed: ' + ($store.pluginInstallStore.result?.title || $store.pluginInstallStore.result?.plugin_name || '')"></strong>
271 - <div class="pi-result-path" x-text="$store.pluginInstallStore.result?.path || ''"></div>
272 - </div>
273 - </div>
274 - </div>
37 + <template x-if="$store.pluginInstallStore.activeTab === 'store'">
38 + <x-component path="/plugins/plugin_installer/webui/install-index.html"></x-component>
39 + </template>
40 +
41 + <template x-if="$store.pluginInstallStore.activeTab === 'git'">
42 + <x-component path="/plugins/plugin_installer/webui/install-git.html"></x-component>
43 + </template>
44 +
45 + <template x-if="$store.pluginInstallStore.activeTab === 'zip'">
46 + <x-component path="/plugins/plugin_installer/webui/install-zip.html"></x-component>
47 + </template>
48
49 </div>
50 </template>
@@ -819,4 +592,4 @@
592 </style>
593 </body>
594
822 -</html>
\ No newline at end of file
595 +</html>
plugins/plugin_installer/webui/pluginInstallStore.js
+27 -8
@@ -1,12 +1,12 @@
1 -import { createStore, getStore } from "/js/AlpineStore.js";
1 +import { createStore } from "/js/AlpineStore.js";
2 import * as api from "/js/api.js";
3 import { openModal } from "/js/modals.js";
4 import { marked } from "/vendor/marked/marked.esm.js";
5 import { toastFrontendSuccess } from "/components/notifications/notification-store.js";
6 import { showConfirmDialog } from "/js/confirmDialog.js";
7 -import {store as pluginListStore } from "/components/plugins/list/pluginListStore.js";
8 -// import {store as pluginInitStore } from "/components/plugins/list/plugin-init-store.js";
9 -// import {store as imageViewerStore } from "/components/modals/image-viewer/image-viewer-store.js";
7 +import { store as imageViewerStore } from "/components/modals/image-viewer/image-viewer-store.js";
8 +import { store as pluginListStore } from "/components/plugins/list/pluginListStore.js";
9 +import { store as pluginInitStore } from "/components/plugins/list/plugin-init-store.js";
10
11 const PLUGIN_API = "plugins/plugin_installer/plugin_install";
12 const PER_PAGE = 20;
@@ -49,6 +49,7 @@ const model = {
49 loading: false,
50 loadingMessage: "",
51 error: "",
52 + result: null,
53
54 // README state
55 readmeContent: null,
@@ -65,6 +66,7 @@ const model = {
66 setTab(tab) {
67 this.activeTab = tab;
68 this.error = "";
69 + this.result = null;
70 },
71
72 setBrowseFilter(filter) {
@@ -129,6 +131,7 @@ const model = {
131 this.zipFile = file;
132 this.zipFileName = file.name;
133 this.error = "";
134 + this.result = null;
135 },
136
137 async installZip() {
@@ -144,6 +147,7 @@ const model = {
147 this.loading = true;
148 this.loadingMessage = "Installing plugin from ZIP...";
149 this.error = "";
150 + this.result = null;
151
152 const formData = new FormData();
153 formData.append("action", "install_zip");
@@ -160,6 +164,8 @@ const model = {
164 return;
165 }
166
167 + this.result = data;
168 +
169 toastFrontendSuccess(
170 `Plugin "${data.title || data.plugin_name}" installed`,
171 "Plugin Installer"
@@ -189,6 +195,7 @@ const model = {
195 this.loading = true;
196 this.loadingMessage = "Cloning repository...";
197 this.error = "";
198 + this.result = null;
199
200 const data = await api.callJsonApi(PLUGIN_API, {
201 action: "install_git",
@@ -201,6 +208,8 @@ const model = {
208 return;
209 }
210
211 + this.result = data;
212 +
213 toastFrontendSuccess(
214 `Plugin "${data.title || data.plugin_name}" installed`,
215 "Plugin Installer"
@@ -350,8 +359,9 @@ const model = {
359 },
360
361 openDetail(plugin) {
353 - this.selectedPlugin = {...plugin, name: this._pluginName(plugin) };
362 + this.selectedPlugin = { ...plugin, name: this._pluginName(plugin) };
363 this.error = "";
364 + this.result = null;
365 this.installedPluginInfo = null;
366 this.readmeContent = null;
367 this.detailThumbnailUrl = this.getThumbnailUrl(this.selectedPlugin);
@@ -491,9 +501,8 @@ const model = {
501 },
502
503 manageOpenInit() {
494 - const initStore = this._pluginInitStore();
495 - if (initStore?.open && this.installedPluginInfo) {
496 - initStore.open(this.installedPluginInfo);
504 + if (this.installedPluginInfo) {
505 + pluginInitStore.open(this.installedPluginInfo);
506 }
507 },
508
@@ -527,18 +536,27 @@ const model = {
536 return this.detailThumbnailUrl;
537 },
538
539 + openScreenshot(url) {
540 + if (!url) return;
541 + imageViewerStore.open(url, {
542 + name: this.selectedPlugin?.title || this.selectedPlugin?.key || "Plugin screenshot",
543 + });
544 + },
545 +
546 // ── Shared ───────────────────────────────────
547
548 resetZip() {
549 this.zipFile = null;
550 this.zipFileName = "";
551 this.error = "";
552 + this.result = null;
553 },
554
555 resetGit() {
556 this.gitUrl = "";
557 this.gitToken = "";
558 this.error = "";
559 + this.result = null;
560 },
561
562 resetIndex() {
@@ -547,6 +565,7 @@ const model = {
565 this.sortBy = "stars";
566 this.browseFilter = "all";
567 this.error = "";
568 + this.result = null;
569 this.selectedPlugin = null;
570 },
571
webui/components/plugins/list/plugin-list.html
+1 -1
@@ -113,7 +113,7 @@
113 <button type="button"
114 class="button"
115 title="Run initializer script"
116 - @click="$store.pluginInitStore.open(plugin)">
116 + @click="$store.pluginListStore.openPluginInit(plugin)">
117 <span class="icon material-symbols-outlined">terminal</span> Init
118 </button>
119 </template>
webui/components/plugins/list/pluginListStore.js
+11 -10
@@ -1,9 +1,9 @@
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 "/components/plugins/list/plugin-init-store.js";
6 -import "/components/modals/markdown/markdown-store.js";
3 +import { store as pluginSettingsStore } from "/components/plugins/plugin-settings-store.js";
4 +import { store as pluginToggleStore } from "/components/plugins/toggle/plugin-toggle-store.js";
5 +import { store as pluginInitStore } from "/components/plugins/list/plugin-init-store.js";
6 +import { store as markdownModalStore } from "/components/modals/markdown/markdown-store.js";
7 import {
8 store as notificationStore,
9 defaultPriority,
@@ -56,14 +56,17 @@ const model = {
56 window.openModal?.(`/plugins/${plugin.name}/webui/main.html`);
57 },
58
59 + openPluginInit(plugin) {
60 + if (!plugin?.name || !plugin?.has_init_script) return;
61 + pluginInitStore.open(plugin);
62 + },
63 +
64 async openPluginConfig(plugin) {
65 if (!plugin?.name || !plugin?.has_config_screen) return;
66 try {
67 // Initialize toggle store for activation state UI in settings modal
63 - const pluginToggleStore = Alpine.store("pluginToggle");
68 if (pluginToggleStore?.open) await pluginToggleStore.open(plugin);
69
66 - const pluginSettingsStore = Alpine.store("pluginSettingsPrototype");
70 if (!pluginSettingsStore?.open) {
71 throw new Error("Plugin settings store is unavailable.");
72 }
@@ -85,7 +88,6 @@ const model = {
88 if (!plugin?.name) return;
89 this.selectedPlugin = plugin;
90 try {
88 - const pluginToggleStore = Alpine.store("pluginToggle");
91 if (!pluginToggleStore?.open) {
92 throw new Error("Plugin toggle store is unavailable.");
93 }
@@ -136,9 +138,8 @@ const model = {
138 doc,
139 });
140 if (response?.error) throw new Error(response.error);
139 - const markdownModal = Alpine.store("markdownModal");
140 - if (!markdownModal) throw new Error("Markdown modal store unavailable.");
141 - markdownModal.open(response.filename, response.content);
141 + if (!markdownModalStore?.open) throw new Error("Markdown modal store unavailable.");
142 + markdownModalStore.open(response.filename, response.content);
143 window.openModal?.("components/modals/markdown/markdown-modal.html");
144 } catch (e) {
145 showErrorNotification(e, "Failed to open document");
webui/components/plugins/plugin-settings-store.js
+8 -8
@@ -1,5 +1,7 @@
1 import { createStore } from "/js/AlpineStore.js";
2 import { showConfirmDialog } from "/js/confirmDialog.js";
3 +import { store as settingsStore } from "/components/settings/settings-store.js";
4 +import { store as pluginToggleStore } from "/components/plugins/toggle/plugin-toggle-store.js";
5
6 const fetchApi = globalThis.fetchApi;
7 const justToast = globalThis.justToast;
@@ -56,11 +58,10 @@ const model = {
58 await this.loadSettings();
59
60 // Mirror scope change to pluginToggle so activation state stays in sync
59 - const toggleStore = Alpine.store('pluginToggle');
60 - if (toggleStore) {
61 - toggleStore.projectName = nextProject;
62 - toggleStore.agentProfileKey = nextProfile;
63 - await toggleStore.loadToggleStatus();
61 + if (pluginToggleStore?.loadToggleStatus) {
62 + pluginToggleStore.projectName = nextProject;
63 + pluginToggleStore.agentProfileKey = nextProfile;
64 + await pluginToggleStore.loadToggleStatus();
65 }
66 },
67
@@ -292,9 +293,8 @@ const model = {
293
294 // Core-backed plugins (e.g. memory) delegate to the settings store
295 if (this.saveMode === 'core') {
295 - const coreStore = Alpine.store('settings');
296 - if (coreStore?.saveSettings) {
297 - const ok = await coreStore.saveSettings();
296 + if (settingsStore?.saveSettings) {
297 + const ok = await settingsStore.saveSettings();
298 if (ok) window.closeModal?.();
299 }
300 return;
webui/components/plugins/toggle/plugin-toggle-advanced.html
+1 -1
@@ -63,7 +63,7 @@
63 <label class="toggle" :class="{ 'disabled-appearance': !$store.pluginToggle.hasExplicitRuleForScope && !$store.pluginToggle.alwaysEnabled }">
64 <input type="checkbox"
65 :checked="$store.pluginToggle.status === 'enabled'"
66 - :disabled="$store.pluginToggle.alwaysEnabled || $store.pluginToggle.isSaving || !$store.pluginToggle.hasExplicitRuleForScope"
66 + :disabled="$store.pluginToggle.alwaysEnabled || $store.pluginToggle.isSaving"
67 @change="$store.pluginToggle.setEnabled($event.target.checked)">
68 <span class="toggler"></span>
69 </label>
webui/components/settings/plugins/plugins-subsection-store.js new
+58
@@ -0,0 +1,58 @@
1 +import { createStore } from "/js/AlpineStore.js";
2 +import * as api from "/js/api.js";
3 +import { store as pluginListStore } from "/components/plugins/list/pluginListStore.js";
4 +
5 +const model = {
6 + tab: "",
7 + plugins: [],
8 + loading: false,
9 +
10 + resolveTab(element) {
11 + const host =
12 + element?.closest("x-component")
13 + || element?.parentElement?.closest("x-component");
14 + return host?.getAttribute("data-tab") || "";
15 + },
16 +
17 + async init(element) {
18 + this.tab = this.resolveTab(element);
19 + await this.load();
20 + },
21 +
22 + cleanup() {
23 + this.tab = "";
24 + this.plugins = [];
25 + this.loading = false;
26 + },
27 +
28 + async load() {
29 + if (!this.tab) {
30 + this.plugins = [];
31 + return;
32 + }
33 +
34 + this.loading = true;
35 + try {
36 + const response = await api.callJsonApi("plugins_list", {
37 + filter: { custom: true, builtin: true },
38 + });
39 + const plugins = Array.isArray(response?.plugins) ? response.plugins : [];
40 + this.plugins = plugins.filter((plugin) => {
41 + const sections = Array.isArray(plugin?.settings_sections)
42 + ? plugin.settings_sections
43 + : [];
44 + return plugin?.has_config_screen && sections.includes(this.tab);
45 + });
46 + } catch {
47 + this.plugins = [];
48 + } finally {
49 + this.loading = false;
50 + }
51 + },
52 +
53 + async openPluginConfig(plugin) {
54 + await pluginListStore.openPluginConfig(plugin);
55 + },
56 +};
57 +
58 +export const store = createStore("pluginsSubsectionPrototype", model);
webui/components/settings/plugins/plugins-subsection.html
+35 -71
@@ -1,7 +1,7 @@
1 <html>
2 <head>
3 <script type="module">
4 - import { store } from "/components/plugins/plugin-settings-store.js";
4 + import { store } from "/components/settings/plugins/plugins-subsection-store.js";
5 </script>
6 </head>
7 <body>
@@ -11,80 +11,44 @@
11 <x-component path="settings/plugins/plugins-subsection.html" data-tab="agent"></x-component>
12 The tab value is read at init time from the x-component element's dataset.
13 -->
14 - <div x-data="{
15 - tab: '',
16 - plugins: [],
17 - loading: false,
18 - async init() {
19 - // find closest x-component ancestor to read the data-tab attribute
20 - const host = this.$el.closest('x-component') || this.$el.parentElement?.closest('x-component');
21 - this.tab = host?.getAttribute('data-tab') || '';
22 - await this.load();
23 - },
24 - async load() {
25 - if (!this.tab) return;
26 - this.loading = true;
27 - try {
28 - const r = await globalThis.fetchApi('/plugins_list', {
29 - method: 'POST',
30 - headers: { 'Content-Type': 'application/json' },
31 - body: JSON.stringify({
32 - filter: { custom: true, builtin: true },
33 - }),
34 - });
35 - const data = await r.json().catch(() => ({}));
36 - const plugins = data.ok ? (data.plugins || []) : [];
37 - this.plugins = plugins.filter((plugin) => {
38 - const sections = Array.isArray(plugin.settings_sections)
39 - ? plugin.settings_sections
40 - : [];
41 - return plugin.has_config_screen && sections.includes(this.tab);
42 - });
43 - } catch {
44 - this.plugins = [];
45 - } finally {
46 - this.loading = false;
47 - }
48 - },
49 - async openPluginConfig(plugin) {
50 - if (!plugin?.name) return;
51 - await Alpine.store('pluginSettings')?.open(plugin.name);
52 - window.openModal?.('components/plugins/plugin-settings.html');
53 - }
54 - }" x-init="init()">
55 -
56 - <template x-if="loading || plugins.length > 0">
57 - <div>
58 - <div class="section-title">Plugins</div>
59 - <div class="section-description">
60 - Plugin-specific settings for this tab.
61 - </div>
14 + <div x-data="(() => ({ context: $instantiate($store.pluginsSubsectionPrototype) }))()">
15 + <template x-if="context">
16 + <div x-create="context.init($el)"
17 + x-destroy="context.cleanup()">
18 + <template x-if="context.loading || context.plugins.length > 0">
19 + <div>
20 + <div class="section-title">Plugins</div>
21 + <div class="section-description">
22 + Plugin-specific settings for this tab.
23 + </div>
24
63 - <div x-show="loading" class="loading">
64 - <span>Loading plugins...</span>
65 - </div>
25 + <div x-show="context.loading" class="loading">
26 + <span>Loading plugins...</span>
27 + </div>
28
67 - <div class="plugins-list">
68 - <template x-for="plugin in plugins" :key="plugin.path || plugin.name">
69 - <div class="plugin-card">
70 - <div class="plugin-header">
71 - <div class="plugin-heading">
72 - <div class="plugin-title" x-text="plugin.display_name || plugin.name || '(unnamed plugin)'"></div>
73 - <code class="plugin-path" x-text="plugin.path"></code>
74 - </div>
75 - <div class="plugin-actions">
76 - <button type="button"
77 - class="button"
78 - title="Config"
79 - @click="openPluginConfig(plugin)">
80 - <span class="icon material-symbols-outlined">settings</span> Config
81 - </button>
29 + <div class="plugins-list">
30 + <template x-for="plugin in context.plugins" :key="plugin.path || plugin.name">
31 + <div class="plugin-card">
32 + <div class="plugin-header">
33 + <div class="plugin-heading">
34 + <div class="plugin-title" x-text="plugin.display_name || plugin.name || '(unnamed plugin)'"></div>
35 + <code class="plugin-path" x-text="plugin.path"></code>
36 + </div>
37 + <div class="plugin-actions">
38 + <button type="button"
39 + class="button"
40 + title="Config"
41 + @click="context.openPluginConfig(plugin)">
42 + <span class="icon material-symbols-outlined">settings</span> Config
43 + </button>
44 + </div>
45 + </div>
46 + <div class="plugin-description" x-text="plugin.description || 'No description provided.'"></div>
47 </div>
83 - </div>
84 - <div class="plugin-description" x-text="plugin.description || 'No description provided.'"></div>
48 + </template>
49 </div>
86 - </template>
87 - </div>
50 + </div>
51 + </template>
52 </div>
53 </template>
54 </div>