agent-zero/extensions/python/user_message_ui/_10_update_check.py
Alessandro a1c12e9247 Refine settings and remote access UX
Restyle Settings and standard modals around a streamlined left-rail layout, clearer section hierarchy, advanced settings disclosures, and stronger update states.

Add persistent update visibility with quieter once-daily update notifications, plus Remote Link and Space Agent actions in the canvas rail. Refresh the tunnel experience as a normal Remote Link modal with clearer copy, QR/mobile affordances, and safer state handling.
2026-04-27 02:48:23 +02:00

105 lines
3.9 KiB
Python

from helpers import notification
from helpers.extension import Extension
from agent import LoopData
from helpers import files, settings, update_check
import datetime
import json
# check for newer versions of A0 available and send notification
# check after user message is sent from UI, not API, MCP etc. (user is active and can see the notification)
# do not check too often, use cooldown
# do not notify too often
last_check = datetime.datetime.fromtimestamp(0)
check_cooldown_seconds = 60
last_notification_id = None
last_notification_time = datetime.datetime.fromtimestamp(0)
notification_cooldown_seconds = 60 * 60 * 24
notification_state_file = "usr/update-check-state.json"
def _load_notification_state() -> dict:
try:
return json.loads(files.read_file(notification_state_file))
except Exception:
return {}
def _parse_timestamp(value: str | None) -> datetime.datetime | None:
if not value:
return None
try:
parsed = datetime.datetime.fromisoformat(value)
except ValueError:
return None
if parsed.tzinfo:
return parsed.astimezone(datetime.timezone.utc).replace(tzinfo=None)
return parsed
def _remember_notification(notif: dict, now: datetime.datetime):
state = {
"last_notification_at": now.replace(tzinfo=datetime.timezone.utc).isoformat(),
"last_notification_id": notif.get("id") or "",
"last_notification_group": notif.get("group", "update_check"),
}
files.write_file(notification_state_file, json.dumps(state, indent=2))
class UpdateCheck(Extension):
async def execute(self, loop_data: LoopData = LoopData(), text: str = "", **kwargs):
if not self.agent:
return
try:
global last_check, last_notification_id, last_notification_time
# first check if update check is enabled
current_settings = settings.get_settings()
if not current_settings["update_check_enabled"]:
return
# check if cooldown has passed
if (datetime.datetime.now() - last_check).total_seconds() < check_cooldown_seconds:
return
last_check = datetime.datetime.now()
# check for updates
version = await update_check.check_version()
# if the user should update, send notification
if notif := version.get("notification"):
now = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None)
stored_state = _load_notification_state()
stored_notification_time = _parse_timestamp(stored_state.get("last_notification_at"))
effective_notification_time = stored_notification_time or last_notification_time
if (now - effective_notification_time).total_seconds() > notification_cooldown_seconds:
last_notification_id = notif.get("id")
last_notification_time = now
try:
_remember_notification(notif, now)
except Exception:
pass
self.send_notification(notif)
except Exception as e:
pass # no need to log if the update server is inaccessible
def send_notification(self, notif):
if not self.agent:
return
notifs = self.agent.context.get_notification_manager()
notifs.send_notification(
title=notif.get("title", "Newer version available"),
message=notif.get("message", "A newer version of Agent Zero is available. Please update to the latest version."),
type=notif.get("type", "info"),
detail=notif.get("detail", ""),
display_time=notif.get("display_time", 10),
group=notif.get("group", "update_check"),
priority=notif.get("priority", notification.NotificationPriority.NORMAL),
id=notif.get("id", "update_check_available"),
)