Fix plugin reload toast behavior

Persist the frontend-extension notification as read before reloading so it does not reappear after refresh. Align plugin and updater toast actions with the shared small spacing token.

Alessandro committed Jul 12, 2026 at 17:26 UTC 4b5077c493cb0c698ec498cec73e6ed89de14d4f
6 files changed +21 -3
helpers/plugins.py
+1 -1
@@ -840,7 +840,7 @@ def send_frontend_reload_notification(plugin_names: list[str] | None = None):
840 type=notification.NotificationType.INFO,
841 priority=notification.NotificationPriority.NORMAL,
842 title="Plugins with frontend extensions updated, page reload recommended",
843 - message="""<button type="button" class="button confirm" onclick="window.location.reload()"><span class="icon material-symbols-outlined">refresh</span>Reload page</button>""",
843 + message="""<div class="toast-action-row"><button type="button" class="button confirm" @click.stop="$store.notificationStore.dismissToastAndReload(toast.toastId)"><span class="icon material-symbols-outlined">refresh</span>Reload page</button></div>""",
844 detail="",
845 display_time=0,
846 group="plugins_changed",
helpers/plugins.py.dox.md
+1 -1
@@ -40,7 +40,7 @@
40 - `find_plugin_asset(plugin_name: str, *subpaths, project_name=..., agent_profile=...)`
41 - `find_plugin_assets(*subpaths, plugin_name: str=..., project_name: str=..., agent_profile: str=..., only_first: bool=...) -> list[PluginAssetFile]`
42 - `determine_plugin_asset_path(plugin_name: str, project_name: str, agent_profile: str, *subpaths)`
43 -- `send_frontend_reload_notification(plugin_names: list[str] | None=...)`: If the plugin changed has webui extensions, show a persistent reload notification
43 +- `send_frontend_reload_notification(plugin_names: list[str] | None=...)`: If the plugin changed has webui extensions, show a persistent reload notification that marks itself read before refreshing
44 - `call_plugin_hook(plugin_name: str, hook_name: str, default: Any=..., *args, **kwargs)`
45 - `_apply_defaults_from_env(plugin_name: str, config: dict[str, Any])`
46 - Notable constants/configuration names: `_META_TARGET_RE`, `META_FILE_NAME`, `CONFIG_FILE_NAME`, `CONFIG_DEFAULT_FILE_NAME`, `DISABLED_FILE_NAME`, `ENABLED_FILE_NAME`, `TOGGLE_FILE_PATTERN`, `HOOKS_SCRIPT`, `HOOKS_CACHE_AREA`, `PLUGINS_LIST_CACHE_AREA`, `ENABLED_PLUGINS_LIST_CACHE_AREA`, `ENABLED_PLUGINS_PATHS_CACHE_AREA`.
tests/test_download_toast_regressions.py
+7
@@ -101,13 +101,20 @@ def test_notification_store_supports_persistent_grouped_toasts():
101 assert "if display_time < 0:" in api
102 assert "if display_time <= 0:" not in api
103 assert 'id="plugins_frontend_reload",' in plugins
104 + assert "$store.notificationStore.dismissToastAndReload(toast.toastId)" in plugins
105 + assert "onclick=\"window.location.reload()\"" not in plugins
106 assert 'id=notif.get("id", "update_check_available"),' in update_check
107 assert "display_time=0," in plugins
108 assert "display_time=0," in update_check
109 + assert 'class="toast-action-row"' in plugins
110 assert 'class="toast-action-row"' in update_check
111 assert 'class="button confirm"' in update_check
112 assert "$store.notificationStore.dismissToast(toast.toastId)" in update_check
113 assert ".toast-action-row" in toast_stack
114 + assert "margin-top: var(--spacing-sm);" in toast_stack
115 + assert "async dismissToastAndReload(toastId)" in store
116 + assert 'await API.callJsonApi("notifications_mark_read"' in store
117 + assert "if (response?.success) window.location.reload();" in store
118
119
120 def test_backup_zip_downloads_emit_grouped_preparing_and_downloading_toasts():
webui/components/notifications/AGENTS.md
+1
@@ -16,6 +16,7 @@
16 - Use the notification system for user-facing success, warning, info, and error feedback.
17 - Keep public helper names stable for core and plugin callers.
18 - Keep the toast stack visible above normal and legacy modal overlays while staying below confirmation dialogs.
19 +- Reload actions must persist their notification as read before refreshing the page.
20 - Avoid exposing secrets or raw auth payloads in notification text.
21
22 ## Work Guidance
webui/components/notifications/notification-store.js
+10
@@ -200,6 +200,16 @@ const model = {
200 this.removeFromToastStack(toastId, true);
201 },
202
203 + async dismissToastAndReload(toastId) {
204 + const toast = this.toastStack.find((item) => item.toastId === toastId);
205 + if (!toast?.id) return;
206 +
207 + const response = await API.callJsonApi("notifications_mark_read", {
208 + notification_ids: [toast.id],
209 + });
210 + if (response?.success) window.location.reload();
211 + },
212 +
213 async afterToastRemoved(toast, removedByUser = false) {
214 // if the toast is closed by the user OR timed out with normal priority, mark it as read
215 if (removedByUser || toast.priority <= NotificationPriority.NORMAL) {
webui/components/notifications/notification-toast-stack.html
+1 -1
@@ -151,7 +151,7 @@
151 }
152
153 .toast-action-row {
154 - margin-top: 10px;
154 + margin-top: var(--spacing-sm);
155 }
156
157 .toast-timestamp {