From 4c82b2bf194ff2394f35f0f9c8003f00fa56fd9b Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Tue, 21 Jul 2026 14:36:36 +0200 Subject: [PATCH] Dashboard: indicate when a newer Serena version is available --- CHANGELOG.md | 1 + src/serena/dashboard.py | 26 ++++++++++++++++ src/serena/resources/dashboard/dashboard.css | 18 +++++++++++ src/serena/resources/dashboard/dashboard.js | 7 ++++- src/serena/util/pypi.py | 32 ++++++++++++++++++++ 5 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 src/serena/util/pypi.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 6902a65f..002f2ab7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,6 +70,7 @@ Status of the `main` branch. Changes prior to the next official version change w when using plugin version 2023.3.3+ * Dashboard: + - The version display now indicates when a newer Serena version is available - Fix: the "Last Execution" panel stayed on "Loading..." forever when there was no logged execution (e.g. a fresh server / no tool run yet), because `loadLastExecution()` only rendered the panel when the backend returned a non-null execution; the empty state is now rendered via the existing diff --git a/src/serena/dashboard.py b/src/serena/dashboard.py index b5a85495..80415b45 100644 --- a/src/serena/dashboard.py +++ b/src/serena/dashboard.py @@ -10,6 +10,7 @@ import urllib.error import urllib.request from collections.abc import Callable from dataclasses import dataclass +from functools import cached_property from html import escape from pathlib import Path from typing import TYPE_CHECKING, Any, Optional, Self @@ -26,7 +27,9 @@ from serena.config.serena_config import SerenaConfig, SerenaPaths from serena.constants import SERENA_DASHBOARD_DIR, SerenaPorts from serena.task_executor import TaskExecutor from serena.util.logging import MemoryLogHandler +from serena.util.pypi import PyPIPackageInfo from serena.util.pywebview import WebViewWithTray +from serena.util.version import Version if TYPE_CHECKING: from serena.agent import SerenaAgent @@ -71,6 +74,7 @@ class ResponseConfigOverview(BaseModel): encoding: str | None current_client: str | None serena_version: str + newer_serena_version: str | None class ResponseAvailableLanguages(BaseModel): @@ -224,6 +228,27 @@ class SerenaDashboardAPI: def memory_log_handler(self) -> MemoryLogHandler: return self._memory_log_handler + @cached_property + def _newer_serena_version(self) -> str | None: + """The version of a newer serena-agent package release on PyPI, if any. + + The result is computed only once (on first access) and cached thereafter. + + :return: the latest version available on PyPI if it is newer than the running version, else None + """ + try: + # query PyPI for the latest released version + latest_serena_version = PyPIPackageInfo("serena-agent").get_latest_version(timeout_secs=3) + + # compare against the running version + latest_version = Version(latest_serena_version) + current_version = Version(self._agent.version) + if not current_version.is_at_least(*latest_version.components): + return latest_serena_version + except Exception as e: + log.warning("Failed to check for newer Serena version on PyPI: %s", e) + return None + def _setup_routes(self) -> None: @self._app.route("/") def redirect_to_dashboard() -> Response: @@ -606,6 +631,7 @@ class SerenaDashboardAPI: encoding=encoding, current_client=Tool.get_last_tool_call_client_str(), serena_version=self._agent.version, + newer_serena_version=self._newer_serena_version, ) def _on_agent_config_changed(self) -> None: diff --git a/src/serena/resources/dashboard/dashboard.css b/src/serena/resources/dashboard/dashboard.css index fb2d70da..f8e4e1af 100644 --- a/src/serena/resources/dashboard/dashboard.css +++ b/src/serena/resources/dashboard/dashboard.css @@ -556,6 +556,24 @@ code, pre, kbd, samp, color: var(--text-primary); } +.newer-version-badge { + display: inline-flex; + align-items: center; + padding: 2px 10px; + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 6px; + color: var(--text-primary); + font-size: 12px; + font-weight: 500; + text-decoration: none; + cursor: pointer; +} + +.newer-version-badge:hover { + background: var(--btn-secondary-hover); +} + .config-list { list-style: none; padding: 0; diff --git a/src/serena/resources/dashboard/dashboard.js b/src/serena/resources/dashboard/dashboard.js index 571df521..74308c93 100644 --- a/src/serena/resources/dashboard/dashboard.js +++ b/src/serena/resources/dashboard/dashboard.js @@ -618,7 +618,12 @@ class Dashboard { let html = '
'; - html += '
Version:
' + config.serena_version + '
'; + html += '
Version:
' + config.serena_version; + if (config.newer_serena_version) { + html += ' ' + + 'newer version available: ' + config.newer_serena_version + ''; + } + html += '
'; // Project info html += '
Active Project:
'; diff --git a/src/serena/util/pypi.py b/src/serena/util/pypi.py new file mode 100644 index 00000000..3ac0c2c4 --- /dev/null +++ b/src/serena/util/pypi.py @@ -0,0 +1,32 @@ +"""Utilities for querying package information from the Python Package Index (PyPI).""" + +import requests + + +class PyPIPackageInfo: + """Provider of information on a package hosted on the Python Package Index (PyPI). + + Information is retrieved on demand via PyPI's JSON API. + """ + + JSON_API_URL_TEMPLATE = "https://pypi.org/pypi/%s/json" + + def __init__(self, package_name: str) -> None: + """ + :param package_name: the name of the package on PyPI (e.g. "requests") + """ + self.package_name = package_name + + def get_latest_version(self, timeout_secs: int = 5) -> str: + """Retrieves the latest released version of the package from PyPI. + + :return: the latest version string (e.g. "2.33.0") + :raises requests.RequestException: if the HTTP request fails or PyPI responds with an error status + """ + # query PyPI's JSON API for the package metadata + url = self.JSON_API_URL_TEMPLATE % self.package_name + response = requests.get(url, timeout=timeout_secs) + response.raise_for_status() + + # extract the latest version from the metadata + return response.json()["info"]["version"]