diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index 5bb0b84f7..c6b3c487b 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -29,7 +29,6 @@ import { CSS } from "@dnd-kit/utilities"; import { DEFAULT_RUNTIME_MODE, DEFAULT_MODEL_BY_PROVIDER, - type DesktopUpdateState, ProjectId, ThreadId, type GitStatusResult, @@ -39,7 +38,8 @@ import { useMutation, useQueries, useQuery, useQueryClient } from "@tanstack/rea import { useLocation, useNavigate, useParams } from "@tanstack/react-router"; import { useAppSettings } from "../appSettings"; import { isElectron } from "../env"; -import { APP_STAGE_LABEL, APP_VERSION } from "../branding"; +import { APP_STAGE_LABEL } from "../branding"; +import { resolveDisplayedAppVersion, useDesktopUpdateState } from "../hooks/useDesktopUpdateState"; import { isMacPlatform, newCommandId, newProjectId, newThreadId } from "../lib/utils"; import { useStore } from "../store"; import { isChatNewLocalShortcut, isChatNewShortcut, shortcutLabelForCommand } from "../keybindings"; @@ -301,7 +301,7 @@ export default function Sidebar() { const renamingInputRef = useRef(null); const dragInProgressRef = useRef(false); const suppressProjectClickAfterDragRef = useRef(false); - const [desktopUpdateState, setDesktopUpdateState] = useState(null); + const desktopUpdateState = useDesktopUpdateState(); const selectedThreadIds = useThreadSelectionStore((s) => s.selectedThreadIds); const toggleThreadSelection = useThreadSelectionStore((s) => s.toggleThread); const rangeSelectTo = useThreadSelectionStore((s) => s.rangeSelectTo); @@ -1078,40 +1078,11 @@ export default function Sidebar() { threads, ]); - useEffect(() => { - if (!isElectron) return; - const bridge = window.desktopBridge; - if ( - !bridge || - typeof bridge.getUpdateState !== "function" || - typeof bridge.onUpdateState !== "function" - ) { - return; - } - - let disposed = false; - let receivedSubscriptionUpdate = false; - const unsubscribe = bridge.onUpdateState((nextState) => { - if (disposed) return; - receivedSubscriptionUpdate = true; - setDesktopUpdateState(nextState); - }); - - void bridge - .getUpdateState() - .then((nextState) => { - if (disposed || receivedSubscriptionUpdate) return; - setDesktopUpdateState(nextState); - }) - .catch(() => undefined); - - return () => { - disposed = true; - unsubscribe(); - }; - }, []); - const showDesktopUpdateButton = isElectron && shouldShowDesktopUpdateButton(desktopUpdateState); + const displayedAppVersion = resolveDisplayedAppVersion({ + desktopUpdateState, + isDesktopRuntime: isElectron, + }); const desktopUpdateTooltip = desktopUpdateState ? getDesktopUpdateButtonTooltip(desktopUpdateState) @@ -1239,7 +1210,7 @@ export default function Sidebar() { } /> - Version {APP_VERSION} + Version {displayedAppVersion} diff --git a/apps/web/src/hooks/useDesktopUpdateState.test.ts b/apps/web/src/hooks/useDesktopUpdateState.test.ts new file mode 100644 index 000000000..d41f1f4d3 --- /dev/null +++ b/apps/web/src/hooks/useDesktopUpdateState.test.ts @@ -0,0 +1,52 @@ +import { describe, expect, it } from "vitest"; +import type { DesktopUpdateState } from "@t3tools/contracts"; + +import { resolveDisplayedAppVersion } from "./useDesktopUpdateState"; + +const baseDesktopUpdateState: DesktopUpdateState = { + enabled: true, + status: "up-to-date", + currentVersion: "0.0.11", + hostArch: "arm64", + appArch: "arm64", + runningUnderArm64Translation: false, + availableVersion: null, + downloadedVersion: null, + downloadPercent: null, + checkedAt: null, + message: null, + errorContext: null, + canRetry: false, +}; + +describe("resolveDisplayedAppVersion", () => { + it("keeps the fallback version outside desktop runtime", () => { + expect( + resolveDisplayedAppVersion({ + desktopUpdateState: baseDesktopUpdateState, + fallbackVersion: "0.0.10", + isDesktopRuntime: false, + }), + ).toBe("0.0.10"); + }); + + it("prefers the desktop runtime version when available", () => { + expect( + resolveDisplayedAppVersion({ + desktopUpdateState: baseDesktopUpdateState, + fallbackVersion: "0.0.10", + isDesktopRuntime: true, + }), + ).toBe("0.0.11"); + }); + + it("falls back when the runtime state is missing", () => { + expect( + resolveDisplayedAppVersion({ + desktopUpdateState: null, + fallbackVersion: "0.0.10", + isDesktopRuntime: true, + }), + ).toBe("0.0.10"); + }); +}); diff --git a/apps/web/src/hooks/useDesktopUpdateState.ts b/apps/web/src/hooks/useDesktopUpdateState.ts new file mode 100644 index 000000000..40bf1418c --- /dev/null +++ b/apps/web/src/hooks/useDesktopUpdateState.ts @@ -0,0 +1,63 @@ +import { useEffect, useState } from "react"; +import type { DesktopUpdateState } from "@t3tools/contracts"; + +import { APP_VERSION } from "../branding"; +import { isElectron } from "../env"; + +interface ResolveDisplayedAppVersionInput { + readonly desktopUpdateState: DesktopUpdateState | null; + readonly fallbackVersion?: string; + readonly isDesktopRuntime: boolean; +} + +export function resolveDisplayedAppVersion({ + desktopUpdateState, + fallbackVersion = APP_VERSION, + isDesktopRuntime, +}: ResolveDisplayedAppVersionInput): string { + if (!isDesktopRuntime) { + return fallbackVersion; + } + + const runtimeVersion = desktopUpdateState?.currentVersion.trim(); + return runtimeVersion && runtimeVersion.length > 0 ? runtimeVersion : fallbackVersion; +} + +export function useDesktopUpdateState(): DesktopUpdateState | null { + const [desktopUpdateState, setDesktopUpdateState] = useState(null); + + useEffect(() => { + if (!isElectron) return; + const bridge = window.desktopBridge; + if ( + !bridge || + typeof bridge.getUpdateState !== "function" || + typeof bridge.onUpdateState !== "function" + ) { + return; + } + + let disposed = false; + let receivedSubscriptionUpdate = false; + const unsubscribe = bridge.onUpdateState((nextState) => { + if (disposed) return; + receivedSubscriptionUpdate = true; + setDesktopUpdateState(nextState); + }); + + void bridge + .getUpdateState() + .then((nextState) => { + if (disposed || receivedSubscriptionUpdate) return; + setDesktopUpdateState(nextState); + }) + .catch(() => undefined); + + return () => { + disposed = true; + unsubscribe(); + }; + }, []); + + return desktopUpdateState; +} diff --git a/apps/web/src/routes/_chat.settings.tsx b/apps/web/src/routes/_chat.settings.tsx index 93e074442..4a0946490 100644 --- a/apps/web/src/routes/_chat.settings.tsx +++ b/apps/web/src/routes/_chat.settings.tsx @@ -6,6 +6,7 @@ import { getModelOptions, normalizeModelSlug } from "@t3tools/shared/model"; import { MAX_CUSTOM_MODEL_LENGTH, useAppSettings } from "../appSettings"; import { isElectron } from "../env"; +import { resolveDisplayedAppVersion, useDesktopUpdateState } from "../hooks/useDesktopUpdateState"; import { useTheme } from "../hooks/useTheme"; import { serverConfigQueryOptions } from "../lib/serverReactQuery"; import { ensureNativeApi } from "../nativeApi"; @@ -13,7 +14,6 @@ import { preferredTerminalEditor } from "../terminal-links"; import { Button } from "../components/ui/button"; import { Input } from "../components/ui/input"; import { Switch } from "../components/ui/switch"; -import { APP_VERSION } from "../branding"; import { SidebarInset } from "~/components/ui/sidebar"; const THEME_OPTIONS = [ @@ -84,6 +84,7 @@ function SettingsRouteView() { const { theme, setTheme, resolvedTheme } = useTheme(); const { settings, defaults, updateSettings } = useAppSettings(); const serverConfigQuery = useQuery(serverConfigQueryOptions()); + const desktopUpdateState = useDesktopUpdateState(); const [isOpeningKeybindings, setIsOpeningKeybindings] = useState(false); const [openKeybindingsError, setOpenKeybindingsError] = useState(null); const [customModelInputByProvider, setCustomModelInputByProvider] = useState< @@ -98,6 +99,10 @@ function SettingsRouteView() { const codexBinaryPath = settings.codexBinaryPath; const codexHomePath = settings.codexHomePath; const keybindingsConfigPath = serverConfigQuery.data?.keybindingsConfigPath ?? null; + const displayedAppVersion = resolveDisplayedAppVersion({ + desktopUpdateState, + isDesktopRuntime: isElectron, + }); const openKeybindingsFile = useCallback(() => { if (!keybindingsConfigPath) return; @@ -573,7 +578,9 @@ function SettingsRouteView() { Current version of the application.

- {APP_VERSION} + + {displayedAppVersion} +