diff --git a/src/common/static/global.ts b/src/common/static/global.ts
index cf784a2..8afcdcf 100644
--- a/src/common/static/global.ts
+++ b/src/common/static/global.ts
@@ -2,6 +2,22 @@ import { authentication } from "@modular-rest/client";
export const VERSION = require("../../../package.json").version;
+/**
+ * Installed-extension version for display. Prefers manifest `version_name`
+ * (carries the full semver incl. prerelease channel, e.g. "1.15.1-dev.1") and
+ * falls back to the numeric manifest `version` on stable builds. Outside an
+ * extension context (unit tests / non-chrome), falls back to the package.json
+ * VERSION constant — the Vitest chrome shim doesn't implement getManifest.
+ */
+export function getExtensionVersion(): string {
+ try {
+ const manifest = chrome.runtime.getManifest();
+ return manifest.version_name || manifest.version;
+ } catch {
+ return VERSION;
+ }
+}
+
export const SUBTURTLE_DASHBOARD_URL = process.env.SUBTURTLE_DASHBOARD_URL;
export function getSubturtleDashboardUrlWithToken(redirectPath?: string) {
diff --git a/src/popup/views/HomeView.vue b/src/popup/views/HomeView.vue
index 0bc7d53..721ac3e 100644
--- a/src/popup/views/HomeView.vue
+++ b/src/popup/views/HomeView.vue
@@ -283,6 +283,13 @@
+
+
+
+ v{{ appVersion }}
+
@@ -293,7 +300,10 @@ import { computed, onMounted, ref, watch } from "vue";
import { getAsset } from "../helper/assets";
import { isLogin, logout } from "../../plugins/modular-rest";
import { useRouter } from "vue-router";
-import { getSubturtleDashboardUrlWithToken } from "../../common/static/global";
+import {
+ getSubturtleDashboardUrlWithToken,
+ getExtensionVersion,
+} from "../../common/static/global";
import { useSettingsStore } from "../../common/store/settings";
import TranslateCard from "../components/TranslateCard.vue";
@@ -304,6 +314,7 @@ defineOptions({ name: "HomeView" });
const router = useRouter();
const isLoading = ref(false);
const showLogoutConfirm = ref(false);
+const appVersion = getExtensionVersion();
const settings = useSettingsStore();
const currentHost = ref("");
diff --git a/tests/global-version.test.ts b/tests/global-version.test.ts
new file mode 100644
index 0000000..c8914a3
--- /dev/null
+++ b/tests/global-version.test.ts
@@ -0,0 +1,34 @@
+import { describe, it, expect, afterEach } from "vitest";
+import { getExtensionVersion, VERSION } from "../src/common/static/global";
+
+// getExtensionVersion drives the "vX.Y.Z" line shown in the popup Home footer.
+// It must prefer the manifest's version_name (full semver incl. prerelease
+// channel) over the numeric version, and degrade gracefully when there's no
+// real extension runtime (the Vitest chrome shim has no getManifest).
+describe("getExtensionVersion", () => {
+ const runtime = (globalThis as any).chrome.runtime;
+
+ afterEach(() => {
+ // The shim is shared across files — don't leak a getManifest into it.
+ delete runtime.getManifest;
+ });
+
+ it("prefers version_name when present (prerelease build)", () => {
+ runtime.getManifest = () => ({
+ version: "1.15.1.2",
+ version_name: "1.15.1-dev.2",
+ });
+ expect(getExtensionVersion()).toBe("1.15.1-dev.2");
+ });
+
+ it("falls back to numeric version on a stable build", () => {
+ runtime.getManifest = () => ({ version: "1.15.1" });
+ expect(getExtensionVersion()).toBe("1.15.1");
+ });
+
+ it("falls back to the package.json VERSION outside an extension context", () => {
+ // No getManifest on the shim → call throws → catch returns VERSION.
+ expect(runtime.getManifest).toBeUndefined();
+ expect(getExtensionVersion()).toBe(VERSION);
+ });
+});