main
js 285 lines 10 KB
Raw
1 import { createStore } from "/js/AlpineStore.js";
2 import { fetchApi } from "/js/api.js";
3 import { store as settingsStore } from "/components/plugins/plugin-settings-store.js";
4
5 const model = {
6 pluginName: null,
7
8 // Context selectors
9 projects: [],
10 agentProfiles: [],
11 projectName: "",
12 agentProfileKey: "",
13
14 // State
15 isLoading: false,
16 isSaving: false,
17 error: null,
18
19 // Status: 'enabled' | 'disabled'
20 status: 'enabled',
21 alwaysEnabled: false,
22 perProjectConfig: true,
23 perAgentConfig: true,
24 hasExplicitRuleForScope: false,
25 hasConfigScreen: false,
26
27 // Where the effective toggle was actually resolved (mirrors plugin-settings-store loadedXxx fields)
28 loadedPath: "",
29 loadedProjectName: "",
30 loadedAgentProfile: "",
31
32 configs: [],
33
34 async open(plugin, { projectName = "", agentProfileKey = "" } = {}) {
35 this.isLoading = true;
36 this.error = null;
37 this.projects = [];
38 this.agentProfiles = [];
39 this.projectName = projectName || "";
40 this.agentProfileKey = agentProfileKey || "";
41 this.configs = [];
42 this.status = 'enabled';
43 this.hasExplicitRuleForScope = false;
44 this.loadedPath = "";
45 this.loadedProjectName = "";
46 this.loadedAgentProfile = "";
47
48 const pluginName = typeof plugin === 'string' ? plugin : plugin?.name;
49 this.pluginName = pluginName;
50 this.alwaysEnabled = typeof plugin === 'object' ? !!plugin.always_enabled : false;
51 this.perProjectConfig = typeof plugin === 'object' ? !!plugin.per_project_config : true;
52 this.perAgentConfig = typeof plugin === 'object' ? !!plugin.per_agent_config : true;
53 this.hasConfigScreen = typeof plugin === 'object' ? !!plugin.has_config_screen : false;
54
55 try {
56 await Promise.all([this.loadProjects(), this.loadAgentProfiles()]);
57 await this.loadConfigs();
58 } finally {
59 this.isLoading = false;
60 }
61 },
62
63 cleanup() {
64 this.pluginName = null;
65 this.projectName = "";
66 this.agentProfileKey = "";
67 this.error = null;
68 this.configs = [];
69 this.perProjectConfig = true;
70 this.perAgentConfig = true;
71 this.alwaysEnabled = false;
72 this.hasConfigScreen = false;
73 this.hasExplicitRuleForScope = false;
74 this.loadedPath = "";
75 this.loadedProjectName = "";
76 this.loadedAgentProfile = "";
77 this.isLoading = false;
78 this.isSaving = false;
79 },
80
81 async loadProjects() {
82 try {
83 const response = await fetchApi("/projects", {
84 method: "POST",
85 headers: { "Content-Type": "application/json" },
86 body: JSON.stringify({ action: "list_options" }),
87 });
88 const data = await response.json().catch(() => ({}));
89 this.projects = data.ok ? (data.data || []) : [];
90 } catch {
91 this.projects = [];
92 }
93 },
94
95 async loadAgentProfiles() {
96 try {
97 const response = await fetchApi("/agents", {
98 method: "POST",
99 headers: { "Content-Type": "application/json" },
100 body: JSON.stringify({ action: "list" }),
101 });
102 const data = await response.json().catch(() => ({}));
103 this.agentProfiles = data.ok ? (data.data || []) : [];
104 } catch {
105 this.agentProfiles = [];
106 }
107 },
108
109 async loadConfigs() {
110 if (!this.pluginName) return;
111 this.isLoading = true;
112 try {
113 const response = await fetchApi("/plugins", {
114 method: "POST",
115 headers: { "Content-Type": "application/json" },
116 body: JSON.stringify({
117 action: "list_configs",
118 plugin_name: this.pluginName,
119 asset_type: "toggle"
120 }),
121 });
122 const result = await response.json().catch(() => ({}));
123 this.configs = result.ok ? (result.data || []) : [];
124 await this.loadToggleStatus();
125 } catch (e) {
126 this.error = e?.message || "Failed to load configurations";
127 } finally {
128 this.isLoading = false;
129 }
130 },
131
132 async loadToggleStatus() {
133 if (!this.pluginName) return;
134
135 if (this.alwaysEnabled) {
136 this.status = 'enabled';
137 this.hasExplicitRuleForScope = true;
138 this.loadedPath = "";
139 this.loadedProjectName = this.projectName || "";
140 this.loadedAgentProfile = this.agentProfileKey || "";
141 return;
142 }
143
144 try {
145 const response = await fetchApi("/plugins", {
146 method: "POST",
147 headers: { "Content-Type": "application/json" },
148 body: JSON.stringify({
149 action: "get_toggle_status",
150 plugin_name: this.pluginName,
151 project_name: this.projectName || "",
152 agent_profile: this.agentProfileKey || "",
153 }),
154 });
155 const result = await response.json().catch(() => ({}));
156 if (result.ok) {
157 this.status = result.status || 'enabled';
158 this.loadedPath = result.loaded_path || "";
159 this.loadedProjectName = result.loaded_project_name || "";
160 this.loadedAgentProfile = result.loaded_agent_profile || "";
161 const p = this.projectName || "";
162 const a = this.agentProfileKey || "";
163 this.hasExplicitRuleForScope = !!(this.loadedPath) &&
164 this.loadedProjectName === p &&
165 this.loadedAgentProfile === a;
166 }
167 } catch (e) {
168 this.error = e?.message || "Failed to load toggle status";
169 }
170 },
171
172 async switchToConfig(projectName, agentProfile) {
173 this.projectName = projectName || "";
174 this.agentProfileKey = agentProfile || "";
175 await this.onScopeChanged();
176 await window.closeModal?.();
177 },
178
179 async deleteConfig(path) {
180 if (!this.pluginName || !path) return;
181 this.isLoading = true;
182 try {
183 const response = await fetchApi("/plugins", {
184 method: "POST",
185 headers: { "Content-Type": "application/json" },
186 body: JSON.stringify({
187 action: "delete_config",
188 plugin_name: this.pluginName,
189 path: path
190 }),
191 });
192 const result = await response.json();
193 if (!result.ok) throw new Error(result.error);
194 await this.loadConfigs();
195 } catch (e) {
196 this.error = e.message || "Delete failed";
197 } finally {
198 this.isLoading = false;
199 }
200 },
201
202 async setEnabled(enabled, { projectName = this.projectName, agentProfileKey = this.agentProfileKey } = {}) {
203 if (!this.pluginName || this.alwaysEnabled) return;
204 const previousStatus = this.status;
205 const previousProjectName = this.projectName;
206 const previousAgentProfileKey = this.agentProfileKey;
207 this.projectName = projectName || "";
208 this.agentProfileKey = agentProfileKey || "";
209 this.status = enabled ? 'enabled' : 'disabled';
210 this.isSaving = true;
211 try {
212 const response = await fetchApi("/plugins", {
213 method: "POST",
214 headers: { "Content-Type": "application/json" },
215 body: JSON.stringify({
216 action: "toggle_plugin",
217 plugin_name: this.pluginName,
218 project_name: this.projectName || "",
219 agent_profile: this.agentProfileKey || "",
220 enabled: enabled
221 }),
222 });
223 const result = await response.json();
224 if (!result.ok) throw new Error(result.error);
225 await new Promise(r => setTimeout(r, 100));
226 await this.loadConfigs();
227 } catch (e) {
228 this.status = previousStatus;
229 this.projectName = previousProjectName;
230 this.agentProfileKey = previousAgentProfileKey;
231 this.error = e.message || "Failed to save";
232 throw e;
233 } finally {
234 this.isSaving = false;
235 }
236 },
237
238 async onScopeChanged() {
239 await this.loadToggleStatus();
240
241 // Sync scope with settings store so its loadSettings picks up the right context
242 settingsStore.projectName = this.projectName || "";
243 settingsStore.agentProfileKey = this.agentProfileKey || "";
244 await settingsStore.loadSettings();
245 },
246
247 async addRule() {
248 await this.setEnabled(this.status === 'enabled');
249 },
250
251 projectLabel(key) {
252 if (!key) return "Global";
253 const found = (this.projects || []).find(p => p.key === key);
254 return found?.label || key;
255 },
256
257 agentProfileLabel(key) {
258 if (!key) return "All profiles";
259 const found = (this.agentProfiles || []).find(p => p.key === key);
260 return found?.label || key;
261 },
262
263 get statusLabel() {
264 return this.status === 'enabled' ? 'ON' : 'OFF';
265 },
266
267 get noScopeRuleMessage() {
268 if (this.alwaysEnabled || this.isLoading) return "";
269 if (this.hasExplicitRuleForScope) return "";
270
271 if (!this.loadedPath) {
272 return this.configs.length === 0
273 ? "No activation rule exists yet. This plugin is currently ON by default."
274 : "No activation rule exists for this scope yet. This scope is currently ON by default.";
275 }
276
277 // Inherited from a parent scope - mirrors plugin-settings-store scopeMismatchMessage
278 const pLabel = this.projectLabel(this.loadedProjectName || "");
279 const aLabel = this.agentProfileLabel(this.loadedAgentProfile || "");
280 const state = this.status === 'enabled' ? 'ON' : 'OFF';
281 return `No rule for this scope. Inheriting from ${pLabel}, ${aLabel}: ${state}.`;
282 }
283 };
284
285 export const store = createStore("pluginToggle", model);