Merge pull request #52828 from helix4u/fix/desktop-backend-update-indicator

fix(desktop): show remote backend updates without counts
This commit is contained in:
brooklyn! 2026-06-26 11:49:07 -05:00 committed by GitHub
commit 5cc4009deb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 39 additions and 6 deletions

View file

@ -231,10 +231,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}`
@ -243,13 +245,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 ? <Loader2 className="size-3 animate-spin" /> : <Hash className="size-3" />,
id: 'version-backend',
@ -262,6 +265,7 @@ export function useStatusbarItems({
connection?.mode,
statusSnapshot?.version,
backendUpdateStatus?.behind,
backendUpdateStatus?.updateAvailable,
backendUpdateApply.applying,
backendUpdateApply.message,
backendUpdateApply.stage,

View file

@ -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'
@ -119,6 +120,7 @@ export function UpdatesOverlay() {
onRetryCheck={() => void check()}
status={status}
target={target}
updateAvailable={updateAvailable}
/>
)}
</DialogContent>
@ -134,7 +136,8 @@ function IdleView({
onLater,
onRetryCheck,
status,
target
target,
updateAvailable
}: {
behind: number
checking: boolean
@ -144,6 +147,7 @@ function IdleView({
onRetryCheck: () => void
status: DesktopUpdateStatus | null
target: UpdateTarget
updateAvailable: boolean
}) {
const { t } = useI18n()
const u = t.updates
@ -196,7 +200,7 @@ function IdleView({
)
}
if (behind === 0) {
if (!updateAvailable) {
return (
<CenteredStatus
body={target === 'backend' ? u.latestBodyBackend : u.latestBody}

View file

@ -273,6 +273,7 @@ export interface DesktopUpdateCommit {
export interface DesktopUpdateStatus {
supported: boolean
updateAvailable?: boolean
branch?: string
currentBranch?: string
reason?: string

View file

@ -200,11 +200,31 @@ describe('checkBackendUpdates', () => {
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({

View file

@ -249,6 +249,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,

View file

@ -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

View file

@ -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