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}
+