main
js 268 lines 7.53 KB
Raw
1 import { createStore } from "/js/AlpineStore.js";
2 import * as api from "/js/api.js";
3 import { renderSafeMarkdown } from "/js/safe-markdown.js";
4 import { store as pluginSettingsStore } from "/components/plugins/plugin-settings-store.js";
5 import { store as pluginExecuteStore } from "/components/plugins/list/plugin-execute-store.js";
6 import { store as fileBrowserStore } from "/components/modals/file-browser/file-browser-store.js";
7 import { store as markdownModalStore } from "/components/modals/markdown/markdown-store.js";
8 import { callJsExtensions } from "/js/extensions.js";
9 import {
10 store as notificationStore,
11 defaultPriority,
12 } from "/components/notifications/notification-store.js";
13
14 const MODAL_PATH = "components/plugins/list/plugin-list.html";
15
16 // define the model object holding data and functions
17 const model = {
18 loading: false,
19 plugins: [],
20 selectedPlugin: null,
21 activeTab: "custom",
22 readmeContent: "",
23 readmeLoading: false,
24 readmeError: "",
25
26 async open(tab = "custom") {
27 await this.setTab(tab);
28 window.openModal?.(MODAL_PATH);
29 },
30
31 async init() {
32 this.loading = false;
33 // If a tab is already selected (e.g. via open()), use it.
34 // Otherwise default to custom -> builtin fallback.
35 if (this.activeTab && this.activeTab !== "custom") {
36 await this.setTab(this.activeTab);
37 } else {
38 await this.setTab("custom");
39 if (this.plugins.length === 0) {
40 await this.setTab("builtin");
41 }
42 }
43 },
44
45 async loadPluginList(filter) {
46 this.loading = true;
47 this.selectedPlugin = null;
48 try {
49 const response = await api.callJsonApi("plugins_list", { filter });
50 this.plugins = Array.isArray(response.plugins) ? response.plugins : [];
51 void callJsExtensions("plugins_list_after_load", {
52 filter: filter ? { ...filter } : null,
53 plugins: this.plugins,
54 store: this,
55 });
56 } catch (e) {
57 this.plugins = [];
58 showErrorNotification(e, "Failed to load plugins list");
59 } finally {
60 this.loading = false;
61 }
62 },
63
64 async setTab(tab) {
65 if (tab === "pluginHub") {
66 this.activeTab = "pluginHub";
67 this.loading = false;
68 return;
69 }
70
71 this.activeTab = tab === "builtin" ? "builtin" : "custom";
72 const filter =
73 this.activeTab === "builtin"
74 ? { builtin: true, custom: false, search: "" }
75 : { builtin: false, custom: true, search: "" };
76 await this.loadPluginList(filter);
77 },
78
79 async refresh() {
80 if (this.activeTab === "pluginHub") {
81 return;
82 }
83 await this.setTab(this.activeTab);
84 },
85
86 openPlugin(plugin) {
87 if (!plugin?.name || !plugin?.has_main_screen) return;
88 window.openModal?.(`/plugins/${plugin.name}/webui/main.html`);
89 },
90
91 openPluginExecute(plugin) {
92 if (!plugin?.name || !plugin?.has_execute_script) return;
93 pluginExecuteStore.open(plugin);
94 },
95
96 canOpenPluginConfig(plugin) {
97 return !!(
98 plugin?.has_config_screen ||
99 plugin?.per_project_config ||
100 plugin?.per_agent_config
101 );
102 },
103
104 async openPluginConfig(pluginOrName) {
105 const pluginName =
106 typeof pluginOrName === "string" ? pluginOrName : pluginOrName?.name;
107 if (!pluginName) return;
108
109 if (
110 typeof pluginOrName === "object" &&
111 !this.canOpenPluginConfig(pluginOrName)
112 )
113 return;
114
115 try {
116 if (!pluginSettingsStore?.openConfig) {
117 throw new Error("Plugin settings store is unavailable.");
118 }
119 await pluginSettingsStore.openConfig(pluginName);
120 } catch (e) {
121 showErrorNotification(e, "Failed to open plugin config");
122 }
123 },
124
125 isPluginEnabled(plugin) {
126 if (plugin?.always_enabled) return true;
127 return plugin?.toggle_state === "enabled";
128 },
129
130 toggleStatusLabel(plugin) {
131 return this.isPluginEnabled(plugin) ? "ON" : "OFF";
132 },
133
134 async updateToggle(plugin, enabled) {
135 if (!plugin?.name) return;
136 if (plugin.always_enabled) return;
137
138 const nextEnabled = !!enabled;
139 const previousState = plugin.toggle_state;
140 plugin.toggle_state = nextEnabled ? "enabled" : "disabled";
141
142 this.loading = true;
143 try {
144 const response = await api.callJsonApi("plugins", {
145 action: "toggle_plugin",
146 plugin_name: plugin.name,
147 enabled: nextEnabled,
148 project_name: "",
149 agent_profile: "",
150 clear_overrides: false,
151 });
152 if (response?.error) throw new Error(response.error);
153 await this.refresh();
154 } catch (e) {
155 plugin.toggle_state = previousState;
156 showErrorNotification(e, "Failed to toggle plugin");
157 this.loading = false;
158 }
159 },
160
161 async openPluginDoc(plugin, doc) {
162 try {
163 const response = await api.callJsonApi("plugins", {
164 action: "get_doc",
165 plugin_name: plugin.name,
166 doc,
167 });
168 if (response?.error) throw new Error(response.error);
169 if (!markdownModalStore?.open) throw new Error("Markdown modal store unavailable.");
170 markdownModalStore.open(response.filename, response.content);
171 window.openModal?.("components/modals/markdown/markdown-modal.html");
172 } catch (e) {
173 showErrorNotification(e, "Failed to open document");
174 }
175 },
176
177 async loadPluginReadme(plugin) {
178 this.readmeLoading = true;
179 this.readmeContent = "";
180 this.readmeError = "";
181 try {
182 const response = await api.callJsonApi("plugins", {
183 action: "get_doc",
184 plugin_name: plugin.name,
185 doc: "readme",
186 });
187 if (response?.error) throw new Error(response.error);
188 this.readmeContent = renderSafeMarkdown(response.content || "");
189 } catch (e) {
190 const error = e instanceof Error ? e : new Error(String(e));
191 this.readmeError = error.message || "Failed to load README";
192 } finally {
193 this.readmeLoading = false;
194 }
195 },
196
197 openPluginInfo(plugin) {
198 if (!plugin) return;
199 this.selectedPlugin = plugin;
200 this.readmeContent = "";
201 this.readmeLoading = false;
202 this.readmeError = "";
203 if (plugin.has_readme) {
204 void this.loadPluginReadme(plugin);
205 }
206 window.openModal?.("components/plugins/plugin-info.html");
207 },
208
209 async openPluginFolder(plugin) {
210 if (!plugin?.path) return;
211 await fileBrowserStore.open(plugin.path);
212 },
213
214 async openPluginHub(plugin) {
215 const pluginKey = (plugin?.pluginHub?.key || "").trim();
216 if (!pluginKey) return;
217 const { store: pluginInstallStore } = await import(
218 "/plugins/_plugin_installer/webui/pluginInstallStore.js"
219 );
220 await pluginInstallStore.openPluginHubDetailByKey(pluginKey);
221 },
222
223 async deletePlugin(plugin) {
224 if (!plugin?.name) return;
225
226 if (!plugin.is_custom) {
227 showErrorNotification(
228 new Error("Only custom plugins can be deleted from this modal."),
229 "Delete blocked",
230 );
231 return;
232 }
233
234 try {
235 const response = await api.callJsonApi("plugins", {
236 action: "delete_plugin",
237 plugin_name: plugin.name,
238 });
239 if (response?.error) {
240 throw new Error(response.error);
241 }
242 if (window.toastFrontendSuccess) {
243 window.toastFrontendSuccess("Plugin deleted", "Plugins");
244 }
245 await this.refresh();
246 } catch (e) {
247 showErrorNotification(e, "Failed to delete plugin");
248 }
249 },
250 };
251
252 function showErrorNotification(error, heading) {
253 const text = error.message || error.text || JSON.stringify(error);
254 notificationStore.frontendError(
255 text,
256 heading,
257 3,
258 "pluginsList",
259 defaultPriority,
260 true,
261 );
262 }
263
264 // convert it to alpine store
265 const store = createStore("pluginListStore", model);
266
267 // export for use in other files
268 export { store };