plugins - frontend PoC

frdel committed Feb 16, 2026 at 20:01 UTC 3918133cbf316296b312cd4cbed9592c4261cb95
23 files changed +185 -93
jsconfig.json
+3 -1
@@ -2,7 +2,9 @@
2 "compilerOptions": {
3 "baseUrl": ".",
4 "paths": {
5 - "*": ["webui/*"]
5 + "*": ["webui/*"],
6 + "/plugins/*": ["plugins/*"],
7 + "/usr/plugins/*": ["usr/plugins/*"]
8 }
9 },
10 "include": ["webui/**/*.js"]
plugins/README.md
+3 -1
@@ -37,9 +37,11 @@ Components without the meta tag (e.g. modals, dashboards) are standalone and not
37
38 Resolution flow:
39
40 +TODO update:ú
41 +
42 1. The backend parses `<meta name="plugin-target">` from each component HTML at scan time.
43 2. `/plugins_resolve` returns component URLs with their target selectors.
42 -3. `plugins.js` (loaded globally) creates `<x-component>` elements at the declared target selectors.
44 +3. TODO REM: `plugins.js` (loaded globally) creates `<x-component>` elements at the declared target selectors.
45 4. The standard `components.js` MutationObserver handles loading automatically.
46 5. A MutationObserver in `plugins.js` retries for targets that appear after initial page render.
47
plugins/memory/extensions/frontend/sidebar.quick_actions.dropdown/memory-entry.html deleted
-14
@@ -1,14 +0,0 @@
1 -<html>
2 -
3 -<head>
4 - <meta name="plugin-target" content=".quick-actions-dropdown">
5 -</head>
6 -
7 -<body>
8 - <button class="dropdown-item" @click="openModal('../plugins/memory/extensions/frontend/memory_dashboard/memory-dashboard.html'); dropdownOpen = false">
9 - <span class="material-symbols-outlined">psychology</span>
10 - <span>Memories</span>
11 - </button>
12 -</body>
13 -
14 -</html>
plugins/memory/extensions/webui/sidebar-quick-actions-main-2/memory-entry.html new
+19
@@ -0,0 +1,19 @@
1 +<!-- <html>
2 +
3 +<head>
4 + <meta name="plugin-target" content=".quick-actions-dropdown">
5 +</head>
6 +
7 +<body>
8 + <button class="dropdown-item" @click="openModal('../plugins/memory/webui/memory-dashboard.html'); dropdownOpen = false">
9 + <span class="material-symbols-outlined">psychology</span>
10 + <span>Memories</span>
11 + </button>
12 +</body>
13 +
14 +</html> -->
15 +
16 + <!-- Memory -->
17 + <button class="config-button" id="memory-dash" @click="openModal('../plugins/memory/webui/memory-dashboard.html')" title="Memory">
18 + <span class="material-symbols-outlined">psychology</span>
19 + </button>
plugins/memory/webui/memory-dashboard-store.js renamed
+2 -2
@@ -54,7 +54,7 @@ const memoryDashboardStore = {
54 pollingEnabled: false,
55
56 async openModal() {
57 - await openModal("../plugins/memory/extensions/frontend/memory_dashboard/memory-dashboard.html");
57 + await openModal("../plugins/memory/webui/memory-dashboard.html");
58 },
59
60 init() {
@@ -449,7 +449,7 @@ ${memory.content_full}
449 this.editMode = false;
450 this.editMemoryBackup = null;
451 // Use global modal system
452 - openModal("../plugins/memory/extensions/frontend/memory_dashboard/memory-detail-modal.html");
452 + openModal("../plugins/memory/webui/memory-detail-modal.html");
453 },
454
455 closeMemoryDetails() {
plugins/memory/webui/memory-dashboard.html renamed
+1 -1
@@ -3,7 +3,7 @@
3 <head>
4 <title>Memory Dashboard</title>
5 <script type="module">
6 - import { store } from "/plugins/memory/extensions/frontend/memory_dashboard/memory-dashboard-store.js";
6 + import { store } from "/plugins/memory/webui/memory-dashboard-store.js";
7 </script>
8 </head>
9
plugins/memory/webui/memory-detail-modal.html renamed
python/api/banners.py
-3
@@ -17,6 +17,3 @@ class GetBanners(ApiHandler):
17
18 return {"banners": banners}
19
20 - @classmethod
21 - def get_methods(cls) -> list[str]:
22 - return ["POST"]
python/api/load_webui_extensions.py new
+19
@@ -0,0 +1,19 @@
1 +from python.helpers.api import ApiHandler, Request, Response
2 +from python.helpers import plugins
3 +
4 +
5 +class LoadWebuiExtensions(ApiHandler):
6 + """
7 + API endpoint for Welcome Screen banners.
8 + Add checks as extension scripts in python/extensions/banners/ or usr/extensions/banners/
9 + """
10 +
11 + async def process(self, input: dict, request: Request) -> dict | Response:
12 + extension_point = input.get("extension_point", [])
13 +
14 + if not extension_point:
15 + return Response(status=400, response="Missing extension_point")
16 +
17 + exts = plugins.get_webui_extensions(extension_point)
18 +
19 + return {"extensions": exts or []}
python/api/plugins_resolve.py deleted
-18
@@ -1,18 +0,0 @@
1 -from python.helpers.api import ApiHandler, Request, Response
2 -from python.helpers import plugins
3 -
4 -
5 -class PluginsResolve(ApiHandler):
6 - """
7 - Return all injectable plugin frontend components.
8 - Each plugin's extensions/frontend/**/*.html files are returned.
9 - The components themselves declare their injection target via <meta> tags.
10 - """
11 -
12 - @classmethod
13 - def get_methods(cls):
14 - return ["POST"]
15 -
16 - async def process(self, input: dict, request: Request) -> dict | Response:
17 - data = plugins.get_frontend_components()
18 - return {"ok": True, "data": data}
python/helpers/plugins.py
+13 -24
@@ -5,7 +5,7 @@ from dataclasses import dataclass
5 from pathlib import Path
6 from typing import Any, Dict, List, Optional
7
8 -from python.helpers import files
8 +from python.helpers import files, print_style
9
10 # Extracts target selector from <meta name="plugin-target" content="...">
11 _META_TARGET_RE = re.compile(
@@ -62,8 +62,8 @@ def get_plugin_paths(*subpaths: str) -> List[str]:
62 Resolve existing directories under each plugin matching subpaths.
63
64 Example:
65 - get_plugin_paths("extensions", "backend", "monologue_end")
66 - -> ["/abs/plugins/memory/extensions/backend/monologue_end", ...]
65 + get_plugin_paths("extensions", "python", "monologue_end")
66 + -> ["/abs/plugins/memory/extensions/python/monologue_end", ...]
67 """
68 sub = "/".join(subpaths) if subpaths else ""
69 paths: List[str] = []
@@ -74,32 +74,21 @@ def get_plugin_paths(*subpaths: str) -> List[str]:
74 return paths
75
76
77 -def get_frontend_components() -> List[Dict[str, Any]]:
78 - """
79 - Return all injectable plugin frontend components.
80 - Convention: plugins/*/extensions/frontend/**/*.html
81 - The backend reads each file to extract the optional
82 - <meta name="plugin-target" content=".css-selector"> tag so the
83 - frontend never needs to fetch component HTML just to discover targets.
84 - """
77 +def get_webui_extensions(extension_point:str) -> List[Dict[str, Any]]:
78 entries: List[Dict[str, Any]] = []
79 for plugin in list_plugins():
87 - frontend_dir = plugin.path / "extensions" / "frontend"
80 + frontend_dir = plugin.path / "extensions" / "webui" / extension_point
81 if not frontend_dir.is_dir():
82 continue
83 for html_file in sorted(frontend_dir.rglob("*.html"), key=lambda p: p.name):
91 - rel_path = html_file.relative_to(plugin.path).as_posix()
92 - entry: Dict[str, Any] = {
93 - "plugin_id": plugin.id,
94 - "component_url": f"/plugins/{plugin.id}/{rel_path}",
95 - }
96 - # Extract injection target from meta tag (if present)
84 try:
98 - content = html_file.read_text(encoding="utf-8")
99 - m = _META_TARGET_RE.search(content)
100 - if m:
101 - entry["target"] = m.group(1)
85 + rel_path = html_file.relative_to(plugin.path).as_posix()
86 + entry: Dict[str, Any] = {
87 + "plugin_id": plugin.id,
88 + "component_url": f"{plugin.path}/{rel_path}",
89 + "html": html_file.read_text(encoding="utf-8"),
90 + }
91 + entries.append(entry)
92 except Exception:
103 - pass
104 - entries.append(entry)
93 + print_style.PrintStyle.error(f"Failed to load frontend extension file {html_file}")
94 return entries
python/helpers/skills.py
+9 -1
@@ -54,13 +54,21 @@ def get_skill_roots(
54 projects = files.find_existing_paths_by_pattern("usr/projects/*/.a0proj/skills") # projects
55 usr_agents = files.find_existing_paths_by_pattern("usr/agents/*/skills") # agents
56 agents = files.find_existing_paths_by_pattern("agents/*/skills") # agents
57 + plugins = files.find_existing_paths_by_pattern("plugins/*/skills") # plugins
58 + usr_plugins = files.find_existing_paths_by_pattern("usr/plugins/*/skills") # plugins
59 + plugins_agents = files.find_existing_paths_by_pattern("plugins/*/agents/*/skills") # agents in plugins
60 + usr_plugins_agents = files.find_existing_paths_by_pattern("usr/plugins/*/agents/*/skills") # agents in plugins
61 paths = [
62 files.get_abs_path("skills"),
63 files.get_abs_path("usr/skills"),
64 *project_agents,
65 *projects,
66 *usr_agents,
63 - *agents
67 + *agents,
68 + *plugins,
69 + *usr_plugins,
70 + *plugins_agents,
71 + *usr_plugins_agents,
72 ]
73 return paths
74
run_ui.py
+3 -5
@@ -260,7 +260,7 @@ async def serve_plugin_asset(plugin_id, asset_path):
260 plugin_root = plugin.path.resolve()
261
262 # Security: ensure the resolved path is within the plugin directory
263 - if not str(asset_file).startswith(str(plugin_root) + os.sep) and str(asset_file) != str(plugin_root):
263 + if not files.is_in_dir(str(asset_file), str(plugin_root)):
264 return Response("Access denied", 403)
265
266 if not asset_file.is_file():
@@ -494,7 +494,7 @@ def run():
494
495 handlers = load_classes_from_folder("python/api", "*.py", ApiHandler)
496 for handler in handlers:
497 - register_api_handler(webapp, handler)
497 + register_api_handler(webapp, handler, url_prefix="/api")
498
499 # Load API handlers from plugins (prefixed with /plugins/{plugin_id}/)
500 from python.helpers import plugins
@@ -507,9 +507,7 @@ def run():
507 plugin_handlers = load_classes_from_folder(str(api_path), "*.py", ApiHandler)
508 for handler in plugin_handlers:
509 # prefixed route for explicit namespacing
510 - register_api_handler(webapp, handler, url_prefix=f"/plugins/{plugin.id}")
511 - # bare route so callers don't need to know the plugin prefix
512 - register_api_handler(webapp, handler)
510 + register_api_handler(webapp, handler, url_prefix=f"/api/plugins/{plugin.id}")
511
512 handlers_by_namespace = _build_websocket_handlers_by_namespace(socketio_server, lock)
513 configure_websocket_namespaces(
webui/components/settings/a2a/a2a-connection.html
+1 -1
@@ -58,7 +58,7 @@
58 // Fetch and populate projects
59 const projectSelect = document.getElementById('a2a-project-select');
60 try {
61 - const response = await fetch('/projects', {
61 + const response = await fetchApi('/projects', {
62 method: 'POST',
63 headers: { 'Content-Type': 'application/json' },
64 body: JSON.stringify({ action: 'list' })
webui/components/settings/agent/memory.html
+1 -1
@@ -34,7 +34,7 @@
34 <div class="field-control">
35 <button
36 class="btn btn-field"
37 - @click="openModal('../plugins/memory/extensions/frontend/memory_dashboard/memory-dashboard.html');"
37 + @click="openModal('../plugins/memory/webui/memory-dashboard.html');"
38 >
39 Open Dashboard
40 </button>
webui/components/settings/external/api-examples.html
+12 -12
@@ -209,7 +209,7 @@
209 const basicExample = `// Basic message example
210 async function sendMessage() {
211 try {
212 - const response = await fetch('${url}/api_message', {
212 + const response = await fetch('${url}/api/api_message', {
213 method: 'POST',
214 headers: {
215 'Content-Type': 'application/json',
@@ -249,7 +249,7 @@ sendMessage().then(result => {
249 const continuationExample = `// Continue conversation example
250 async function continueConversation(contextId) {
251 try {
252 - const response = await fetch('${url}/api_message', {
252 + const response = await fetch('${url}/api/api_message', {
253 method: 'POST',
254 headers: {
255 'Content-Type': 'application/json',
@@ -296,7 +296,7 @@ async function sendWithAttachment() {
296 const textContent = "Hello World from attachment!";
297 const base64Content = btoa(textContent);
298
299 - const response = await fetch('${url}/api_message', {
299 + const response = await fetch('${url}/api/api_message', {
300 method: 'POST',
301 headers: {
302 'Content-Type': 'application/json',
@@ -342,7 +342,7 @@ async function getLogsGET(contextId, length = 50) {
342 length: length.toString()
343 });
344
345 - const response = await fetch('${url}/api_log_get?' + params, {
345 + const response = await fetch('${url}/api/api_log_get?' + params, {
346 method: 'GET',
347 headers: {
348 'X-API-KEY': '${token}'
@@ -374,7 +374,7 @@ getLogsGET('ctx_abc123', 20);`;
374 const logPostExample = `// Get logs using POST request
375 async function getLogsPOST(contextId, length = 50) {
376 try {
377 - const response = await fetch('${url}/api_log_get', {
377 + const response = await fetch('${url}/api/api_log_get', {
378 method: 'POST',
379 headers: {
380 'Content-Type': 'application/json',
@@ -415,7 +415,7 @@ getLogsPOST('ctx_abc123', 10);`;
415 const terminateExample = `// Basic terminate chat function
416 async function terminateChat(contextId) {
417 try {
418 - const response = await fetch('${url}/api_terminate_chat', {
418 + const response = await fetch('${url}/api/api_terminate_chat', {
419 method: 'POST',
420 headers: {
421 'Content-Type': 'application/json',
@@ -469,7 +469,7 @@ simpleWorkflow();`;
469 const resetExample = `// Basic reset chat function
470 async function resetChat(contextId) {
471 try {
472 - const response = await fetch('${url}/api_reset_chat', {
472 + const response = await fetch('${url}/api/api_reset_chat', {
473 method: 'POST',
474 headers: {
475 'Content-Type': 'application/json',
@@ -511,7 +511,7 @@ async function resetAndContinue() {
511 console.log('Chat reset, starting fresh conversation...');
512
513 // Continue with same context_id but fresh history
514 - const response = await fetch('${url}/api_message', {
514 + const response = await fetch('${url}/api/api_message', {
515 method: 'POST',
516 headers: {
517 'Content-Type': 'application/json',
@@ -536,7 +536,7 @@ resetAndContinue();`;
536 const filesGetExample = `// Basic file retrieval
537 async function getFiles(filePaths) {
538 try {
539 - const response = await fetch('${url}/api_files_get', {
539 + const response = await fetch('${url}/api/api_files_get', {
540 method: 'POST',
541 headers: {
542 'Content-Type': 'application/json',
@@ -584,7 +584,7 @@ getFiles(filePaths);
584 // Example 2: Complete attachment workflow
585 async function attachmentWorkflow() {
586 // Step 1: Send message with attachments
587 - const messageResponse = await fetch('${url}/api_message', {
587 + const messageResponse = await fetch('${url}/api/api_message', {
588 method: 'POST',
589 headers: {
590 'Content-Type': 'application/json',
@@ -621,7 +621,7 @@ attachmentWorkflow();`;
621 async function sendMessageWithProject() {
622 try {
623 // First message - activate project
624 - const response = await fetch('${url}/api_message', {
624 + const response = await fetch('${url}/api/api_message', {
625 method: 'POST',
626 headers: {
627 'Content-Type': 'application/json',
@@ -641,7 +641,7 @@ async function sendMessageWithProject() {
641 console.log('Response:', data.response);
642
643 // Continue conversation - project already set
644 - const followUp = await fetch('${url}/api_message', {
644 + const followUp = await fetch('${url}/api/api_message', {
645 method: 'POST',
646 headers: {
647 'Content-Type': 'application/json',
webui/components/settings/mcp/server/example.html
+1 -1
@@ -58,7 +58,7 @@
58 // Fetch and populate projects
59 const projectSelect = document.getElementById('mcp-project-select');
60 try {
61 - const response = await fetch('/projects', {
61 + const response = await fetchApi('/projects', {
62 method: 'POST',
63 headers: { 'Content-Type': 'application/json' },
64 body: JSON.stringify({ action: 'list' })
webui/components/sidebar/top-section/quick-actions.html
+28 -2
@@ -11,26 +11,38 @@
11 <body>
12 <div class="wrapper" x-data="{ dropdownOpen: false }" @click.outside="dropdownOpen = false" @keydown.escape.window="dropdownOpen = false" x-init="$watch('dropdownOpen', v => v && $store.sidebar.updateDropdownPosition($el))">
13 <div id="quick-actions">
14 +
15 + <x-extension id="sidebar-quick-actions-main-start"></x-extension>
16 +
17 <!-- Dashboard -->
18 <button class="config-button" id="dashboard" @click="deselectChat()" title="Dashboard">
19 <span class="material-symbols-outlined">home</span>
20 </button>
21
22 + <x-extension id="sidebar-quick-actions-main-2"></x-extension>
23 +
24 +
25 <!-- Memory -->
20 - <button class="config-button" id="memory-dash" @click="openModal('../plugins/memory/extensions/frontend/memory_dashboard/memory-dashboard.html')" title="Memory">
26 + <!-- <button class="config-button" id="memory-dash" @click="openModal('../plugins/memory/webui/memory-dashboard.html')" title="Memory">
27 <span class="material-symbols-outlined">psychology</span>
22 - </button>
28 + </button> -->
29 +
30 + <x-extension id="sidebar-quick-actions-main-3"></x-extension>
31
32 <!-- Scheduler -->
33 <button class="config-button" id="scheduler" @click="openModal('modals/scheduler/scheduler-modal.html')" title="Scheduler">
34 <span class="material-symbols-outlined">schedule</span>
35 </button>
36
37 + <x-extension id="sidebar-quick-actions-main-4"></x-extension>
38 +
39 <!-- Settings -->
40 <button class="config-button" id="settings" @click="openModal('settings/settings.html')" title="Settings">
41 <span class="material-symbols-outlined">settings</span>
42 </button>
43
44 + <x-extension id="sidebar-quick-actions-main-end"></x-extension>
45 +
46 <!-- Dropdown Toggle -->
47 <button class="config-button dropdown-toggle" @click="dropdownOpen = !dropdownOpen" title="More options">
48 <span class="material-symbols-outlined" :class="dropdownOpen ? 'rotated' : ''">expand_more</span>
@@ -42,8 +54,12 @@
54 x-show="dropdownOpen"
55 :style="$store.sidebar.dropdownStyle">
56
57 + <x-extension id="sidebar-quick-actions-dropdown-start"></x-extension>
58 +
59 <div class="dropdown-header">Navigation</div>
60
61 + <x-extension id="sidebar-quick-actions-dropdown-navigation-start"></x-extension>
62 +
63 <button class="dropdown-item" @click="deselectChat(); dropdownOpen = false">
64 <span class="material-symbols-outlined">home</span>
65 <span>Dashboard</span>
@@ -63,10 +79,15 @@
79 <span class="material-symbols-outlined">settings</span>
80 <span>Settings</span>
81 </button>
82 +
83 + <x-extension id="sidebar-quick-actions-dropdown-navigation-end"></x-extension>
84 +
85
86 <div class="dropdown-separator"></div>
87 <div class="dropdown-header">Chat Actions</div>
88
89 + <x-extension id="sidebar-quick-actions-dropdown-chat-start"></x-extension>
90 +
91 <button class="dropdown-item" @click="$store.chats.newChat(); dropdownOpen = false">
92 <span class="material-symbols-outlined">edit_square</span>
93 <span>New Chat</span>
@@ -87,8 +108,12 @@
108 <span>Clear Chat</span>
109 </button>
110
111 + <x-extension id="sidebar-quick-actions-dropdown-chat-end"></x-extension>
112 +
113 <div class="dropdown-separator"></div>
114
115 + <x-extension id="sidebar-quick-actions-dropdown-rest-start"></x-extension>
116 +
117 <button class="dropdown-item" @click="$confirmClick($event, () => { $store.chats.logout(); dropdownOpen = false })" :disabled="!$store.chats.loggedIn">
118 <span class="material-symbols-outlined">logout</span>
119 <span>Logout</span>
@@ -99,6 +124,7 @@
124 <span>Restart A0</span>
125 </button>
126
127 + <x-extension id="sidebar-quick-actions-dropdown-rest-end"></x-extension>
128 </div>
129 </div>
130 </body>
webui/components/welcome/welcome-store.js
+1 -1
@@ -1,7 +1,7 @@
1 import { createStore } from "/js/AlpineStore.js";
2 import { getContext } from "/index.js";
3 import { store as chatsStore } from "/components/sidebar/chats/chats-store.js";
4 -import { store as memoryStore } from "/plugins/memory/extensions/frontend/memory_dashboard/memory-dashboard-store.js";
4 +import { store as memoryStore } from "/plugins/memory/webui/memory-dashboard-store.js";
5 import { store as projectsStore } from "/components/projects/projects-store.js";
6 import { store as chatInputStore } from "/components/chat/input/input-store.js";
7 import * as API from "/js/api.js";
webui/js/api.js
+3 -2
@@ -45,7 +45,8 @@ export async function fetchApi(url, request) {
45 finalRequest.headers["X-CSRF-Token"] = token;
46
47 // perform the fetch with the updated request
48 - const response = await fetch(url, finalRequest);
48 + const apiUrl = url.startsWith('/api/') || url.startsWith('api/') ? `/${url.replace(/^\/+/, '')}` : `/api/${url.replace(/^\/+/, '')}`;
49 + const response = await fetch(apiUrl, finalRequest);
50
51 // check if there was an CSRF error
52 if (response.status === 403 && retry) {
@@ -125,7 +126,7 @@ export async function getCsrfToken() {
126 fetchOptions.signal = controller.signal;
127 }
128
128 - const fetchPromise = fetch("/csrf_token", fetchOptions);
129 + const fetchPromise = fetch("/api/csrf_token", fetchOptions);
130 response = timeoutPromise
131 ? await Promise.race([fetchPromise, timeoutPromise])
132 : await fetchPromise;
webui/js/components.js
+2 -2
@@ -114,7 +114,7 @@ export async function importComponent(path, targetElement) {
114
115 const modulePromise = import(blobUrl)
116 .catch((err) => {
117 - console.error("Failed to load inline module", err);
117 + console.error(`Failed to load inline module ${virtualUrl}:`, err);
118 throw err;
119 })
120 .finally(() => URL.revokeObjectURL(blobUrl));
@@ -261,4 +261,4 @@ const observer = new MutationObserver((mutations) => {
261 }
262 }
263 });
264 -observer.observe(document.body, { childList: true, subtree: true });
264 +observer.observe(document.body, { childList: true, subtree: true });
\ No newline at end of file
webui/js/extensions.js new
+63
@@ -0,0 +1,63 @@
1 +import * as api from "./api.js";
2 +
3 +// Load all x-component tags starting from root elements
4 +export async function loadExtensions(roots = [document.documentElement]) {
5 + try {
6 + // Convert single root to array if needed
7 + const rootElements = Array.isArray(roots) ? roots : [roots];
8 +
9 + // Find all top-level components and load them in parallel
10 + const extensions = rootElements.flatMap((root) =>
11 + Array.from(root.querySelectorAll("x-extension")),
12 + );
13 +
14 + if (extensions.length === 0) return;
15 +
16 + await Promise.all(
17 + extensions.map(async (extension) => {
18 + const path = extension.getAttribute("id");
19 + if (!path) {
20 + console.error("x-extension missing id attribute:", extension);
21 + return;
22 + }
23 + await importExtensions(path, extension);
24 + }),
25 + );
26 + } catch (error) {
27 + console.error("Error loading extensions:", error);
28 + }
29 +}
30 +
31 +// import all extensions for extension point via backend api
32 +export async function importExtensions(extensionPointId, targetElement) {
33 + try {
34 + const response = await api.callJsonApi(`/api/load_webui_extensions`, {
35 + extension_point: extensionPointId,
36 + });
37 + let combinedHTML = "";
38 + for (const extension of response.extensions) {
39 + combinedHTML += extension.html.trim();
40 + }
41 + targetElement.innerHTML = combinedHTML;
42 + } catch (error) {
43 + console.error("Error importing extensions:", error);
44 + }
45 +}
46 +
47 +// Watch for DOM changes to dynamically load x-extensions
48 +const extensionObserver = new MutationObserver((mutations) => {
49 + for (const mutation of mutations) {
50 + for (const node of mutation.addedNodes) {
51 + if (node.nodeType === 1) {
52 + // ELEMENT_NODE
53 + // Check if this node or its descendants contain x-extension(s)
54 + if (node.matches?.("x-extension")) {
55 + importExtensions(node.getAttribute("id"), node);
56 + } else if (node.querySelectorAll) {
57 + loadExtensions([node]);
58 + }
59 + }
60 + }
61 + }
62 +});
63 +extensionObserver.observe(document.body, { childList: true, subtree: true });
webui/js/initFw.js
+1 -1
@@ -1,7 +1,7 @@
1 import * as initializer from "./initializer.js";
2 import * as _modals from "./modals.js";
3 import * as _components from "./components.js";
4 -import "./plugins.js";
4 +import * as _extensions from "./extensions.js";
5 import { registerAlpineMagic } from "./confirmClick.js";
6
7 // initialize required elements