From 1c8594b634e4b485341ef471dd7b4dd16391c0aa Mon Sep 17 00:00:00 2001 From: helix4u <4317663+helix4u@users.noreply.github.com> Date: Thu, 25 Jun 2026 21:39:29 -0600 Subject: [PATCH] fix(desktop): show remote backend updates without counts --- .../app/shell/hooks/use-statusbar-items.tsx | 8 ++++++-- apps/desktop/src/app/updates-overlay.tsx | 8 ++++++-- apps/desktop/src/global.d.ts | 1 + apps/desktop/src/store/updates.test.ts | 20 +++++++++++++++++++ apps/desktop/src/store/updates.ts | 1 + hermes_cli/banner.py | 5 ++++- tests/hermes_cli/test_update_check.py | 2 +- 7 files changed, 39 insertions(+), 6 deletions(-) diff --git a/apps/desktop/src/app/shell/hooks/use-statusbar-items.tsx b/apps/desktop/src/app/shell/hooks/use-statusbar-items.tsx index 3d1f27eb205..eb0fe8bee25 100644 --- a/apps/desktop/src/app/shell/hooks/use-statusbar-items.tsx +++ b/apps/desktop/src/app/shell/hooks/use-statusbar-items.tsx @@ -241,10 +241,12 @@ export function useStatusbarItems({ const backendVersion = statusSnapshot?.version const behind = backendUpdateStatus?.behind ?? 0 + const updateAvailable = backendUpdateStatus?.updateAvailable || behind > 0 const applying = backendUpdateApply.applying || backendUpdateApply.stage === 'restart' const base = copy.backendLabel(backendVersion ?? copy.unknown) - const behindHint = !applying && behind > 0 ? ` (+${behind})` : '' + const behindHint = + !applying && behind > 0 ? ` (+${behind})` : !applying && updateAvailable ? ` (${copy.update})` : '' const label = applying ? `${base} · ${backendUpdateApply.stage === 'restart' ? copy.restart : copy.update}` @@ -253,13 +255,14 @@ export function useStatusbarItems({ const tooltip = [ applying ? backendUpdateApply.message || copy.updateInProgress : null, !applying && behind > 0 && copy.commitsBehind(behind, 'main'), + !applying && behind <= 0 && updateAvailable && copy.update, backendVersion && copy.backendVersion(backendVersion) ] .filter(Boolean) .join(' · ') return { - className: !applying && behind > 0 ? 'text-primary hover:text-primary' : undefined, + className: !applying && updateAvailable ? 'text-primary hover:text-primary' : undefined, hidden: !backendVersion, icon: applying ? : , id: 'version-backend', @@ -272,6 +275,7 @@ export function useStatusbarItems({ connection?.mode, statusSnapshot?.version, backendUpdateStatus?.behind, + backendUpdateStatus?.updateAvailable, backendUpdateApply.applying, backendUpdateApply.message, backendUpdateApply.stage, diff --git a/apps/desktop/src/app/updates-overlay.tsx b/apps/desktop/src/app/updates-overlay.tsx index b4c0f30cebc..6ec059ded1a 100644 --- a/apps/desktop/src/app/updates-overlay.tsx +++ b/apps/desktop/src/app/updates-overlay.tsx @@ -60,6 +60,7 @@ export function UpdatesOverlay() { }, [check, checking, open, status]) const behind = status?.behind ?? 0 + const updateAvailable = status?.updateAvailable || behind > 0 const phase: 'idle' | 'applying' | 'manual' | 'guiSkew' | 'error' = apply.stage === 'manual' @@ -124,6 +125,7 @@ export function UpdatesOverlay() { onRetryCheck={() => void check()} status={status} target={target} + updateAvailable={updateAvailable} /> )} @@ -139,7 +141,8 @@ function IdleView({ onLater, onRetryCheck, status, - target + target, + updateAvailable }: { behind: number checking: boolean @@ -149,6 +152,7 @@ function IdleView({ onRetryCheck: () => void status: DesktopUpdateStatus | null target: UpdateTarget + updateAvailable: boolean }) { const { t } = useI18n() const u = t.updates @@ -198,7 +202,7 @@ function IdleView({ ) } - if (behind === 0) { + if (!updateAvailable) { return ( { expect(checkHermesUpdateSpy).toHaveBeenCalled() expect(result?.behind).toBe(2) + expect(result?.updateAvailable).toBe(true) expect(result?.commits?.[0]?.sha).toBe('abc1234') expect(result?.supported).toBe(true) expect($backendUpdateStatus.get()?.commits?.[0]?.summary).toBe('feat: x') }) + it('preserves backend update_available when the backend cannot count commits', async () => { + setRemote(true) + checkHermesUpdateSpy.mockResolvedValue({ + install_method: 'nixos', + current_version: '0.16.0', + behind: -1, + update_available: true, + can_apply: false, + update_command: 'managed outside dashboard', + message: 'Update available.' + }) + + const result = await checkBackendUpdates() + + expect(result?.behind).toBe(0) + expect(result?.updateAvailable).toBe(true) + expect(result?.targetSha).toBe('backend:0.16.0') + }) + it('honours can_apply=false (docker/nix): not supported, carries message', async () => { setRemote(true) checkHermesUpdateSpy.mockResolvedValue({ diff --git a/apps/desktop/src/store/updates.ts b/apps/desktop/src/store/updates.ts index 86cf75b4a9b..6591c81bb72 100644 --- a/apps/desktop/src/store/updates.ts +++ b/apps/desktop/src/store/updates.ts @@ -247,6 +247,7 @@ function mapBackendCheck(res: BackendUpdateCheckResponse): DesktopUpdateStatus { return { supported: res.can_apply, message: res.message ?? undefined, + updateAvailable: res.update_available, behind: behind > 0 ? behind : 0, targetSha: res.update_available ? `backend:${res.current_version}` : undefined, commits: res.commits, diff --git a/hermes_cli/banner.py b/hermes_cli/banner.py index 68d33e43fdb..df127c54b8a 100644 --- a/hermes_cli/banner.py +++ b/hermes_cli/banner.py @@ -197,7 +197,10 @@ def _check_via_local_git(repo_dir: Path) -> Optional[int]: origin_url = _git_stdout(["remote", "get-url", "origin"], cwd=repo_dir) if _is_official_ssh_remote(origin_url): head_rev = _git_stdout(["rev-parse", "HEAD"], cwd=repo_dir) - return _check_via_rev(head_rev) if head_rev else None + checked = _check_via_rev(head_rev) if head_rev else None + if checked == UPDATE_AVAILABLE_NO_COUNT: + return 1 + return checked # Installer checkouts are shallow (`git clone --depth 1`). On a shallow # clone the history stops at a single commit, so a plain `git fetch` would diff --git a/tests/hermes_cli/test_update_check.py b/tests/hermes_cli/test_update_check.py index 66c40a5ab17..84b9e3a6c99 100644 --- a/tests/hermes_cli/test_update_check.py +++ b/tests/hermes_cli/test_update_check.py @@ -125,7 +125,7 @@ def test_check_for_updates_official_ssh_origin_uses_https_probe(tmp_path): with patch("hermes_cli.banner.subprocess.run", side_effect=fake_run): result = banner._check_via_local_git(repo_dir) - assert result == banner.UPDATE_AVAILABLE_NO_COUNT + assert result == 1 assert ["git", "fetch", "origin", "--quiet"] not in calls