feat(code): persistent banner when installation is stale (#4459)

Deep Agents Code now shows a persistent header banner when your
installed version is more than two weeks old.

---

Informs users when their `dcode` installation is more than two weeks old
by reusing the existing persistent header bar (`_StaticHeader`,
previously only shown behind `DEEPAGENTS_CODE_SHOW_HEADER` and used for
the sandbox indicator). When the installed version's release is `>= 14`
days old, the header is rendered even without the env var and its
subtitle carries an advisory that overrides the sandbox label. Install
age is derived cache-only (no startup network calls), and the check is
suppressed for editable/dev installs and when update checks are
disabled.

Made by [Open
SWE](https://openswe.vercel.app/agents/cca5fce9-67e0-cf5d-377f-d9acf912ed65)

## References
- Plan:
https://openswe.vercel.app/agents/cca5fce9-67e0-cf5d-377f-d9acf912ed65/plan

---------

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
This commit is contained in:
Mason Daugherty
2026-07-02 23:45:00 -04:00
committed by GitHub
parent d999181e84
commit b74c18591a
4 changed files with 348 additions and 5 deletions
+36 -5
View File
@@ -1985,7 +1985,8 @@ class DeepAgentsApp(App):
defer_server_start: Whether to keep app-owned server startup paused
until the user configures credentials or explicitly picks a model.
title: Override the Textual `App.title` shown in the optional
header bar.
header bar (shown when `DEEPAGENTS_CODE_SHOW_HEADER` is set or
the installation is stale).
When `None`, the class-level `TITLE` is used.
@@ -1993,7 +1994,8 @@ class DeepAgentsApp(App):
sub_title: Override the Textual `App.sub_title` shown in the
optional header bar.
When `None`, the parent default is used.
When `None`, a sandbox label or a stale-install advisory may be
substituted; otherwise the parent default is used.
Reassigning `app.sub_title` at runtime updates the header live.
**kwargs: Additional arguments passed to parent
@@ -2263,6 +2265,34 @@ class DeepAgentsApp(App):
)
self.sub_title = f"Sandbox: {display}"
from deepagents_code.update_check import (
installed_days_old,
is_installation_stale,
)
self._installation_stale: bool = sub_title is None and is_installation_stale()
"""Whether the installed version is old enough to force the header banner.
Set once at construction from the cache-only install-age check. When
`True`, `compose` renders the header even without `DEEPAGENTS_CODE_SHOW_HEADER`
and the subtitle carries the advisory below overriding the sandbox
subtitle by design.
"""
if self._installation_stale:
days = installed_days_old()
if days is None:
# The cache changed between the staleness gate and this read
# (e.g. a concurrent `dcode` process). Drop the banner rather
# than render "installed version is None days old".
self._installation_stale = False
else:
unit = "day" if days == 1 else "days"
self.sub_title = (
f"Update available \u2014 installed version is "
f"{days} {unit} old (run /update)"
)
# Per-turn model overrides
self._model_override: str | None = None
"""Per-turn model override set via `/model`; `None` uses session default."""
@@ -2770,7 +2800,7 @@ class DeepAgentsApp(App):
"""
from deepagents_code._env_vars import SHOW_HEADER, is_env_truthy
if is_env_truthy(SHOW_HEADER):
if is_env_truthy(SHOW_HEADER) or self._installation_stale:
yield _StaticHeader(id="app-header")
# Main chat area with scrollable messages
# VerticalScroll tracks user scroll intent for better auto-scroll behavior.
@@ -16343,8 +16373,9 @@ async def run_textual_app(
defer_server_start: Whether to keep app-owned server startup paused
until credentials or a model are configured from inside the TUI.
title: Override the Textual `App.title` shown in the optional header
bar (gated on `DEEPAGENTS_CODE_SHOW_HEADER`). When `None`, the
default `"Deep Agents"` is used.
bar (gated on `DEEPAGENTS_CODE_SHOW_HEADER`, or shown automatically
when the installation is stale). When `None`, the default
`"Deep Agents"` is used.
sub_title: Override the Textual `App.sub_title` shown in the optional
header bar.
+45
View File
@@ -69,6 +69,9 @@ call to PyPI; older payloads trigger a fresh fetch. Set conservatively at
INSTALLED_AGE_NOTICE_DAYS = 7
"""Minimum installed-version age before update notices call it out explicitly."""
INSTALLED_STALE_NOTICE_DAYS = 14
"""Minimum installed-version age (days) before the stale-install banner shows."""
_SDK_RELEASE_TIMES_KEY = "sdk_release_times"
"""`CACHE_FILE` key for cached SDK upload timestamps, keyed by version string."""
@@ -709,6 +712,48 @@ def format_installed_age_suffix(version: str | None) -> str:
return f" ({days} {unit} old)"
def installed_days_old() -> int | None:
"""Return whole days since the installed version's release, or `None`.
Cache-only (`get_release_time`), so it never blocks on the network.
Returns `None` when the release time is unknown (cold cache or an
unparseable timestamp). Best-effort: any unexpected error degrades to
`None` rather than propagating, since this runs on the startup path.
"""
try:
return _days_old_from_iso(get_release_time(__version__))
except Exception:
logger.debug("Failed to compute installed version age", exc_info=True)
return None
def is_installation_stale() -> bool:
"""Return whether an older installed version should warn about updating.
`True` only when a fresh cache says an update is available and the installed
version's release is at least `INSTALLED_STALE_NOTICE_DAYS` old. Returns
`False` for editable/dev installs (release age is meaningless there), when
update checks are disabled (the opt-out silences this too), and when the age
or update answer is unknown.
Best-effort: any unexpected error degrades to `False` rather than
propagating, so this cosmetic check can never abort TUI startup.
"""
from deepagents_code.config import _is_editable_install
try:
if _is_editable_install() or not is_update_check_enabled():
return False
available, _ = get_cached_update_available()
if not available:
return False
days = installed_days_old()
except Exception:
logger.debug("Failed to determine installation staleness", exc_info=True)
return False
return days is not None and days >= INSTALLED_STALE_NOTICE_DAYS
def get_sdk_release_time(
version: str | None, *, bypass_cache: bool = False
) -> str | None:
+115
View File
@@ -17074,6 +17074,121 @@ class TestSandboxSubTitle:
assert app.sub_title == "Sandbox: Kubernetes"
class TestStaleInstallBanner:
"""The persistent header banner surfaces stale installations."""
_MESSAGE_21 = (
"Update available \u2014 installed version is 21 days old (run /update)"
)
async def test_header_mounted_when_stale_without_env_var(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
"""A stale install shows the header even without the env var."""
monkeypatch.delenv("DEEPAGENTS_CODE_SHOW_HEADER", raising=False)
from textual.widgets import Header
with (
patch(
"deepagents_code.update_check.is_installation_stale",
return_value=True,
),
patch(
"deepagents_code.update_check.installed_days_old",
return_value=21,
),
):
app = DeepAgentsApp()
assert app._installation_stale is True
assert app.sub_title == self._MESSAGE_21
async with app.run_test() as pilot:
await pilot.pause()
assert len(app.query(Header)) == 1
async def test_header_absent_when_not_stale(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
"""A fresh install keeps the header hidden without the env var."""
monkeypatch.delenv("DEEPAGENTS_CODE_SHOW_HEADER", raising=False)
from textual.widgets import Header
with patch(
"deepagents_code.update_check.is_installation_stale",
return_value=False,
):
app = DeepAgentsApp()
assert app._installation_stale is False
async with app.run_test() as pilot:
await pilot.pause()
assert not app.query(Header)
async def test_stale_overrides_sandbox_sub_title(self) -> None:
"""The stale-install advisory takes precedence over the sandbox label."""
with (
patch(
"deepagents_code.update_check.is_installation_stale",
return_value=True,
),
patch(
"deepagents_code.update_check.installed_days_old",
return_value=21,
),
):
app = DeepAgentsApp(server_kwargs={"sandbox_type": "modal"})
assert app.sub_title == self._MESSAGE_21
async def test_explicit_sub_title_suppresses_stale_banner(self) -> None:
"""An explicitly passed sub_title is never overwritten by the advisory."""
with patch(
"deepagents_code.update_check.is_installation_stale",
return_value=True,
):
app = DeepAgentsApp(sub_title="custom")
assert app._installation_stale is False
assert app.sub_title == "custom"
async def test_singular_day_wording(self) -> None:
"""The day count is pluralized correctly."""
with (
patch(
"deepagents_code.update_check.is_installation_stale",
return_value=True,
),
patch(
"deepagents_code.update_check.installed_days_old",
return_value=1,
),
):
app = DeepAgentsApp()
assert app.sub_title == (
"Update available \u2014 installed version is 1 day old (run /update)"
)
async def test_none_days_drops_banner(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
"""A `None` age (cache race) drops the banner, never `"None days old"`."""
monkeypatch.delenv("DEEPAGENTS_CODE_SHOW_HEADER", raising=False)
from textual.widgets import Header
with (
patch(
"deepagents_code.update_check.is_installation_stale",
return_value=True,
),
patch(
"deepagents_code.update_check.installed_days_old",
return_value=None,
),
):
app = DeepAgentsApp()
assert app._installation_stale is False
assert "None" not in (app.sub_title or "")
async with app.run_test() as pilot:
await pilot.pause()
assert not app.query(Header)
class TestHandleExternalSignal:
"""Verify routing of `kind=signal` external events."""
@@ -20,6 +20,7 @@ from deepagents_code._version import __version__
from deepagents_code.extras_info import ExtrasIntrospectionError, installed_extra_names
from deepagents_code.update_check import (
CACHE_TTL,
INSTALLED_STALE_NOTICE_DAYS,
DependencyChange,
InstallMethod,
ShadowedDcode,
@@ -61,8 +62,10 @@ from deepagents_code.update_check import (
install_extra_recovery_command,
install_extras_command,
install_package_command,
installed_days_old,
is_auto_update_enabled,
is_auto_update_explicitly_set,
is_installation_stale,
is_installed_version_at_least,
is_update_available,
is_valid_extra_name,
@@ -1173,6 +1176,155 @@ class TestFormatInstalledAgeSuffix:
assert format_installed_age_suffix("1.0.0") == ""
def _write_installed_release_time(
cache_file: Path, *, days_ago: int, latest_version: str | None = None
) -> None:
"""Seed the cache with a release time for the running version."""
from datetime import UTC, datetime, timedelta
iso = (datetime.now(tz=UTC) - timedelta(days=days_ago)).isoformat()
data: dict[str, object] = {
"release_times": {__version__: iso},
"checked_at": time.time(),
}
if latest_version is not None:
data["version"] = latest_version
cache_file.write_text(
json.dumps(data),
encoding="utf-8",
)
class TestInstalledDaysOld:
def test_returns_days_for_known_release(self, cache_file) -> None:
_write_installed_release_time(cache_file, days_ago=21)
assert installed_days_old() == 21
def test_unknown_release_returns_none(self, cache_file) -> None: # noqa: ARG002
assert installed_days_old() is None
def test_malformed_timestamp_returns_none(self, cache_file: Path) -> None:
cache_file.write_text(
json.dumps(
{
"release_times": {__version__: "not-a-timestamp"},
"checked_at": time.time(),
}
),
encoding="utf-8",
)
assert installed_days_old() is None
def test_none_when_release_lookup_raises(self, cache_file) -> None: # noqa: ARG002
"""An unexpected error degrades to `None` instead of propagating."""
with patch(
"deepagents_code.update_check.get_release_time",
side_effect=OSError("boom"),
):
assert installed_days_old() is None
class TestIsInstallationStale:
def test_true_when_older_than_threshold(self, cache_file) -> None:
_write_installed_release_time(
cache_file,
days_ago=INSTALLED_STALE_NOTICE_DAYS + 1,
latest_version="99.0.0",
)
with (
patch("deepagents_code.config._is_editable_install", return_value=False),
patch(
"deepagents_code.update_check.is_update_check_enabled",
return_value=True,
),
):
assert is_installation_stale() is True
def test_false_when_current_version_is_old(self, cache_file) -> None:
_write_installed_release_time(
cache_file,
days_ago=INSTALLED_STALE_NOTICE_DAYS + 30,
latest_version=__version__,
)
with (
patch("deepagents_code.config._is_editable_install", return_value=False),
patch(
"deepagents_code.update_check.is_update_check_enabled",
return_value=True,
),
):
assert is_installation_stale() is False
def test_true_at_exact_threshold(self, cache_file) -> None:
"""Exactly `INSTALLED_STALE_NOTICE_DAYS` old is stale (inclusive `>=`)."""
_write_installed_release_time(
cache_file,
days_ago=INSTALLED_STALE_NOTICE_DAYS,
latest_version="99.0.0",
)
with (
patch("deepagents_code.config._is_editable_install", return_value=False),
patch(
"deepagents_code.update_check.is_update_check_enabled",
return_value=True,
),
):
assert is_installation_stale() is True
def test_false_when_newer_than_threshold(self, cache_file) -> None:
_write_installed_release_time(
cache_file,
days_ago=INSTALLED_STALE_NOTICE_DAYS - 1,
latest_version="99.0.0",
)
with (
patch("deepagents_code.config._is_editable_install", return_value=False),
patch(
"deepagents_code.update_check.is_update_check_enabled",
return_value=True,
),
):
assert is_installation_stale() is False
def test_false_when_editable_check_raises(self, cache_file) -> None: # noqa: ARG002
"""A raising editable check degrades to `False`, never aborting startup."""
with patch(
"deepagents_code.config._is_editable_install",
side_effect=PermissionError("direct_url.json unreadable"),
):
assert is_installation_stale() is False
def test_false_for_editable_install(self, cache_file) -> None:
_write_installed_release_time(
cache_file, days_ago=INSTALLED_STALE_NOTICE_DAYS + 30
)
with patch("deepagents_code.config._is_editable_install", return_value=True):
assert is_installation_stale() is False
def test_false_when_update_checks_disabled(self, cache_file) -> None:
_write_installed_release_time(
cache_file, days_ago=INSTALLED_STALE_NOTICE_DAYS + 30
)
with (
patch("deepagents_code.config._is_editable_install", return_value=False),
patch(
"deepagents_code.update_check.is_update_check_enabled",
return_value=False,
),
):
assert is_installation_stale() is False
def test_false_on_cold_cache(self, cache_file) -> None: # noqa: ARG002
with (
patch("deepagents_code.config._is_editable_install", return_value=False),
patch(
"deepagents_code.update_check.is_update_check_enabled",
return_value=True,
),
):
assert is_installation_stale() is False
class TestDetectInstallMethod:
def test_non_editable_non_uv_non_brew_returns_other(self) -> None:
"""The fallback bucket is not a positive pip detection."""