diff --git a/src/gateway/call.ts b/src/gateway/call.ts index d52ffcc6d08..ba1e079e455 100644 --- a/src/gateway/call.ts +++ b/src/gateway/call.ts @@ -17,6 +17,7 @@ import { type GatewayClientMode, type GatewayClientName, } from "../utils/message-channel.js"; +import { VERSION } from "../version.js"; import { GatewayClient } from "./client.js"; import { resolveGatewayCredentialsFromConfig } from "./credentials.js"; import { @@ -628,7 +629,7 @@ async function executeGatewayRequestWithScopes(params: { instanceId: opts.instanceId ?? randomUUID(), clientName: opts.clientName ?? GATEWAY_CLIENT_NAMES.CLI, clientDisplayName: opts.clientDisplayName, - clientVersion: opts.clientVersion ?? "dev", + clientVersion: opts.clientVersion ?? VERSION, platform: opts.platform, mode: opts.mode ?? GATEWAY_CLIENT_MODES.CLI, role: "operator", diff --git a/src/gateway/client.ts b/src/gateway/client.ts index a887c757df1..bcf5edd72fb 100644 --- a/src/gateway/client.ts +++ b/src/gateway/client.ts @@ -13,6 +13,7 @@ import { } from "../infra/device-identity.js"; import { clearDevicePairing } from "../infra/device-pairing.js"; import { normalizeFingerprint } from "../infra/tls/fingerprint.js"; +import { VERSION } from "../version.js"; import { rawDataToString } from "../infra/ws.js"; import { logDebug, logError } from "../logger.js"; import { @@ -21,6 +22,7 @@ import { type GatewayClientMode, type GatewayClientName, } from "../utils/message-channel.js"; +import { VERSION } from "../version.js"; import { buildDeviceAuthPayloadV3 } from "./device-auth.js"; import { isSecureWebSocketUrl } from "./net.js"; import { @@ -302,7 +304,7 @@ export class GatewayClient { client: { id: this.opts.clientName ?? GATEWAY_CLIENT_NAMES.GATEWAY_CLIENT, displayName: this.opts.clientDisplayName, - version: this.opts.clientVersion ?? "dev", + version: this.opts.clientVersion ?? VERSION, platform, deviceFamily: this.opts.deviceFamily, mode: this.opts.mode ?? GATEWAY_CLIENT_MODES.BACKEND, diff --git a/src/gateway/control-ui-contract.ts b/src/gateway/control-ui-contract.ts index 654835e0424..b53eca81db5 100644 --- a/src/gateway/control-ui-contract.ts +++ b/src/gateway/control-ui-contract.ts @@ -5,4 +5,5 @@ export type ControlUiBootstrapConfig = { assistantName: string; assistantAvatar: string; assistantAgentId: string; + serverVersion?: string; }; diff --git a/src/gateway/control-ui.ts b/src/gateway/control-ui.ts index 6075e8281a5..de8b36d47d6 100644 --- a/src/gateway/control-ui.ts +++ b/src/gateway/control-ui.ts @@ -25,6 +25,7 @@ import { normalizeControlUiBasePath, resolveAssistantAvatarUrl, } from "./control-ui-shared.js"; +import { resolveRuntimeServiceVersion } from "../version.js"; const ROOT_PREFIX = "/"; const CONTROL_UI_ASSETS_MISSING_MESSAGE = @@ -350,6 +351,7 @@ export function handleControlUiHttpRequest( assistantName: identity.name, assistantAvatar: avatarValue ?? identity.avatar, assistantAgentId: identity.agentId, + serverVersion: resolveRuntimeServiceVersion(process.env), } satisfies ControlUiBootstrapConfig); return true; } diff --git a/ui/src/ui/app-gateway.ts b/ui/src/ui/app-gateway.ts index aa324c32b4c..e61e3e8b658 100644 --- a/ui/src/ui/app-gateway.ts +++ b/ui/src/ui/app-gateway.ts @@ -69,6 +69,7 @@ type GatewayHost = { assistantName: string; assistantAvatar: string | null; assistantAgentId: string | null; + serverVersion: string | null; sessionKey: string; chatRunId: string | null; refreshSessionsAfterChat: Set; @@ -150,6 +151,7 @@ export function connectGateway(host: GatewayHost) { token: host.settings.token.trim() ? host.settings.token : undefined, password: host.password.trim() ? host.password : undefined, clientName: "openclaw-control-ui", + clientVersion: host.serverVersion ?? undefined, mode: "webchat", instanceId: host.clientInstanceId, onHello: (hello) => { diff --git a/ui/src/ui/app-lifecycle.ts b/ui/src/ui/app-lifecycle.ts index 36527c161fc..cd603b96aa5 100644 --- a/ui/src/ui/app-lifecycle.ts +++ b/ui/src/ui/app-lifecycle.ts @@ -27,6 +27,7 @@ type LifecycleHost = { assistantName: string; assistantAvatar: string | null; assistantAgentId: string | null; + serverVersion: string | null; chatHasAutoScrolled: boolean; chatManualRefreshInFlight: boolean; chatLoading: boolean; diff --git a/ui/src/ui/app.ts b/ui/src/ui/app.ts index 3b50922bdfc..2576196e2e5 100644 --- a/ui/src/ui/app.ts +++ b/ui/src/ui/app.ts @@ -135,6 +135,7 @@ export class OpenClawApp extends LitElement { @state() assistantName = bootAssistantIdentity.name; @state() assistantAvatar = bootAssistantIdentity.avatar; @state() assistantAgentId = bootAssistantIdentity.agentId ?? null; + @state() serverVersion: string | null = null; @state() sessionKey = this.settings.sessionKey; @state() chatLoading = false; diff --git a/ui/src/ui/controllers/control-ui-bootstrap.test.ts b/ui/src/ui/controllers/control-ui-bootstrap.test.ts index 29e66fab854..fbe0750ac27 100644 --- a/ui/src/ui/controllers/control-ui-bootstrap.test.ts +++ b/ui/src/ui/controllers/control-ui-bootstrap.test.ts @@ -13,6 +13,7 @@ describe("loadControlUiBootstrapConfig", () => { assistantName: "Ops", assistantAvatar: "O", assistantAgentId: "main", + serverVersion: "2026.3.2", }), }); vi.stubGlobal("fetch", fetchMock as unknown as typeof fetch); @@ -22,6 +23,7 @@ describe("loadControlUiBootstrapConfig", () => { assistantName: "Assistant", assistantAvatar: null, assistantAgentId: null, + serverVersion: null, }; await loadControlUiBootstrapConfig(state); @@ -33,6 +35,7 @@ describe("loadControlUiBootstrapConfig", () => { expect(state.assistantName).toBe("Ops"); expect(state.assistantAvatar).toBe("O"); expect(state.assistantAgentId).toBe("main"); + expect(state.serverVersion).toBe("2026.3.2"); vi.unstubAllGlobals(); }); @@ -46,6 +49,7 @@ describe("loadControlUiBootstrapConfig", () => { assistantName: "Assistant", assistantAvatar: null, assistantAgentId: null, + serverVersion: null, }; await loadControlUiBootstrapConfig(state); @@ -68,6 +72,7 @@ describe("loadControlUiBootstrapConfig", () => { assistantName: "Assistant", assistantAvatar: null, assistantAgentId: null, + serverVersion: null, }; await loadControlUiBootstrapConfig(state); diff --git a/ui/src/ui/controllers/control-ui-bootstrap.ts b/ui/src/ui/controllers/control-ui-bootstrap.ts index a996e1265d3..6542fe1a9ba 100644 --- a/ui/src/ui/controllers/control-ui-bootstrap.ts +++ b/ui/src/ui/controllers/control-ui-bootstrap.ts @@ -10,6 +10,7 @@ export type ControlUiBootstrapState = { assistantName: string; assistantAvatar: string | null; assistantAgentId: string | null; + serverVersion: string | null; }; export async function loadControlUiBootstrapConfig(state: ControlUiBootstrapState) { @@ -43,6 +44,7 @@ export async function loadControlUiBootstrapConfig(state: ControlUiBootstrapStat state.assistantName = normalized.name; state.assistantAvatar = normalized.avatar; state.assistantAgentId = normalized.agentId ?? null; + state.serverVersion = parsed.serverVersion ?? null; } catch { // Ignore bootstrap failures; UI will update identity after connecting. } diff --git a/ui/src/ui/gateway.ts b/ui/src/ui/gateway.ts index 5d0c4e73f2f..d8fd305ae3e 100644 --- a/ui/src/ui/gateway.ts +++ b/ui/src/ui/gateway.ts @@ -233,7 +233,7 @@ export class GatewayBrowserClient { maxProtocol: 3, client: { id: this.opts.clientName ?? GATEWAY_CLIENT_NAMES.CONTROL_UI, - version: this.opts.clientVersion ?? "dev", + version: this.opts.clientVersion ?? "control-ui", platform: this.opts.platform ?? navigator.platform ?? "web", mode: this.opts.mode ?? GATEWAY_CLIENT_MODES.WEBCHAT, instanceId: this.opts.instanceId,