From 1627aec9fb635a2d22e73302d66f05a262f0fb81 Mon Sep 17 00:00:00 2001 From: xmanrui <841206367@qq.com> Date: Sat, 7 Mar 2026 03:54:42 +0800 Subject: [PATCH] feat(ui): show OpenClaw version tooltip on Gateway button --- app/api/gateway-health/route.ts | 27 +++++++++++++++++++++++++++ app/gateway-status.tsx | 16 ++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/app/api/gateway-health/route.ts b/app/api/gateway-health/route.ts index 8c3bb01..dd66ea4 100644 --- a/app/api/gateway-health/route.ts +++ b/app/api/gateway-health/route.ts @@ -8,6 +8,7 @@ const OPENCLAW_HOME = process.env.OPENCLAW_HOME || path.join(process.env.HOME || const CONFIG_PATH = path.join(OPENCLAW_HOME, "openclaw.json"); const DEGRADED_LATENCY_MS = 1500; const execFileAsync = promisify(execFile); +let cachedOpenclawVersion: { value: string | null; expiresAt: number } | null = null; function parseJsonFromMixedOutput(output: string): any { for (let i = 0; i < output.length; i++) { @@ -65,9 +66,29 @@ async function probeGatewayViaCli(token: string, timeoutMs = 5000): Promise<{ ok } } +async function getOpenclawVersion(): Promise { + const now = Date.now(); + if (cachedOpenclawVersion && cachedOpenclawVersion.expiresAt > now) { + return cachedOpenclawVersion.value || undefined; + } + try { + const { stdout } = await execFileAsync("openclaw", ["--version"], { + maxBuffer: 1024 * 1024, + env: { ...process.env, FORCE_COLOR: "0" }, + }); + const version = stdout.trim().split(/\s+/)[0] || null; + cachedOpenclawVersion = { value: version, expiresAt: now + 60 * 60 * 1000 }; + return version || undefined; + } catch { + cachedOpenclawVersion = { value: null, expiresAt: now + 60 * 1000 }; + return undefined; + } +} + export async function GET() { const startedAt = Date.now(); try { + const openclawVersion = await getOpenclawVersion(); const raw = fs.readFileSync(CONFIG_PATH, "utf-8"); const config = JSON.parse(raw); const port = config.gateway?.port || 18789; @@ -89,6 +110,7 @@ export async function GET() { return NextResponse.json({ ok: true, data, + openclawVersion, status: responseMs > DEGRADED_LATENCY_MS ? "degraded" : "healthy", checkedAt, responseMs, @@ -104,6 +126,7 @@ export async function GET() { return NextResponse.json({ ok: true, data: null, + openclawVersion, status: "healthy", checkedAt, responseMs, @@ -113,12 +136,14 @@ export async function GET() { return NextResponse.json({ ok: false, + openclawVersion, error: cli.error || `HTTP ${resp.status}`, status: "down", checkedAt, responseMs, }); } catch (err: any) { + const openclawVersion = await getOpenclawVersion(); // If HTTP probe fails due transport/runtime issues, attempt CLI probe before declaring down. const raw = err.cause?.code === "ECONNREFUSED" ? "Gateway ζœͺ运葌" @@ -150,6 +175,7 @@ export async function GET() { return NextResponse.json({ ok: true, data: null, + openclawVersion, status: "healthy", checkedAt, responseMs, @@ -158,6 +184,7 @@ export async function GET() { } return NextResponse.json({ ok: false, + openclawVersion, error: cli.error || raw, status: "down", checkedAt, diff --git a/app/gateway-status.tsx b/app/gateway-status.tsx index da91b29..dc24149 100644 --- a/app/gateway-status.tsx +++ b/app/gateway-status.tsx @@ -17,6 +17,7 @@ interface HealthResult { error?: string; data?: any; webUrl?: string; + openclawVersion?: string; } interface GatewayStatusProps { @@ -29,6 +30,7 @@ export function GatewayStatus({ compact = false, className = "", hideIconOnMobil const { t } = useI18n(); const [health, setHealth] = useState(null); const [showError, setShowError] = useState(false); + const [showVersionTip, setShowVersionTip] = useState(false); const check = useCallback(() => { fetch("/api/gateway-health") @@ -43,12 +45,21 @@ export function GatewayStatus({ compact = false, className = "", hideIconOnMobil return () => clearInterval(timer); }, [check]); + const gatewayTitle = health?.openclawVersion + ? `OpenClaw ${health.openclawVersion}` + : "OpenClaw"; + return (
setShowVersionTip(true)} + onMouseLeave={() => setShowVersionTip(false)} + onFocus={() => setShowVersionTip(true)} + onBlur={() => setShowVersionTip(false)} className={`inline-flex items-center rounded-full font-medium border hover:bg-cyan-500/30 transition-colors cursor-pointer ${ compact ? "px-2 py-1 text-[10px]" : "px-2 py-0.5 text-xs" } ${ @@ -65,6 +76,11 @@ export function GatewayStatus({ compact = false, className = "", hideIconOnMobil ) : "🦞 Gateway"} β†— + {showVersionTip && ( +
+ {gatewayTitle} +
+ )} {!health ? ( -- ) : health.ok ? (