From cb9e9b72725a76ba850821f65184e785b0f6d10b Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Wed, 29 Jul 2026 00:24:38 -0500 Subject: [PATCH 1/3] refactor(desktop): resolve version/update labels in one place The statusbar derived its client and backend version labels inline, in two near-identical blocks. Move the wording into a pure resolver so every surface that names an install agrees on the label, the commit diff, and the tooltip. --- .../app/shell/hooks/use-statusbar-items.tsx | 83 ++++++-------- apps/desktop/src/lib/version-status.test.ts | 85 ++++++++++++++ apps/desktop/src/lib/version-status.ts | 106 ++++++++++++++++++ 3 files changed, 224 insertions(+), 50 deletions(-) create mode 100644 apps/desktop/src/lib/version-status.test.ts create mode 100644 apps/desktop/src/lib/version-status.ts 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 fb2aac6d2da..0aa3c69fe3b 100644 --- a/apps/desktop/src/app/shell/hooks/use-statusbar-items.tsx +++ b/apps/desktop/src/app/shell/hooks/use-statusbar-items.tsx @@ -14,6 +14,7 @@ import type { RuntimeReadinessResult } from '@/lib/runtime-readiness' import { contextBarLabel, LiveDuration, usageContextLabel } from '@/lib/statusbar' import { useStoreSelector } from '@/lib/use-session-slice' import { cn } from '@/lib/utils' +import { resolveVersionStatus } from '@/lib/version-status' import { copyFilePath, revealFile } from '@/store/file-actions' import { revealFileInTree } from '@/store/layout' import { $activeGatewayProfile } from '@/store/profile' @@ -218,42 +219,33 @@ export function useStatusbarItems({ : 'text-destructive hover:text-destructive' const clientVersionItem = useMemo(() => { - 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' - 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 - ? `${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), - sha && copy.commit(sha), - updateStatus?.branch && copy.branch(updateStatus.branch) - ] - .filter(Boolean) - .join(' · ') + const status = resolveVersionStatus({ + applying, + applyMessage: updateApply.message, + behind: updateStatus?.behind ?? 0, + branch: updateStatus?.branch, + copy, + remote: connection?.mode === 'remote', + restarting: updateApply.stage === 'restart', + sha: updateStatus?.currentSha?.slice(0, 7) ?? null, + target: 'client', + version: desktopVersion?.appVersion + }) return { - className: !applying && behind > 0 ? 'text-primary hover:text-primary' : undefined, - detail: appVersion && sha && !applying && !remote ? sha : undefined, - hidden: !appVersion && !sha, + className: status.hasUpdate ? 'text-primary hover:text-primary' : undefined, + detail: status.detail, + hidden: status.unknown, icon: applying ? : , id: 'version-client', - label, + label: status.label, // Update state is not a preference: hiding it is how a user misses that // their client is behind. Listed in the menu, but locked on. lockedVisible: true, onSelect: () => openUpdateOverlayFor('client'), - title: tooltip || undefined, + title: status.tooltip, toggleLabel: copy.toggleVersion, variant: 'action' } @@ -274,38 +266,29 @@ export function useStatusbarItems({ return null } - 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})` : !applying && updateAvailable ? ` (${copy.update})` : '' - - 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'), - !applying && behind <= 0 && updateAvailable && copy.update, - backendVersion && copy.backendVersion(backendVersion) - ] - .filter(Boolean) - .join(' · ') + const status = resolveVersionStatus({ + applying, + applyMessage: backendUpdateApply.message, + behind: backendUpdateStatus?.behind ?? 0, + copy, + remote: true, + restarting: backendUpdateApply.stage === 'restart', + target: 'backend', + updateAvailable: backendUpdateStatus?.updateAvailable, + version: statusSnapshot?.version + }) return { - className: !applying && updateAvailable ? 'text-primary hover:text-primary' : undefined, - hidden: !backendVersion, + className: status.hasUpdate ? 'text-primary hover:text-primary' : undefined, + hidden: status.unknown, icon: applying ? : , id: 'version-backend', - label, + label: status.label, lockedVisible: true, onSelect: () => openUpdateOverlayFor('backend'), - title: tooltip || undefined, + title: status.tooltip, toggleLabel: copy.toggleBackendVersion, variant: 'action' } diff --git a/apps/desktop/src/lib/version-status.test.ts b/apps/desktop/src/lib/version-status.test.ts new file mode 100644 index 00000000000..54a4bb2d576 --- /dev/null +++ b/apps/desktop/src/lib/version-status.test.ts @@ -0,0 +1,85 @@ +import { describe, expect, it } from 'vitest' + +import { en } from '@/i18n/en' + +import { resolveVersionStatus } from './version-status' + +const copy = en.shell.statusbar + +const client = (over: Partial[0]> = {}) => + resolveVersionStatus({ applying: false, copy, remote: false, restarting: false, target: 'client', ...over }) + +const backend = (over: Partial[0]> = {}) => + resolveVersionStatus({ applying: false, copy, remote: true, restarting: false, target: 'backend', ...over }) + +describe('resolveVersionStatus', () => { + it('labels a current local client with its version and sha detail', () => { + const status = client({ sha: 'abc1234', version: '0.4.2' }) + + expect(status.label).toBe('v0.4.2') + expect(status.detail).toBe('abc1234') + expect(status.hasUpdate).toBe(false) + expect(status.unknown).toBe(false) + }) + + it('appends the commit diff when the client is behind', () => { + const status = client({ behind: 12, branch: 'main', version: '0.4.2' }) + + expect(status.label).toBe('v0.4.2 (+12)') + expect(status.hasUpdate).toBe(true) + expect(status.tooltip).toContain('12 commits behind main') + }) + + it('names the client as one of two versions in remote mode', () => { + expect(client({ remote: true, version: '0.4.2' }).label).toBe('client v0.4.2') + }) + + it('falls back to the sha, then to unknown, when there is no version', () => { + expect(client({ sha: 'abc1234' }).label).toBe('abc1234') + expect(client({ sha: 'abc1234' }).unknown).toBe(false) + expect(client().label).toBe(copy.unknown) + expect(client().unknown).toBe(true) + }) + + it('drops the diff and the sha detail while an apply is in flight', () => { + const applying = client({ applying: true, behind: 3, sha: 'abc1234', version: '0.4.2' }) + + expect(applying.label).toBe('v0.4.2 · update') + expect(applying.detail).toBeUndefined() + expect(applying.hasUpdate).toBe(false) + + expect(client({ applying: true, restarting: true, version: '0.4.2' }).label).toBe('v0.4.2 · restart') + }) + + it('leads the tooltip with the apply message while applying', () => { + expect(client({ applyMessage: 'Pulling…', applying: true, version: '0.4.2' }).tooltip).toBe( + 'Pulling… · Hermes Desktop v0.4.2' + ) + expect(client({ applying: true, version: '0.4.2' }).tooltip).toBe( + `${copy.updateInProgress} · Hermes Desktop v0.4.2` + ) + }) + + it('labels the backend target distinctly and never claims a client sha', () => { + const status = backend({ sha: 'abc1234', version: '0.4.2' }) + + expect(status.label).toBe('backend v0.4.2') + expect(status.detail).toBeUndefined() + expect(status.tooltip).toBe('Backend v0.4.2') + }) + + it('falls back to (update) for a backend that cannot count commits', () => { + const status = backend({ updateAvailable: true, version: '0.4.2' }) + + expect(status.label).toBe('backend v0.4.2 (update)') + expect(status.hasUpdate).toBe(true) + }) + + it('prefers the exact commit diff over the generic (update) hint', () => { + expect(backend({ behind: 4, updateAvailable: true, version: '0.4.2' }).label).toBe('backend v0.4.2 (+4)') + }) + + it('hides a backend row that has no version at all', () => { + expect(backend().unknown).toBe(true) + }) +}) diff --git a/apps/desktop/src/lib/version-status.ts b/apps/desktop/src/lib/version-status.ts new file mode 100644 index 00000000000..030464e8eca --- /dev/null +++ b/apps/desktop/src/lib/version-status.ts @@ -0,0 +1,106 @@ +/** + * Pure derivation of how the app names an update target: the label + * (`v0.4.2`, `backend v0.4.2 (+12)`, `v0.4.2 · update`), its tooltip, and + * whether an update is waiting. + * + * The statusbar and the command palette both name the same two targets, so the + * wording lives here once — a palette row and its statusbar item can't drift + * into describing the same install differently. + */ + +import type { UpdateTarget } from '@/lib/update-copy' + +export interface VersionStatusCopy { + backendLabel: (version: string) => string + backendVersion: (version: string) => string + branch: (branch: string) => string + clientLabel: (version: string) => string + commit: (sha: string) => string + commitsBehind: (count: number, branch: string) => string + desktopVersion: (version: string) => string + restart: string + unknown: string + update: string + updateInProgress: string +} + +export interface VersionStatusInput { + /** True while an apply is in flight (including the restart hand-off). */ + applying: boolean + /** Latest line from the apply stream — leads the tooltip while applying. */ + applyMessage?: string + behind?: number + branch?: string + copy: VersionStatusCopy + /** Remote mode: the client is one of two versions on screen, so it says so. */ + remote: boolean + /** The apply reached the restart stage — labels `restart`, not `update`. */ + restarting: boolean + /** Client only: short commit sha of the running build. */ + sha?: null | string + target: UpdateTarget + /** Backend only: an update the commit count can't express (pip installs). */ + updateAvailable?: boolean + version?: null | string +} + +export interface VersionStatusResult { + /** Secondary text beside the label — the commit sha, when it adds anything. */ + detail?: string + /** An update is waiting: callers tint the row with it. */ + hasUpdate: boolean + label: string + tooltip?: string + /** Nothing identifies this target yet — callers hide the row. */ + unknown: boolean +} + +export function resolveVersionStatus({ + applyMessage, + applying, + behind = 0, + branch, + copy, + remote, + restarting, + sha = null, + target, + updateAvailable, + version = null +}: VersionStatusInput): VersionStatusResult { + const client = target === 'client' + const busy = applying || restarting + const available = behind > 0 || (!client && !!updateAvailable) + + // A client with no version still identifies itself by sha; a backend can't. + const named = version ?? (client ? sha : null) ?? copy.unknown + + const base = !client + ? copy.backendLabel(named) + : remote + ? copy.clientLabel(named) + : (version && `v${version}`) || named + + // Commits behind is the precise diff; `(update)` is the fallback for a + // backend that knows it's stale but can't count (pip, non-git checkout). + const hint = busy ? '' : behind > 0 ? ` (+${behind})` : available ? ` (${copy.update})` : '' + + const tooltip = [ + busy && (applyMessage || copy.updateInProgress), + !busy && behind > 0 && copy.commitsBehind(behind, (client ? branch : 'main') || '...'), + !busy && behind <= 0 && available && copy.update, + version && (client ? copy.desktopVersion(version) : copy.backendVersion(version)), + client && sha && copy.commit(sha), + client && branch && copy.branch(branch) + ] + .filter(Boolean) + .join(' · ') + + return { + detail: client && version && sha && !busy && !remote ? sha : undefined, + hasUpdate: !busy && available, + label: busy ? `${base} · ${restarting ? copy.restart : copy.update}` : `${base}${hint}`, + tooltip: tooltip || undefined, + unknown: !version && !(client && sha) + } +} From a121b4972c3434cfb163ec7166fa27c0d30cbfee Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Wed, 29 Jul 2026 00:24:45 -0500 Subject: [PATCH 2/3] fix(desktop): aim the Update Hermes command at the right target The command palette's row called applyBackendUpdate() directly, so in local mode it drove the backend checkout rather than the client, and nothing opened the updates overlay to report either outcome. Route it through the same target selection the statusbar and About panel use, always surface the overlay, and re-check instead of applying when the active target is already current. --- apps/desktop/src/global.d.ts | 2 + apps/desktop/src/store/updates.test.ts | 116 ++++++++++++++++++++++--- apps/desktop/src/store/updates.ts | 20 +++++ 3 files changed, 126 insertions(+), 12 deletions(-) diff --git a/apps/desktop/src/global.d.ts b/apps/desktop/src/global.d.ts index 336f95b54ae..a53965a63b5 100644 --- a/apps/desktop/src/global.d.ts +++ b/apps/desktop/src/global.d.ts @@ -373,6 +373,8 @@ export interface DesktopUpdateStatus { error?: string behind?: number currentSha?: string + /** Backend only: the version string the backend reports for itself. */ + currentVersion?: string targetSha?: string commits?: DesktopUpdateCommit[] dirty?: boolean diff --git a/apps/desktop/src/store/updates.test.ts b/apps/desktop/src/store/updates.test.ts index 7b19ba15f45..9dd1cbed6a3 100644 --- a/apps/desktop/src/store/updates.test.ts +++ b/apps/desktop/src/store/updates.test.ts @@ -51,6 +51,8 @@ const { applyUpdates, $updateApply, $updateOverlayOpen, + $updateOverlayTarget, + requestActiveUpdate, resetUpdateApplyState, startUpdatePoller, stopUpdatePoller, @@ -69,6 +71,18 @@ const status = (over: Partial = {}): DesktopUpdateStatus => const lastToast = () => notifySpy.mock.calls.at(-1)?.[0] as { onDismiss: () => void } +const setRemote = (on: boolean) => + setConnection({ + baseUrl: 'http://box:9119', + isFullscreen: false, + mode: on ? 'remote' : 'local', + nativeOverlayWidth: 0, + token: 't', + wsUrl: 'ws://box:9119', + logs: [], + windowButtonPosition: null + }) + describe('maybeNotifyUpdateAvailable', () => { beforeEach(() => { storage.clear() @@ -175,18 +189,6 @@ describe('checkBackendUpdates', () => { vi.useRealTimers() }) - const setRemote = (on: boolean) => - setConnection({ - baseUrl: 'http://box:9119', - isFullscreen: false, - mode: on ? 'remote' : 'local', - nativeOverlayWidth: 0, - token: 't', - wsUrl: 'ws://box:9119', - logs: [], - windowButtonPosition: null - }) - it('maps the backend /update/check onto the backend status, including commits', async () => { setRemote(true) checkHermesUpdateSpy.mockResolvedValue({ @@ -254,6 +256,96 @@ describe('checkBackendUpdates', () => { }) }) +// The ⌘K "Update Hermes" row. It used to call applyBackendUpdate() flat, which +// in local mode aimed at the backend checkout instead of the client and, with +// no overlay open, showed nothing at all. +describe('requestActiveUpdate', () => { + const applyClientMock = vi.fn() + const checkClientMock = vi.fn() + + beforeEach(() => { + storage.clear() + notifySpy.mockClear() + dismissSpy.mockClear() + applyClientMock.mockReset().mockResolvedValue({ ok: true, handedOff: true }) + checkClientMock.mockReset().mockResolvedValue(status({ behind: 0 })) + updateHermesSpy.mockReset().mockResolvedValue({ ok: true, name: 'update' }) + checkHermesUpdateSpy.mockReset().mockResolvedValue({ + install_method: 'git', + current_version: '0.4.2', + behind: 0, + update_available: false, + can_apply: true, + update_command: null, + message: null + }) + getActionStatusSpy.mockReset().mockResolvedValue({ lines: [], running: false, exit_code: 0 }) + resetUpdateApplyState() + $updateStatus.set(null) + $backendUpdateStatus.set(null) + $updateOverlayOpen.set(false) + ;(globalThis as unknown as { window: unknown }).window = { + hermesDesktop: { updates: { apply: applyClientMock, check: checkClientMock } } + } + vi.useRealTimers() + }) + + afterEach(() => { + setRemote(false) + delete (globalThis as unknown as { window?: unknown }).window + }) + + it('applies the CLIENT update in local mode, never the backend', async () => { + setRemote(false) + $updateStatus.set(status({ behind: 3 })) + + requestActiveUpdate() + await vi.waitFor(() => expect(applyClientMock).toHaveBeenCalled()) + + expect(updateHermesSpy).not.toHaveBeenCalled() + expect($updateOverlayTarget.get()).toBe('client') + }) + + it('applies the BACKEND update in remote mode', async () => { + setRemote(true) + $backendUpdateStatus.set(status({ behind: 3 })) + + requestActiveUpdate() + await vi.waitFor(() => expect(updateHermesSpy).toHaveBeenCalled()) + + expect(applyClientMock).not.toHaveBeenCalled() + expect($updateOverlayTarget.get()).toBe('backend') + }) + + it('always opens the overlay, so selecting the row is never a silent no-op', () => { + setRemote(false) + $updateStatus.set(status({ behind: 3 })) + + requestActiveUpdate() + + expect($updateOverlayOpen.get()).toBe(true) + }) + + it('opens the overlay to re-check instead of applying when already current', () => { + setRemote(false) + $updateStatus.set(status({ behind: 0, updateAvailable: false })) + + requestActiveUpdate() + + expect($updateOverlayOpen.get()).toBe(true) + expect(applyClientMock).not.toHaveBeenCalled() + expect(updateHermesSpy).not.toHaveBeenCalled() + }) + + it('applies on a backend that reports an update it cannot count commits for', async () => { + setRemote(true) + $backendUpdateStatus.set(status({ behind: 0, updateAvailable: true })) + + requestActiveUpdate() + await vi.waitFor(() => expect(updateHermesSpy).toHaveBeenCalled()) + }) +}) + describe('applyUpdates terminal state', () => { const applyMock = vi.fn() diff --git a/apps/desktop/src/store/updates.ts b/apps/desktop/src/store/updates.ts index 1f0998a5e06..0b46920e30d 100644 --- a/apps/desktop/src/store/updates.ts +++ b/apps/desktop/src/store/updates.ts @@ -254,6 +254,25 @@ export function startActiveUpdate(): void { void (target === 'backend' ? applyBackendUpdate() : applyUpdates()) } +/** + * Command-palette entry point. The About panel's "Update now" only renders once + * we know an update is waiting; this row is always listed, so it also has to + * handle "already current" — open the overlay for the active target and let its + * check answer, and only apply when there's something to install. + */ +export function requestActiveUpdate(): void { + const target: UpdateTarget = isRemoteMode() ? 'backend' : 'client' + const status = target === 'backend' ? $backendUpdateStatus.get() : $updateStatus.get() + + if ((status?.behind ?? 0) > 0 || status?.updateAvailable) { + startActiveUpdate() + + return + } + + openUpdateOverlayFor(target) +} + /** Re-read the running app's version from the Electron main process and * publish it on `$desktopVersion`. Called when the About panel mounts, the * update flow finishes, and the window regains focus, so the About text @@ -294,6 +313,7 @@ function mapBackendCheck(res: BackendUpdateCheckResponse): DesktopUpdateStatus { message: res.message ?? undefined, updateAvailable: res.update_available, behind: behind > 0 ? behind : 0, + currentVersion: res.current_version, targetSha: res.update_available ? `backend:${res.current_version}` : undefined, commits: res.commits, fetchedAt: Date.now() From 20a0bfc9a60a98a63c8b7ba1dc565af1b596bbe9 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Wed, 29 Jul 2026 00:24:50 -0500 Subject: [PATCH 3/3] feat(desktop): show the running version on the Update Hermes row The command palette named the action but not the install it acts on. Carry the version and its commit diff on the row, resolved from the shared resolver so it reads identically to the statusbar. --- .../desktop/src/app/command-palette/index.tsx | 48 +++++++++++++++++-- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/apps/desktop/src/app/command-palette/index.tsx b/apps/desktop/src/app/command-palette/index.tsx index 0014e4e71f1..59f8ba0cbae 100644 --- a/apps/desktop/src/app/command-palette/index.tsx +++ b/apps/desktop/src/app/command-palette/index.tsx @@ -49,6 +49,7 @@ import { } from '@/lib/icons' import { normalize } from '@/lib/text' import { cn } from '@/lib/utils' +import { resolveVersionStatus } from '@/lib/version-status' import { $repoWorktrees } from '@/store/coding-status' import { $commandPaletteOpen, @@ -59,8 +60,16 @@ import { import { $bindings } from '@/store/keybinds' import { openPetGenerate } from '@/store/pet-generate' import { requestStartWorkSession } from '@/store/projects' +import { $connection } from '@/store/session' import { runGatewayRestart } from '@/store/system-actions' -import { applyBackendUpdate } from '@/store/updates' +import { + $backendUpdateApply, + $backendUpdateStatus, + $desktopVersion, + $updateApply, + $updateStatus, + requestActiveUpdate +} from '@/store/updates' import { canOpenNewWindow, openNewWindow } from '@/store/windows' import { luminance } from '@/themes/color' import { type ThemeMode, useTheme } from '@/themes/context' @@ -93,6 +102,8 @@ interface PaletteItem { action?: string /** Renders a trailing check: this row IS the current setting (theme, mode). */ active?: boolean + /** Muted text beside the label — state the row acts on (a version, a count). */ + detail?: string icon: IconComponent id: string /** Keep the palette open after running (live-preview pickers like theme/mode). */ @@ -242,6 +253,7 @@ const PaletteRow = memo(function PaletteRow({ > {item.label} + {item.detail && {item.detail}} {combo && } {item.to && } {item.active && } @@ -345,6 +357,35 @@ export function CommandPalette() { const [search, setSearch] = useState('') const [page, setPage] = useState(null) + // The Update row names the same install the statusbar names — same target + // selection, same resolver. Reduced to the label string: an in-flight apply + // rewrites these stores on every progress line, and only a changed string + // should rebuild the palette's groups. + const connection = useStore($connection) + const desktopVersion = useStore($desktopVersion) + const clientStatus = useStore($updateStatus) + const clientApply = useStore($updateApply) + const backendStatus = useStore($backendUpdateStatus) + const backendApply = useStore($backendUpdateApply) + + const updateVersionLabel = useMemo(() => { + const backend = connection?.mode === 'remote' + const apply = backend ? backendApply : clientApply + const status = backend ? backendStatus : clientStatus + + return resolveVersionStatus({ + applying: apply.applying || apply.stage === 'restart', + behind: status?.behind ?? 0, + copy: t.shell.statusbar, + remote: backend, + restarting: apply.stage === 'restart', + sha: status?.currentSha?.slice(0, 7) ?? null, + target: backend ? 'backend' : 'client', + updateAvailable: status?.updateAvailable, + version: backend ? status?.currentVersion : desktopVersion?.appVersion + }).label + }, [backendApply, backendStatus, clientApply, clientStatus, connection?.mode, desktopVersion?.appVersion, t]) + // cmdk's onSelect doesn't forward the triggering event — keep the last // click/keydown modifiers so session rows can honour ⌘-Enter / ⌘-click. const lastSelectMods = useRef<{ ctrlKey: boolean; metaKey: boolean; shiftKey: boolean }>({ @@ -582,11 +623,12 @@ export function CommandPalette() { run: () => void runGatewayRestart() }, { + detail: updateVersionLabel, icon: Download, id: 'cc-update-hermes', keywords: ['update', 'upgrade', 'hermes', 'version', 'system', 'restart'], label: cc.updateHermes, - run: () => void applyBackendUpdate() + run: () => requestActiveUpdate() } ] }, @@ -663,7 +705,7 @@ export function CommandPalette() { ] : []) ] - }, [contributedItems, go, settingsSectionLabel, t, worktrees]) + }, [contributedItems, go, settingsSectionLabel, t, updateVersionLabel, worktrees]) // The long, granular lists (settings fields, API keys, MCP servers, archived // chats) only surface once the user types — otherwise they'd bury the