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