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);