From 1065c1d6978479a671f557b38927c53f4b4cd7c2 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Thu, 30 Jul 2026 08:58:34 +0800 Subject: [PATCH] fix(ui): identify source builds in settings footer (#116134) * fix(ui): identify source builds in settings footer * fix(release): classify all official Control UI builds --- .github/workflows/macos-release.yml | 2 + .github/workflows/openclaw-npm-release.yml | 4 ++ scripts/control-ui-mock-dev.ts | 1 + scripts/package-mac-dist.sh | 1 + scripts/release-fast-pretag-check.sh | 2 +- ...nclaw-npm-extended-stable-workflow.test.ts | 10 +++-- .../package-acceptance-workflow.test.ts | 8 ++++ test/scripts/package-mac-dist.test.ts | 9 +++++ ui/src/app/app-host.ts | 2 +- ui/src/app/gateway-store.test.ts | 1 + ui/src/app/vite-config.node.test.ts | 35 +++++++++++++++++ ui/src/build-info-normalizers.ts | 15 ++++++-- ui/src/build-info-types.ts | 1 + ui/src/build-info.test.ts | 12 ++++-- ui/src/components/settings-sidebar.test.ts | 38 +++++++++++++------ ui/src/components/settings-sidebar.ts | 12 ++++-- .../components/sidebar-build-chip-format.ts | 25 ++++++++++++ .../sidebar-build-chip.node.test.ts | 34 ++++++++++++++++- ui/src/components/sidebar-build-chip.ts | 12 +++++- ui/src/components/viewer-facepile.test.ts | 1 + ui/src/e2e/build-info-unicode.e2e.test.ts | 1 + ui/src/pages/about/view.test.ts | 2 + ui/src/styles/layout.css | 2 +- ui/src/test-helpers/control-ui-e2e.ts | 1 + ui/vite.config.ts | 7 +++- 25 files changed, 205 insertions(+), 33 deletions(-) diff --git a/.github/workflows/macos-release.yml b/.github/workflows/macos-release.yml index 14c20675afbd..06efaf59d580 100644 --- a/.github/workflows/macos-release.yml +++ b/.github/workflows/macos-release.yml @@ -59,6 +59,8 @@ jobs: run: pnpm build - name: Build Control UI + env: + OPENCLAW_CONTROL_UI_RELEASE_BUILD: "1" run: pnpm ui:build - name: Validate release tag and package metadata diff --git a/.github/workflows/openclaw-npm-release.yml b/.github/workflows/openclaw-npm-release.yml index a1f606b5a94b..3a45efde4463 100644 --- a/.github/workflows/openclaw-npm-release.yml +++ b/.github/workflows/openclaw-npm-release.yml @@ -199,6 +199,8 @@ jobs: - name: Build if: steps.dist_build_cache.outputs.cache-hit != 'true' + env: + OPENCLAW_CONTROL_UI_RELEASE_BUILD: "1" run: pnpm build - name: Pack publishable core packages @@ -244,6 +246,8 @@ jobs: - name: Build Control UI if: steps.dist_build_cache.outputs.cache-hit != 'true' + env: + OPENCLAW_CONTROL_UI_RELEASE_BUILD: "1" run: pnpm ui:build - name: Save preflight build outputs diff --git a/scripts/control-ui-mock-dev.ts b/scripts/control-ui-mock-dev.ts index 75e310b661dd..e54e510e06a4 100644 --- a/scripts/control-ui-mock-dev.ts +++ b/scripts/control-ui-mock-dev.ts @@ -2354,6 +2354,7 @@ const server = await createServer({ commit: "0123456789abcdef0123456789abcdef01234567", commitAt: "2026-07-10T11:22:33.000Z", builtAt: "2026-07-10T12:34:56.000Z", + release: false, buildId: "mock", }), }, diff --git a/scripts/package-mac-dist.sh b/scripts/package-mac-dist.sh index 86043d54520f..3fd26f910127 100755 --- a/scripts/package-mac-dist.sh +++ b/scripts/package-mac-dist.sh @@ -20,6 +20,7 @@ APP_VERSION_INPUT="${APP_VERSION:-}" # Default to universal binary for distribution builds (supports both Apple Silicon and Intel Macs) export BUILD_ARCHS="${BUILD_ARCHS:-all}" export BUILD_CONFIG +export OPENCLAW_CONTROL_UI_RELEASE_BUILD=1 DSYM_ARCHS_VALUE="$BUILD_ARCHS" if [[ "$DSYM_ARCHS_VALUE" == "all" ]]; then DSYM_ARCHS_VALUE="arm64 x86_64" diff --git a/scripts/release-fast-pretag-check.sh b/scripts/release-fast-pretag-check.sh index 9831785efd02..a11f5672a230 100755 --- a/scripts/release-fast-pretag-check.sh +++ b/scripts/release-fast-pretag-check.sh @@ -14,5 +14,5 @@ pnpm release:plugins:npm:check -- --selection-mode all-publishable pnpm release:plugins:clawhub:check -- --selection-mode all-publishable node --import tsx scripts/plugin-release-pretag-pack-check.ts pnpm build -pnpm ui:build +OPENCLAW_CONTROL_UI_RELEASE_BUILD=1 pnpm ui:build pnpm release:openclaw:npm:check diff --git a/test/scripts/openclaw-npm-extended-stable-workflow.test.ts b/test/scripts/openclaw-npm-extended-stable-workflow.test.ts index 1a2cf4c49f38..4ee1615a503a 100644 --- a/test/scripts/openclaw-npm-extended-stable-workflow.test.ts +++ b/test/scripts/openclaw-npm-extended-stable-workflow.test.ts @@ -165,10 +165,12 @@ describe("minimal npm extended-stable workflow", () => { // Only the build producers skip on a cache hit; every validation step // still runs against the restored artifacts. - expect(step(preflight, "Build").if).toBe("steps.dist_build_cache.outputs.cache-hit != 'true'"); - expect(step(preflight, "Build Control UI").if).toBe( - "steps.dist_build_cache.outputs.cache-hit != 'true'", - ); + const build = step(preflight, "Build"); + const buildControlUi = step(preflight, "Build Control UI"); + expect(build.if).toBe("steps.dist_build_cache.outputs.cache-hit != 'true'"); + expect(build.env?.OPENCLAW_CONTROL_UI_RELEASE_BUILD).toBe("1"); + expect(buildControlUi.if).toBe("steps.dist_build_cache.outputs.cache-hit != 'true'"); + expect(buildControlUi.env?.OPENCLAW_CONTROL_UI_RELEASE_BUILD).toBe("1"); expect(step(preflight, "Check").if).toBeUndefined(); expect(step(preflight, "Verify release contents").if).toBeUndefined(); expect(step(preflight, "Verify prepared npm tarball install").if).toBeUndefined(); diff --git a/test/scripts/package-acceptance-workflow.test.ts b/test/scripts/package-acceptance-workflow.test.ts index 7840ede71e30..115f4a117497 100644 --- a/test/scripts/package-acceptance-workflow.test.ts +++ b/test/scripts/package-acceptance-workflow.test.ts @@ -4431,9 +4431,17 @@ wait_for_run plugin-clawhub-new.yml 123 "${expectedSha}" || status=$? "validate_macos_release_request", ); const stepNames = validateJob.steps?.map((step) => step.name) ?? []; + const buildControlUi = validateJob.steps?.find((step) => step.name === "Build Control UI"); expect(stepNames).not.toContain("Ensure matching GitHub release exists"); expect(macosRelease.jobs?.validate_macos_release_request).toBeDefined(); + expect(buildControlUi?.env?.OPENCLAW_CONTROL_UI_RELEASE_BUILD).toBe("1"); + }); + + it("classifies fast pretag Control UI output as a release artifact", () => { + const script = readFileSync("scripts/release-fast-pretag-check.sh", "utf8"); + + expect(script).toContain("OPENCLAW_CONTROL_UI_RELEASE_BUILD=1 pnpm ui:build"); }); it("keeps every tracked repository skill visible to Git-aware syncs", () => { diff --git a/test/scripts/package-mac-dist.test.ts b/test/scripts/package-mac-dist.test.ts index b505d517fdbe..e656d5324018 100644 --- a/test/scripts/package-mac-dist.test.ts +++ b/test/scripts/package-mac-dist.test.ts @@ -91,6 +91,15 @@ describe("package-mac-dist plist validation", () => { expect(releaseBlock).not.toContain("*.debug"); }); + it("marks the distributed Control UI as an official release artifact", () => { + const script = readFileSync(scriptPath, "utf8"); + const releaseMarkerIndex = script.indexOf("export OPENCLAW_CONTROL_UI_RELEASE_BUILD=1"); + const packageAppIndex = script.indexOf('"$ROOT_DIR/scripts/package-mac-app.sh"'); + + expect(releaseMarkerIndex).toBeGreaterThanOrEqual(0); + expect(packageAppIndex).toBeGreaterThan(releaseMarkerIndex); + }); + it("does not mask canonical Sparkle build failures for release packaging", () => { const script = readFileSync(scriptPath, "utf8"); diff --git a/ui/src/app/app-host.ts b/ui/src/app/app-host.ts index 57c94108020a..2c08444afb51 100644 --- a/ui/src/app/app-host.ts +++ b/ui/src/app/app-host.ts @@ -2033,7 +2033,7 @@ class OpenClawShell extends OpenClawLightDomElement { offline: gatewaySnapshot.offlineStable, queuedOutboxCount: storedOutboxes?.total ?? 0, lastError: gatewaySnapshot.lastError, - version: + gatewayVersion: context.config.current.serverVersion ?? gatewaySnapshot.hello?.server?.version ?? "", updateAvailable: navigationSurfaceHidden ? null : overlaySnapshot.updateAvailable, updateRunning: overlaySnapshot.updateRunning, diff --git a/ui/src/app/gateway-store.test.ts b/ui/src/app/gateway-store.test.ts index b95a877ebb6f..f88b41f20b8b 100644 --- a/ui/src/app/gateway-store.test.ts +++ b/ui/src/app/gateway-store.test.ts @@ -18,6 +18,7 @@ vi.mock("../build-info.ts", () => ({ builtAt: null, branch: null, dirty: null, + release: false, buildId: "test", }, })); diff --git a/ui/src/app/vite-config.node.test.ts b/ui/src/app/vite-config.node.test.ts index 16c291832f23..e08707cd185b 100644 --- a/ui/src/app/vite-config.node.test.ts +++ b/ui/src/app/vite-config.node.test.ts @@ -62,6 +62,7 @@ describe("Control UI Vite config", () => { builtAt: "2026-07-10T12:34:56.000Z", branch: null, dirty: null, + release: false, buildId: "2026.7.10-0123456789ab-2026-07-10T12-34-56.000Z", }); expect(readGitCommit).not.toHaveBeenCalled(); @@ -86,10 +87,44 @@ describe("Control UI Vite config", () => { builtAt: "2026-07-10T13:14:15.000Z", branch: null, dirty: null, + release: false, buildId: "aaaaaaaaaaaa-2026-07-10T13-14-15.000Z", }); }); + it("records release packaging as an explicit artifact fact", () => { + expect( + resolveControlUiBuildInfo({ + env: { + OPENCLAW_CONTROL_UI_RELEASE_BUILD: "1", + OPENCLAW_BUILD_TIMESTAMP: "2026-07-10T13:14:15.000Z", + }, + readGitCommit: () => "a".repeat(40), + readGitCommitTimestamp: () => null, + readGitBranch: () => "release/2026.7.10", + readGitDirty: () => false, + readPackageVersion: () => "2026.7.10", + }), + ).toMatchObject({ + version: "2026.7.10", + commit: "a".repeat(40), + branch: "release/2026.7.10", + dirty: false, + release: true, + buildId: "2026.7.10-release-aaaaaaaaaaaa-2026-07-10T13-14-15.000Z", + }); + }); + + it("rejects malformed release-build identity", () => { + expect(() => + resolveControlUiBuildInfo({ + env: { OPENCLAW_CONTROL_UI_RELEASE_BUILD: "true" }, + readGitCommit: () => null, + readPackageVersion: () => "2026.7.10", + }), + ).toThrow("OPENCLAW_CONTROL_UI_RELEASE_BUILD must be 1 when set"); + }); + it("uses checked-out Git instead of unverified GitHub workflow context", () => { const readGitCommit = vi.fn(() => "c".repeat(40)); expect( diff --git a/ui/src/build-info-normalizers.ts b/ui/src/build-info-normalizers.ts index ed50d1c5b15e..9c38c5e61e85 100644 --- a/ui/src/build-info-normalizers.ts +++ b/ui/src/build-info-normalizers.ts @@ -3,7 +3,10 @@ import { truncateUtf16Safe } from "../../packages/normalization-core/src/utf16-slice.js"; import type { ControlUiBuildInfo } from "./build-info-types.ts"; -type ControlUiBuildMetadata = Pick; +type ControlUiBuildMetadata = Pick< + ControlUiBuildInfo, + "version" | "commit" | "builtAt" | "release" +>; const FULL_GIT_SHA = /^[0-9a-f]{40}$/u; const UTC_BUILD_TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/u; @@ -44,7 +47,12 @@ function normalizeControlUiBuildId(value: unknown): string { } function deriveControlUiBuildId(info: ControlUiBuildMetadata): string { - const identity = [info.version, info.commit?.slice(0, 12), info.builtAt] + const identity = [ + info.version, + info.release ? "release" : null, + info.commit?.slice(0, 12), + info.builtAt, + ] .filter((value): value is string => Boolean(value)) .join("-"); return normalizeControlUiBuildId(identity); @@ -57,7 +65,8 @@ export function normalizeControlUiBuildInfo(value: unknown): ControlUiBuildInfo const version = optionalString(record.version); const commit = normalizeControlUiCommit(record.commit); const builtAt = normalizeControlUiBuildTimestamp(record.builtAt); - const metadata = { version, commit, builtAt }; + const release = record.release === true; + const metadata = { version, commit, builtAt, release }; return { ...metadata, commitAt: normalizeControlUiBuildTimestamp(record.commitAt), diff --git a/ui/src/build-info-types.ts b/ui/src/build-info-types.ts index 038eaf19955e..df82bfc3ca23 100644 --- a/ui/src/build-info-types.ts +++ b/ui/src/build-info-types.ts @@ -5,5 +5,6 @@ export type ControlUiBuildInfo = Readonly<{ builtAt: string | null; branch: string | null; dirty: boolean | null; + release: boolean; buildId: string; }>; diff --git a/ui/src/build-info.test.ts b/ui/src/build-info.test.ts index 62a2a2148259..1ae56333e4cf 100644 --- a/ui/src/build-info.test.ts +++ b/ui/src/build-info.test.ts @@ -77,6 +77,7 @@ describe("Control UI build info", () => { builtAt: "later", branch: "HEAD", dirty: "yes", + release: "yes", buildId: "", }), ).toEqual({ @@ -86,14 +87,18 @@ describe("Control UI build info", () => { builtAt: null, branch: null, dirty: null, + release: false, buildId: "dev", }); }); - it("passes through normalized branch and boolean dirty state", () => { - expect(normalizeControlUiBuildInfo({ branch: " feature/x ", dirty: false })).toMatchObject({ + it("passes through normalized branch, dirty state, and release identity", () => { + expect( + normalizeControlUiBuildInfo({ branch: " feature/x ", dirty: false, release: true }), + ).toMatchObject({ branch: "feature/x", dirty: false, + release: true, }); }); @@ -103,7 +108,8 @@ describe("Control UI build info", () => { version: "2026.7.10", commit: COMMIT, builtAt: "2026-07-10T12:34:56.000Z", + release: true, }).buildId, - ).toBe("2026.7.10-0123456789ab-2026-07-10T12-34-56.000Z"); + ).toBe("2026.7.10-release-0123456789ab-2026-07-10T12-34-56.000Z"); }); }); diff --git a/ui/src/components/settings-sidebar.test.ts b/ui/src/components/settings-sidebar.test.ts index bd68ab665ee2..3ec1c99ac934 100644 --- a/ui/src/components/settings-sidebar.test.ts +++ b/ui/src/components/settings-sidebar.test.ts @@ -40,7 +40,7 @@ describe("settings sidebar search", () => { activeRouteId: "model-setup", offline: false, lastError: null, - version: "", + gatewayVersion: "", updateAvailable: null, updateRunning: false, onUpdate: vi.fn(), @@ -71,7 +71,7 @@ describe("settings sidebar search", () => { activeRouteId: "appearance", offline: false, lastError: null, - version: "", + gatewayVersion: "", updateAvailable: null, updateRunning: false, onUpdate: vi.fn(), @@ -101,7 +101,7 @@ describe("settings sidebar search", () => { activeRouteId: "appearance", offline: false, lastError: null, - version: "", + gatewayVersion: "", updateAvailable: null, updateRunning: false, onUpdate: vi.fn(), @@ -139,7 +139,7 @@ describe("settings sidebar search", () => { activeRouteId: "appearance", offline: false, lastError: null, - version: "", + gatewayVersion: "", updateAvailable: null, updateRunning: false, onUpdate: vi.fn(), @@ -194,7 +194,7 @@ describe("settings sidebar search", () => { activeRouteId: "appearance", offline: false, lastError: null, - version: "", + gatewayVersion: "", updateAvailable: null, updateRunning: false, onUpdate: vi.fn(), @@ -242,7 +242,7 @@ describe("settings sidebar search", () => { activeRouteId: "agents", offline: false, lastError: null, - version: "", + gatewayVersion: "", updateAvailable: null, updateRunning: false, onUpdate: vi.fn(), @@ -273,7 +273,7 @@ describe("settings sidebar search", () => { activeHash: "#memory-backend", offline: false, lastError: null, - version: "", + gatewayVersion: "", updateAvailable: null, updateRunning: false, onUpdate: vi.fn(), @@ -318,7 +318,7 @@ describe("settings sidebar search", () => { activeRouteId: "appearance", offline: false, lastError: null, - version: "", + gatewayVersion: "", updateAvailable: null, updateRunning: false, onUpdate: vi.fn(), @@ -399,7 +399,7 @@ describe("settings sidebar search", () => { activeRouteId: "appearance", offline: false, lastError: null, - version: "", + gatewayVersion: "", updateAvailable: null, updateRunning: false, onUpdate: vi.fn(), @@ -424,13 +424,14 @@ describe("settings sidebar search", () => { it("keeps the update card above the settings footer", async () => { const onUpdate = vi.fn(); + const onNavigate = vi.fn(); render( renderSettingsSidebar({ basePath: "", activeRouteId: "appearance", offline: false, lastError: null, - version: "1.0.0", + gatewayVersion: "1.0.0", updateAvailable: { currentVersion: "1.0.0", latestVersion: "2.0.0", @@ -441,7 +442,7 @@ describe("settings sidebar search", () => { searchQuery: "", onExit: vi.fn(), onRetryConnect: vi.fn(), - onNavigate: vi.fn(), + onNavigate, onSearchQueryChange: vi.fn(), preloadTimers: new Map(), saveIndicator: saveIndicator(), @@ -456,6 +457,19 @@ describe("settings sidebar search", () => { expect(card?.nextElementSibling?.classList.contains("settings-sidebar__footer")).toBe(true); card?.querySelector(".sidebar-update-card__action")?.click(); expect(onUpdate).toHaveBeenCalledOnce(); + + const buildChip = container.querySelector< + HTMLElement & { + gatewayVersion: string | null; + variant: string; + updateComplete: Promise; + } + >("openclaw-sidebar-build-chip"); + await buildChip?.updateComplete; + expect(buildChip?.gatewayVersion).toBe("1.0.0"); + expect(buildChip?.variant).toBe("settings"); + buildChip?.querySelector(".sidebar-footer-build")?.click(); + expect(onNavigate).toHaveBeenCalledWith("about"); }); it("shows the offline retry action without an online status", () => { @@ -468,7 +482,7 @@ describe("settings sidebar search", () => { offline, queuedOutboxCount, lastError, - version: "1.0.0", + gatewayVersion: "1.0.0", updateAvailable: null, updateRunning: false, onUpdate: vi.fn(), diff --git a/ui/src/components/settings-sidebar.ts b/ui/src/components/settings-sidebar.ts index a07ccf0faf24..cce1370bb0e9 100644 --- a/ui/src/components/settings-sidebar.ts +++ b/ui/src/components/settings-sidebar.ts @@ -23,6 +23,7 @@ import { redactLoginFailureError } from "./login-gate.ts"; import { renderOfflineSidebarStatus } from "./session-row-badges.ts"; import type { SettingsSaveIndicatorProps } from "./settings-save-indicator.ts"; import "./settings-save-indicator.ts"; +import "./sidebar-build-chip.ts"; import "./sidebar-update-card.ts"; type SettingsSidebarProps = { @@ -34,7 +35,7 @@ type SettingsSidebarProps = { offline: boolean; queuedOutboxCount?: number; lastError: string | null; - version: string; + gatewayVersion: string; updateAvailable: UpdateAvailable | null; updateRunning: boolean; onUpdate: () => void; @@ -329,9 +330,12 @@ export function renderSettingsSidebar(props: SettingsSidebarProps) { : html``} - ${props.version - ? html`${props.version}` - : nothing} + props.onNavigate("about")} + > `; diff --git a/ui/src/components/sidebar-build-chip-format.ts b/ui/src/components/sidebar-build-chip-format.ts index 739276b6af96..61c4d57630e8 100644 --- a/ui/src/components/sidebar-build-chip-format.ts +++ b/ui/src/components/sidebar-build-chip-format.ts @@ -25,6 +25,31 @@ export function formatBuildChipText(info: ControlUiBuildInfo): string | null { return `${branch}${commit}`; } +export function formatSettingsBuildLabel( + info: ControlUiBuildInfo, + gatewayVersion: string | null, +): string | null { + const version = info.version ?? gatewayVersion; + const commit = info.commit; + if (!commit) { + return version; + } + const compactBuild = formatBuildChipText(info); + if (!compactBuild) { + return version; + } + + if (info.release) { + return version; + } + + const gitIdentity = + info.branch && info.branch !== "main" + ? compactBuild + : `git@${commit.slice(0, 7)}${info.dirty === true ? "*" : ""}`; + return [version, gitIdentity].filter((value): value is string => Boolean(value)).join(" · "); +} + function formatBuildCardDetails(info: ControlUiBuildInfo, gatewayVersion: string | null) { return { summary: [ diff --git a/ui/src/components/sidebar-build-chip.node.test.ts b/ui/src/components/sidebar-build-chip.node.test.ts index e71bece4045f..ccb64e3ae530 100644 --- a/ui/src/components/sidebar-build-chip.node.test.ts +++ b/ui/src/components/sidebar-build-chip.node.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; import type { ControlUiBuildInfo } from "../build-info.ts"; -import { formatBuildChipText } from "./sidebar-build-chip-format.ts"; +import { formatBuildChipText, formatSettingsBuildLabel } from "./sidebar-build-chip-format.ts"; const COMMIT = "e8cbc62f0123456789abcdef0123456789abcdef"; const BUILT_AT = "2026-07-10T12:00:00.000Z"; @@ -13,6 +13,7 @@ function buildInfo(overrides: Partial = {}): ControlUiBuildI builtAt: BUILT_AT, branch: "main", dirty: false, + release: false, buildId: "test", ...overrides, }; @@ -67,3 +68,34 @@ describe("formatBuildChipText", () => { }); } }); + +describe("formatSettingsBuildLabel", () => { + it("keeps official release artifacts version-only", () => { + expect(formatSettingsBuildLabel(buildInfo({ release: true }), "2026.7.9")).toBe("2026.7.10"); + }); + + it("adds a Git identity for clean main builds", () => { + expect(formatSettingsBuildLabel(buildInfo(), "2026.7.9")).toBe("2026.7.10 · git@e8cbc62"); + }); + + it("adds branch and dirty provenance for development builds", () => { + expect(formatSettingsBuildLabel(buildInfo({ branch: "feat/x", dirty: true }), "2026.7.9")).toBe( + "2026.7.10 · feat/x@e8cbc62*", + ); + }); + + it("falls back to the Gateway version when artifact metadata is unavailable", () => { + expect( + formatSettingsBuildLabel( + buildInfo({ version: null, commit: null, branch: null, dirty: null }), + "2026.7.9", + ), + ).toBe("2026.7.9"); + }); + + it("keeps detached clean source builds distinguishable from releases", () => { + expect(formatSettingsBuildLabel(buildInfo({ branch: null, dirty: false }), "2026.7.9")).toBe( + "2026.7.10 · git@e8cbc62", + ); + }); +}); diff --git a/ui/src/components/sidebar-build-chip.ts b/ui/src/components/sidebar-build-chip.ts index edd50ebc70e5..463577c24e37 100644 --- a/ui/src/components/sidebar-build-chip.ts +++ b/ui/src/components/sidebar-build-chip.ts @@ -4,7 +4,11 @@ import { pathForRoute } from "../app-route-paths.ts"; import { CONTROL_UI_BUILD_INFO } from "../build-info.ts"; import { t } from "../i18n/index.ts"; import { OpenClawLightDomContentsElement } from "../lit/openclaw-element.ts"; -import { formatBuildChipText, renderSidebarServerDetails } from "./sidebar-build-chip-format.ts"; +import { + formatBuildChipText, + formatSettingsBuildLabel, + renderSidebarServerDetails, +} from "./sidebar-build-chip-format.ts"; import "./tooltip.ts"; function shouldHandleNavigationClick(event: MouseEvent): boolean { @@ -23,9 +27,13 @@ class SidebarBuildChip extends OpenClawLightDomContentsElement { @property({ attribute: false }) basePath = ""; @property({ attribute: false }) gatewayVersion: string | null = null; @property({ attribute: false }) onNavigate?: (routeId: "about") => void; + @property({ attribute: false }) variant: "compact" | "settings" = "compact"; override render() { - const text = formatBuildChipText(CONTROL_UI_BUILD_INFO); + const text = + this.variant === "settings" + ? formatSettingsBuildLabel(CONTROL_UI_BUILD_INFO, this.gatewayVersion) + : formatBuildChipText(CONTROL_UI_BUILD_INFO); if (!text) { return nothing; } diff --git a/ui/src/components/viewer-facepile.test.ts b/ui/src/components/viewer-facepile.test.ts index 354e94f0478a..6aeab7c3856c 100644 --- a/ui/src/components/viewer-facepile.test.ts +++ b/ui/src/components/viewer-facepile.test.ts @@ -166,6 +166,7 @@ const BUILD_INFO: ControlUiBuildInfo = { builtAt: "2026-07-20T10:30:00.000Z", branch: "main", dirty: true, + release: false, buildId: "test", }; diff --git a/ui/src/e2e/build-info-unicode.e2e.test.ts b/ui/src/e2e/build-info-unicode.e2e.test.ts index ddaf22b87b15..d52bb2c1a61d 100644 --- a/ui/src/e2e/build-info-unicode.e2e.test.ts +++ b/ui/src/e2e/build-info-unicode.e2e.test.ts @@ -85,6 +85,7 @@ describeControlUiE2e("Control UI Unicode build identity mocked Gateway E2E", () builtAt: "2026-07-10T12:34:56.000Z", branch: RAW_BRANCH, dirty: true, + release: false, buildId: "build-info-unicode-e2e", }); browser = await chromium.launch({ executablePath: chromiumExecutablePath }); diff --git a/ui/src/pages/about/view.test.ts b/ui/src/pages/about/view.test.ts index eb45d8f32147..3bdb293665b4 100644 --- a/ui/src/pages/about/view.test.ts +++ b/ui/src/pages/about/view.test.ts @@ -20,6 +20,7 @@ function createProps(overrides: Partial = {}): AboutProps { builtAt: BUILT_AT, branch: "feature/build-chip", dirty: true, + release: false, buildId: "test", }, gatewayVersion: "2026.7.9", @@ -162,6 +163,7 @@ describe("renderAbout", () => { builtAt: null, branch: null, dirty: null, + release: false, buildId: "dev", }, gatewayVersion: null, diff --git a/ui/src/styles/layout.css b/ui/src/styles/layout.css index 0d9a541fa486..da2b233fde7b 100644 --- a/ui/src/styles/layout.css +++ b/ui/src/styles/layout.css @@ -681,7 +681,7 @@ openclaw-settings-save-indicator { } } -.settings-sidebar__footer-version { +.settings-sidebar__footer .sidebar-footer-build { margin-left: auto; flex-shrink: 0; font-variant-numeric: tabular-nums; diff --git a/ui/src/test-helpers/control-ui-e2e.ts b/ui/src/test-helpers/control-ui-e2e.ts index 192b36f6aa98..1f3732148710 100644 --- a/ui/src/test-helpers/control-ui-e2e.ts +++ b/ui/src/test-helpers/control-ui-e2e.ts @@ -306,6 +306,7 @@ export async function startControlUiE2eServer( builtAt: "2026-07-10T12:34:56.000Z", branch: null, dirty: false, + release: false, buildId: "e2e", }, ): Promise { diff --git a/ui/vite.config.ts b/ui/vite.config.ts index f231fa76d81a..53e808aa6d50 100644 --- a/ui/vite.config.ts +++ b/ui/vite.config.ts @@ -223,7 +223,12 @@ export function resolveControlUiBuildInfo( normalizeControlUiBuildInfo({ branch: githubBranch }).branch ?? normalizeControlUiBuildInfo({ branch: (sources.readGitBranch ?? readGitBranch)() }).branch; const dirty = (sources.readGitDirty ?? readGitDirty)(); - const metadata = { version, commit, builtAt }; + const releaseFlag = env.OPENCLAW_CONTROL_UI_RELEASE_BUILD?.trim(); + if (releaseFlag && releaseFlag !== "1") { + throw new Error("OPENCLAW_CONTROL_UI_RELEASE_BUILD must be 1 when set"); + } + const release = releaseFlag === "1"; + const metadata = { version, commit, builtAt, release }; const explicitBuildId = env.OPENCLAW_CONTROL_UI_BUILD_ID?.trim(); return { ...metadata,