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