Files
hermes-agent/apps/desktop/src/app/chat/sidebar/version-menu.tsx
T
m4 fb13457f6f
CI / Supply-chain scan (push) Has been cancelled
Docker Build, Test, and Publish / Detect affected areas (push) Has been cancelled
auto-fix lint issues & formatting / Generate eslint --fix patch (push) Has been cancelled
Nix flake check / Detect affected areas (push) Has been cancelled
Deploy Site / deploy-vercel (push) Has been cancelled
Deploy Site / deploy-docs (push) Has been cancelled
CI / Detect affected areas (push) Has been cancelled
Docker Build, Test, and Publish / build (amd64, type=gha,scope=docker-amd64, type=gha,mode=max,scope=docker-amd64, linux/amd64, ubuntu-latest-32-core) (push) Has been cancelled
CI / Desktop E2E (push) Has been cancelled
CI / Docs Site (push) Has been cancelled
CI / Deny unrelated histories (push) Has been cancelled
CI / Check contributors (push) Has been cancelled
Docker Build, Test, and Publish / build (arm64, type=gha,scope=docker-arm64, type=gha,mode=max,scope=docker-arm64, linux/arm64, ubuntu-latest-32-arm-core) (push) Has been cancelled
CI / Check uv.lock (push) Has been cancelled
CI / Check no committed infographics (push) Has been cancelled
CI / Profile artifact check (push) Has been cancelled
Docker Build, Test, and Publish / publish (amd64, type=gha,scope=docker-amd64, type=gha,mode=max,scope=docker-amd64, linux/amd64, ubuntu-latest-32-core) (push) Has been cancelled
Docker Build, Test, and Publish / publish (arm64, type=gha,scope=docker-arm64, type=gha,mode=max,scope=docker-arm64, linux/arm64, ubuntu-latest-32-arm-core) (push) Has been cancelled
Docker Build, Test, and Publish / merge (push) Has been cancelled
auto-fix lint issues & formatting / Apply patch (push) Has been cancelled
CI / Check no case-colliding filenames (push) Has been cancelled
CI / package-lock.json diff (push) Has been cancelled
CI / Lint Docker scripts (push) Has been cancelled
CI / Review label gate (push) Has been cancelled
CI / All required checks pass (push) Has been cancelled
CI / CI timing report (push) Has been cancelled
CI / Python tests (push) Has been cancelled
CI / OS-specific tests (push) Has been cancelled
CI / Python lints (push) Has been cancelled
CI / JS & TS checks (push) Has been cancelled
CI / Installer tests (push) Has been cancelled
CI / Rust tests (push) Has been cancelled
Nix flake check / nix flake check (push) Has been cancelled
feat: consolidate desktop and provider updates
2026-09-27 17:37:19 +08:00

157 lines
5.4 KiB
TypeScript

import { useStore } from '@nanostores/react'
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import { Tip } from '@/components/ui/tooltip'
import { useI18n } from '@/i18n'
import { ChevronDown, Download, Loader2, RefreshCw } from '@/lib/icons'
import { useStoreSelector } from '@/lib/use-session-slice'
import { cn } from '@/lib/utils'
import { resolveVersionStatus } from '@/lib/version-status'
import { $connection } from '@/store/session'
import {
$backendUpdateApply,
$backendUpdateChecking,
$backendUpdateStatus,
$desktopVersion,
$updateApply,
$updateChecking,
$updateStatus,
openUpdateOverlayFor
} from '@/store/updates'
export interface SidebarVersionMenuProps {
compact?: boolean
}
/** One entry for the existing desktop and remote-backend update flows. */
export function SidebarVersionMenu({ compact = false }: SidebarVersionMenuProps) {
const { t } = useI18n()
const copy = t.shell.statusbar
const version = useStore($desktopVersion)?.appVersion
const remote = useStoreSelector($connection, connection => connection?.mode === 'remote')
const clientCheck = useStore($updateChecking)
const clientApply = useStore($updateApply)
const clientUpdate = useStore($updateStatus)
const backendCheck = useStore($backendUpdateChecking)
const backendApply = useStore($backendUpdateApply)
const backendUpdate = useStore($backendUpdateStatus)
const targets = [
{ target: 'client' as const, version, checking: clientCheck, apply: clientApply, update: clientUpdate },
...(remote
? [
{
target: 'backend' as const,
version: backendUpdate?.currentVersion,
checking: backendCheck,
apply: backendApply,
update: backendUpdate
}
]
: [])
].map(entry => {
const applying = entry.apply.applying || entry.apply.stage === 'restart'
return {
...entry,
applying,
status: resolveVersionStatus({
applying,
applyMessage: entry.apply.message,
behind: entry.update?.behind ?? 0,
branch: entry.update?.branch,
copy,
remote,
restarting: entry.apply.stage === 'restart',
sha: entry.update?.currentSha?.slice(0, 7),
target: entry.target,
updateAvailable: entry.update?.updateAvailable,
version: entry.version
})
}
})
const busy = targets.some(entry => entry.applying || entry.checking)
const available = targets.some(entry => entry.status.hasUpdate)
return (
<DropdownMenu>
<Tip label={t.settings.about.updates} side={compact ? 'right' : 'bottom'}>
<DropdownMenuTrigger asChild>
<Button
aria-label={t.settings.about.updates}
className={cn('min-w-0 max-w-full [-webkit-app-region:no-drag]', available && 'text-primary')}
data-slot="sidebar-version-menu"
size={compact ? 'icon-sm' : 'inline'}
variant={compact ? 'ghost' : 'text'}
>
{compact ? (
busy ? (
<Loader2 aria-hidden className="animate-spin" />
) : (
<Download aria-hidden />
)
) : (
<>
<span className="truncate text-[0.6875rem]">
{version ? `v${version}` : t.settings.about.versionUnavailable}
</span>
{busy ? (
<Loader2 aria-hidden className="animate-spin" />
) : available ? (
<Download aria-hidden />
) : (
<ChevronDown aria-hidden />
)}
</>
)}
</Button>
</DropdownMenuTrigger>
</Tip>
<DropdownMenuContent align="start" className="w-64 min-w-0 max-w-[calc(100vw-1rem)]" side="bottom">
{targets.map((entry, index) => (
<div key={entry.target}>
{index > 0 && <DropdownMenuSeparator />}
<DropdownMenuLabel className="truncate">
{entry.target === 'client'
? copy.desktopVersion(entry.version ?? copy.unknown)
: copy.backendVersion(entry.version ?? copy.unknown)}
</DropdownMenuLabel>
{entry.target === 'client' && entry.status.detail && (
<DropdownMenuLabel className="truncate font-normal text-(--ui-text-tertiary)">
{copy.commit(entry.status.detail)}
</DropdownMenuLabel>
)}
<DropdownMenuItem onSelect={() => openUpdateOverlayFor(entry.target)}>
{entry.applying || entry.checking ? (
<Loader2 aria-hidden className="animate-spin" />
) : entry.status.hasUpdate ? (
<Download aria-hidden />
) : (
<RefreshCw aria-hidden />
)}
<span className="truncate">
{entry.applying
? copy.updateInProgress
: entry.checking
? t.updates.checking
: entry.status.hasUpdate
? t.updates.updateNow
: t.settings.about.checkNow}
</span>
</DropdownMenuItem>
</div>
))}
</DropdownMenuContent>
</DropdownMenu>
)
}