fix(desktop): split client and backend into two distinct update buttons

The status bar merged both versions into one pill with a single click target,
so there was no way to tell which artifact an update acted on — and the apply
path was overloaded by connection mode. Separate them:

- store: independent client (checkUpdates/applyUpdates) and backend
  (checkBackendUpdates/applyBackendUpdate) flows with their own status/apply
  atoms; openUpdateOverlayFor(target) drives the overlay.
- status bar: two buttons — client vX (always) and backend vY (+N) (remote
  only), each with its own behind-count, opening the overlay for its target.
- overlay: reads the active target's atoms; install/check route per target.

Removes the version-bar merge helper (no longer merging the two versions).
This commit is contained in:
yoniebans
2026-06-08 08:58:26 -07:00
committed by Teknium
parent 9c264555b0
commit 56be1a63a3
6 changed files with 139 additions and 283 deletions
@@ -41,8 +41,14 @@ import {
setYoloActive
} from '@/store/session'
import { $subagentsBySession, activeSubagentCount } from '@/store/subagents'
import { $desktopVersion, $updateApply, $updateStatus, setUpdateOverlayOpen } from '@/store/updates'
import { resolveVersionBar } from '@/lib/version-bar'
import {
$backendUpdateApply,
$backendUpdateStatus,
$desktopVersion,
$updateApply,
$updateStatus,
openUpdateOverlayFor
} from '@/store/updates'
import type { StatusResponse } from '@/types/hermes'
import { CRON_ROUTE } from '../../routes'
@@ -99,6 +105,8 @@ export function useStatusbarItems({
const subagentsBySession = useStore($subagentsBySession)
const updateStatus = useStore($updateStatus)
const updateApply = useStore($updateApply)
const backendUpdateStatus = useStore($backendUpdateStatus)
const backendUpdateApply = useStore($backendUpdateApply)
const desktopVersion = useStore($desktopVersion)
const connection = useStore($connection)
@@ -197,36 +205,25 @@ export function useStatusbarItems({
? 'text-amber-600 hover:text-amber-600'
: 'text-destructive hover:text-destructive'
const versionItem = useMemo<StatusbarItem>(() => {
const clientVersionItem = useMemo<StatusbarItem>(() => {
const appVersion = desktopVersion?.appVersion
const sha = updateStatus?.currentSha?.slice(0, 7) ?? null
const behind = updateStatus?.behind ?? 0
const applying = updateApply.applying || updateApply.stage === 'restart'
const remote = connection?.mode === 'remote'
// In remote thin-client mode the client and backend are separate installs;
// show both versions and flag skew. Local mode is unchanged.
const versionBar = resolveVersionBar({
appVersion,
sha,
backendVersion: statusSnapshot?.version,
mode: connection?.mode,
copy: { clientLabel: copy.clientLabel, backendLabel: copy.backendLabel, unknown: copy.unknown }
})
const base = versionBar.label
const version = appVersion ? `v${appVersion}` : (sha ?? copy.unknown)
const base = remote ? copy.clientLabel(appVersion ?? sha ?? copy.unknown) : version
const behindHint = !applying && behind > 0 ? ` (+${behind})` : ''
const label = applying
? updateApply.stage === 'restart'
? `${base} · ${copy.restart}`
: `${base} · ${copy.update}`
? `${base} · ${updateApply.stage === 'restart' ? copy.restart : copy.update}`
: `${base}${behindHint}`
const tooltip = [
applying ? updateApply.message || copy.updateInProgress : null,
!applying && behind > 0 && copy.commitsBehind(behind, updateStatus?.branch ?? '...'),
appVersion && copy.desktopVersion(appVersion),
versionBar.showsBackend && versionBar.backendVersion && copy.backendVersion(versionBar.backendVersion),
sha && copy.commit(sha),
updateStatus?.branch && copy.branch(updateStatus.branch)
]
@@ -234,21 +231,19 @@ export function useStatusbarItems({
.join(' · ')
return {
className:
versionBar.skew || (!applying && behind > 0) ? 'text-primary hover:text-primary' : undefined,
detail: appVersion && sha && !applying && !versionBar.showsBackend ? sha : undefined,
className: !applying && behind > 0 ? 'text-primary hover:text-primary' : undefined,
detail: appVersion && sha && !applying && !remote ? sha : undefined,
hidden: !appVersion && !sha,
icon: applying ? <Loader2 className="size-3 animate-spin" /> : <Hash className="size-3" />,
id: 'version',
id: 'version-client',
label,
onSelect: () => setUpdateOverlayOpen(true),
onSelect: () => openUpdateOverlayFor('client'),
title: tooltip || undefined,
variant: 'action'
}
}, [
desktopVersion?.appVersion,
connection?.mode,
statusSnapshot?.version,
copy,
updateApply.applying,
updateApply.message,
@@ -258,6 +253,50 @@ export function useStatusbarItems({
updateStatus?.currentSha
])
const backendVersionItem = useMemo<StatusbarItem | null>(() => {
if (connection?.mode !== 'remote') {
return null
}
const backendVersion = statusSnapshot?.version
const behind = backendUpdateStatus?.behind ?? 0
const applying = backendUpdateApply.applying || backendUpdateApply.stage === 'restart'
const base = copy.backendLabel(backendVersion ?? copy.unknown)
const behindHint = !applying && behind > 0 ? ` (+${behind})` : ''
const label = applying
? `${base} · ${backendUpdateApply.stage === 'restart' ? copy.restart : copy.update}`
: `${base}${behindHint}`
const tooltip = [
applying ? backendUpdateApply.message || copy.updateInProgress : null,
!applying && behind > 0 && copy.commitsBehind(behind, 'main'),
backendVersion && copy.backendVersion(backendVersion)
]
.filter(Boolean)
.join(' · ')
return {
className: !applying && behind > 0 ? 'text-primary hover:text-primary' : undefined,
hidden: !backendVersion,
icon: applying ? <Loader2 className="size-3 animate-spin" /> : <Hash className="size-3" />,
id: 'version-backend',
label,
onSelect: () => openUpdateOverlayFor('backend'),
title: tooltip || undefined,
variant: 'action'
}
}, [
connection?.mode,
statusSnapshot?.version,
backendUpdateStatus?.behind,
backendUpdateApply.applying,
backendUpdateApply.message,
backendUpdateApply.stage,
copy
])
const coreLeftStatusbarItems = useMemo<readonly StatusbarItem[]>(
() => [
{
@@ -403,7 +442,8 @@ export function useStatusbarItems({
variant: 'action' as const
})
},
versionItem
...(backendVersionItem ? [backendVersionItem] : []),
clientVersionItem
],
[
busy,
@@ -419,7 +459,8 @@ export function useStatusbarItems({
showYoloToggle,
toggleYolo,
turnStartedAt,
versionItem,
clientVersionItem,
backendVersionItem,
yoloActive
]
)
+25 -14
View File
@@ -13,40 +13,51 @@ import { buildCommitChangelog, type CommitGroup } from '@/lib/commit-changelog'
import { AlertCircle, Check, CheckCircle2, Copy, Terminal } from '@/lib/icons'
import { cn } from '@/lib/utils'
import { resolveUpdateCopy, type UpdateTarget } from '@/lib/update-copy'
import { $connection } from '@/store/session'
import {
$backendUpdateApply,
$backendUpdateChecking,
$backendUpdateStatus,
$updateApply,
$updateChecking,
$updateOverlayOpen,
$updateOverlayTarget,
$updateStatus,
applyBackendUpdate,
applyUpdates,
checkBackendUpdates,
checkUpdates,
resetUpdateApplyState,
setUpdateOverlayOpen,
type UpdateApplyState
} from '@/store/updates'
/** What the overlay is acting on. In remote thin-client mode the overlay
* targets the connected BACKEND; otherwise the local desktop client.
* (Type re-exported from the copy helper.) */
function totalItems(groups: readonly CommitGroup[]) {
return groups.reduce((sum, g) => sum + g.items.length, 0)
}
export function UpdatesOverlay() {
const open = useStore($updateOverlayOpen)
const status = useStore($updateStatus)
const checking = useStore($updateChecking)
const apply = useStore($updateApply)
const connection = useStore($connection)
const target: UpdateTarget = connection?.mode === 'remote' ? 'backend' : 'client'
const target = useStore($updateOverlayTarget)
const clientStatus = useStore($updateStatus)
const clientChecking = useStore($updateChecking)
const clientApply = useStore($updateApply)
const backendStatus = useStore($backendUpdateStatus)
const backendChecking = useStore($backendUpdateChecking)
const backendApply = useStore($backendUpdateApply)
const isBackend = target === 'backend'
const status = isBackend ? backendStatus : clientStatus
const checking = isBackend ? backendChecking : clientChecking
const apply = isBackend ? backendApply : clientApply
const check = isBackend ? checkBackendUpdates : checkUpdates
const install = isBackend ? applyBackendUpdate : applyUpdates
useEffect(() => {
if (open && !status && !checking) {
void checkUpdates()
void check()
}
}, [checking, open, status])
}, [check, checking, open, status])
const behind = status?.behind ?? 0
@@ -72,7 +83,7 @@ export function UpdatesOverlay() {
}
const handleInstall = () => {
void applyUpdates()
void install()
}
return (
@@ -98,7 +109,7 @@ export function UpdatesOverlay() {
commits={status?.commits ?? []}
onInstall={handleInstall}
onLater={() => handleClose(false)}
onRetryCheck={() => void checkUpdates()}
onRetryCheck={() => void check()}
status={status}
target={target}
/>