Optimize plugin reload notifications to check for webui extensions
Add plugin_names parameter to after_plugin_change() and send_frontend_reload_notification() to enable targeted extension checking. Only send reload notification if changed plugins have webui extensions (checks extensions/webui directory existence). Defer notification send by 1 second to allow multiple rapid changes to coalesce. Update delete_plugin() to send notification before deletion for proper extension checking. Pass plugin names
frdel committed
Mar 10, 2026 at 13:51 UTC
999a14e31face2a8e24d32df3e4973c809239808
6 files changed
+48
-23
api/plugins.py
+1
-1
@@ -15,7 +15,7 @@ class Plugins(ApiHandler):
15
"""
16
17
async def process(self, input: dict, request: Request) -> dict | Response:
18
- action = input.get("action", "get_config")
18
+ action = input.get("action", "")
19
20
# Accept legacy aliases during migration.
21
if action == "get_config":
helpers/plugins.py
+42
-18
@@ -1,5 +1,6 @@
1
from __future__ import annotations
2
3
+import asyncio
4
import re, json, glob
5
import time
6
from pathlib import Path
@@ -17,6 +18,8 @@ from typing import (
18
from helpers import files, notification, print_style, yaml as yaml_helper, cache
19
from pydantic import BaseModel, Field
20
21
+from helpers.defer import DeferredTask
22
+
23
if TYPE_CHECKING:
24
from agent import Agent
25
@@ -73,9 +76,9 @@ class PluginListItem(BaseModel):
76
toggle_state: ToggleState = "disabled"
77
78
76
-def after_plugin_change():
79
+def after_plugin_change(plugin_names: list[str] | None = None):
80
clear_plugin_cache()
78
- send_frontend_reload_notification()
81
+ send_frontend_reload_notification(plugin_names)
82
83
84
def clear_plugin_cache():
@@ -195,8 +198,9 @@ def delete_plugin(plugin_name: str):
198
custom_plugins_dir = files.get_abs_path(files.USER_DIR, files.PLUGINS_DIR)
199
if not files.is_in_dir(plugin_dir, custom_plugins_dir):
200
raise ValueError("Only custom plugins can be deleted")
201
+ send_frontend_reload_notification([plugin_name]) # send before deletion to properly check the extensions, second notification will be skipped automatically
202
files.delete_dir(plugin_dir)
199
- after_plugin_change()
203
+ after_plugin_change([plugin_name])
204
205
206
def get_plugin_paths(*subpaths: str) -> List[str]:
@@ -355,7 +359,7 @@ def toggle_plugin(
359
files.write_file(enabled_file, "")
360
else:
361
files.write_file(disabled_file, "")
358
- after_plugin_change()
362
+ after_plugin_change([plugin_name])
363
364
365
def get_plugin_config(
@@ -412,7 +416,7 @@ def save_plugin_config(
416
)
417
if file_path:
418
files.write_file(file_path, json.dumps(settings))
415
- after_plugin_change()
419
+ after_plugin_change([plugin_name])
420
421
422
def find_plugin_asset(
@@ -559,7 +563,8 @@ def determine_plugin_asset_path(
563
return files.get_abs_path(base_path, files.PLUGINS_DIR, plugin_name, *subpaths)
564
565
562
-def send_frontend_reload_notification():
566
+def send_frontend_reload_notification(plugin_names: list[str] | None = None):
567
+ """If the plugin changed has webui extensions, notify frontend to reload the page"""
568
global _last_frontend_reload_notification_at
569
570
display_time = 3
@@ -567,15 +572,34 @@ def send_frontend_reload_notification():
572
if now - _last_frontend_reload_notification_at < display_time:
573
return
574
570
- _last_frontend_reload_notification_at = now
571
-
572
- notification.NotificationManager.send_notification(
573
- type=notification.NotificationType.INFO,
574
- priority=notification.NotificationPriority.NORMAL,
575
- title="Plugins updated, page reload recommended",
576
- message="""<button type="button" class="button confirm" onclick="window.location.reload()"><span class="icon material-symbols-outlined">refresh</span>Reload page</button>""",
577
- detail="",
578
- display_time=display_time,
579
- group="plugins_changed",
580
- id="plugins_frontend_reload",
581
- )
575
+ if plugin_names:
576
+ has_webui_extension = False
577
+ for plugin_name in plugin_names:
578
+ plugin_dir = find_plugin_dir(plugin_name)
579
+ if plugin_dir and files.exists(
580
+ files.get_abs_path(plugin_dir, "extensions", "webui")
581
+ ):
582
+ has_webui_extension = True
583
+ break
584
+ if not has_webui_extension:
585
+ return
586
+
587
+ async def _send_later():
588
+ global _last_frontend_reload_notification_at
589
+
590
+ await asyncio.sleep(1)
591
+
592
+ _last_frontend_reload_notification_at = time.monotonic()
593
+
594
+ notification.NotificationManager.send_notification(
595
+ type=notification.NotificationType.INFO,
596
+ priority=notification.NotificationPriority.NORMAL,
597
+ title="Plugins with frontend extensions updated, page reload recommended",
598
+ message="""<button type="button" class="button confirm" onclick="window.location.reload()"><span class="icon material-symbols-outlined">refresh</span>Reload page</button>""",
599
+ detail="",
600
+ display_time=display_time,
601
+ group="plugins_changed",
602
+ id="plugins_frontend_reload",
603
+ )
604
+
605
+ DeferredTask().start_task(_send_later)
plugins/plugin_installer/helpers/install.py
+2
-2
@@ -138,7 +138,7 @@ def install_from_zip(zip_path: str, original_filename: str | None = None) -> dic
138
dest = os.path.join(_get_user_plugins_dir(), plugin_name)
139
os.makedirs(os.path.dirname(dest), exist_ok=True)
140
shutil.move(plugin_root, dest)
141
- after_plugin_change()
141
+ after_plugin_change([plugin_name])
142
143
return {
144
"success": True,
@@ -181,7 +181,7 @@ def install_from_git(url: str, token: Optional[str] = None) -> dict:
181
shutil.rmtree(dest, ignore_errors=True)
182
raise
183
184
- after_plugin_change()
184
+ after_plugin_change([repo_name])
185
186
return {
187
"success": True,
plugins/plugin_installer/webui/install-index.html
+1
-1
@@ -386,7 +386,7 @@
386
align-items: center;
387
justify-content: center;
388
height: 148px;
389
- padding: 1rem;
389
+ padding: 0.5rem;
390
background:
391
radial-gradient(circle at top, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0)),
392
var(--color-panel);
plugins/plugin_installer/webui/main.html
+1
-1
@@ -511,7 +511,7 @@
511
height: 148px;
512
padding: 1rem;
513
background:
514
- radial-gradient(circle at top, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0)),
514
+ radial-gradient(circle at top, rgba(255, 255, 255, 0.16) 0%, var(--color-panel) 100%),
515
var(--color-panel);
516
border-bottom: 1px solid var(--color-border);
517
}
usr/plugins/agent0ai__a0_example_plugin
new
+1
@@ -0,0 +1 @@
1
+Subproject commit 0c41e09418a13f97bc4dd8f53d7fa9a69383d82f