diff --git a/nanobot/webui/settings_api.py b/nanobot/webui/settings_api.py
index 1f663a121..0e799def8 100644
--- a/nanobot/webui/settings_api.py
+++ b/nanobot/webui/settings_api.py
@@ -34,9 +34,18 @@ from nanobot.webui.workspaces import (
write_webui_default_access_mode,
)
+from nanobot import __version__
+
QueryParams = dict[str, list[str]]
RuntimeSurface = Literal["browser", "native"]
+
+def _version_payload() -> dict[str, Any]:
+ """Return version info for the settings payload."""
+ return {
+ "current": __version__,
+ }
+
_RUNTIME_CAPABILITIES = {
"can_restart_engine": False,
"can_pick_folder": False,
@@ -805,6 +814,7 @@ def settings_payload(
"exec_path_append_set": bool(exec_config.path_append),
},
"requires_restart": requires_restart,
+ "version": _version_payload(),
}
return decorate_settings_payload(
payload,
diff --git a/nanobot/webui/settings_routes.py b/nanobot/webui/settings_routes.py
index b8dbb4b73..017652331 100644
--- a/nanobot/webui/settings_routes.py
+++ b/nanobot/webui/settings_routes.py
@@ -36,6 +36,7 @@ from nanobot.webui.settings_api import (
update_transcription_settings,
update_web_search_settings,
)
+from nanobot.webui.version_check import check_for_update
QueryParams = dict[str, list[str]]
@@ -117,6 +118,8 @@ class WebUISettingsRouter:
return await self._handle_settings_cli_apps_action(request, "test")
if path == "/api/settings/mcp-presets":
return await self._handle_settings_mcp_presets(request)
+ if path == "/api/settings/version-check":
+ return await self._handle_settings_version_check(request)
mcp_action = _MCP_PRESET_ACTIONS_BY_PATH.get(path)
if mcp_action is not None:
return await self._handle_settings_mcp_presets(request, mcp_action)
@@ -347,3 +350,15 @@ class WebUISettingsRouter:
if action is None:
return self._json_response(payload)
return self._json_response(self._with_restart_state(payload, section="runtime"))
+
+ async def _handle_settings_version_check(self, request: WsRequest) -> Response:
+ if not self._authorized(request):
+ return self._unauthorized()
+ try:
+ update_info = await asyncio.to_thread(check_for_update)
+ except Exception:
+ self.logger.exception("version check failed")
+ return self._error_response(500, "version check failed")
+ return self._json_response({
+ "updateAvailable": update_info,
+ })
diff --git a/nanobot/webui/version_check.py b/nanobot/webui/version_check.py
new file mode 100644
index 000000000..6db45c630
--- /dev/null
+++ b/nanobot/webui/version_check.py
@@ -0,0 +1,51 @@
+"""On-demand version checker for nanobot-ai releases.
+
+Checks PyPI for newer versions when explicitly requested (no background polling).
+"""
+
+from __future__ import annotations
+
+import logging
+import time
+from typing import Any
+
+import httpx
+
+from nanobot import __version__
+
+logger = logging.getLogger(__name__)
+
+_PYPI_URL = "https://pypi.org/pypi/nanobot-ai/json"
+_CACHE_TTL_S = 300 # 5 minutes cache to avoid hammering PyPI
+
+_cache: tuple[float, str | None] = (0.0, None)
+
+
+def check_for_update() -> dict[str, Any] | None:
+ """Check PyPI for a newer version. Returns update info dict or None if up-to-date.
+
+ Uses a short cache to avoid repeated requests within the TTL window.
+ This is a blocking call — invoke from a thread or background task.
+ """
+ global _cache
+ now = time.monotonic()
+ cached_at, cached_val = _cache
+ if now - cached_at < _CACHE_TTL_S and cached_val is not None:
+ latest = cached_val
+ else:
+ try:
+ resp = httpx.get(_PYPI_URL, timeout=5.0, follow_redirects=True)
+ resp.raise_for_status()
+ latest = resp.json().get("info", {}).get("version")
+ except Exception:
+ logger.debug("PyPI version check failed", exc_info=True)
+ return None
+ _cache = (now, latest)
+
+ if not latest or latest == __version__:
+ return None
+ return {
+ "currentVersion": __version__,
+ "latestVersion": latest,
+ "pypiUrl": "https://pypi.org/project/nanobot-ai/",
+ }
diff --git a/webui/src/components/settings/SettingsView.tsx b/webui/src/components/settings/SettingsView.tsx
index 0a6ebcf5a..b1ea148d5 100644
--- a/webui/src/components/settings/SettingsView.tsx
+++ b/webui/src/components/settings/SettingsView.tsx
@@ -10,6 +10,7 @@ import {
} from "react";
import {
Activity,
+ ArrowUpCircle,
Bot,
Brain,
Check,
@@ -22,6 +23,7 @@ import {
Database,
Eye,
EyeOff,
+ ExternalLink,
Gem,
Globe2,
Grid3X3,
@@ -75,6 +77,7 @@ import {
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import {
+ checkVersion,
createModelConfiguration,
fetchSettings,
fetchSettingsUsage,
@@ -1852,6 +1855,104 @@ function OverviewSettings({
/>
+
+