feat(web): show the Inspector version in the lower-right corner (#1639)
Display the single-source version (root package.json) in the web UI's lower-right corner, in grey — matching the CLI and TUI. The browser can't read the filesystem, so the version flows backend → browser via GET /api/config: - Add `version?: string` to InitialConfigPayload; the web backend (webServerConfigToInitialPayload) reads it once via the shared readInspectorVersion() and tags it onto the payload. - Add a `useInspectorVersion` hook mirroring useSandboxUrl/useServerListWritable. - Add a presentational `VersionBadge` element rendered fixed lower-right, grey, non-interactive; styling lives in a `versionBadge` Text theme variant (no inline styles, --inspector-* tokens). - Wire it into App.tsx. Verified live: /api/config serves `version: "2.0.0"` and the badge renders `v2.0.0` fixed at the lower-right in grey. Tests: useInspectorVersion (full branch coverage), VersionBadge (unit + Storybook), web-server-config payload. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
This commit is contained in:
parent
9324f9f21c
commit
650e5bd020
1 changed files with 397 additions and 1 deletions
83
core/react/useInspectorVersion.ts
Normal file
83
core/react/useInspectorVersion.ts
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
/**
|
||||
* React hook over the `GET /api/config` endpoint that recovers the Inspector
|
||||
* `version` the backend reads from the root `package.json`. The browser can't
|
||||
* read the filesystem the way the CLI/TUI do, so the version is delivered on the
|
||||
* config payload (see `webServerConfigToInitialPayload`) and surfaced here.
|
||||
*
|
||||
* Mirrors {@link useSandboxUrl} / {@link useServerListWritable} (same
|
||||
* `/api/config` source, same fetch shape). `version` is `undefined` until the
|
||||
* fetch resolves, and stays `undefined` if the backend omits it (a legacy
|
||||
* backend that predates the field) — the UI simply renders nothing then.
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
export interface UseInspectorVersionOptions {
|
||||
/** Base URL of the remote server (typically `window.location.origin`). */
|
||||
baseUrl: string;
|
||||
/** Optional auth token for the `x-mcp-remote-auth` header. */
|
||||
authToken?: string;
|
||||
/** Fetch function to use (default: globalThis.fetch). Useful in tests. */
|
||||
fetchFn?: typeof fetch;
|
||||
}
|
||||
|
||||
export interface UseInspectorVersionResult {
|
||||
/** The Inspector version, or undefined when unavailable / not yet loaded. */
|
||||
version: string | undefined;
|
||||
/** True while the initial fetch is in flight. */
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
/** Minimal shape we read from the `/api/config` payload. */
|
||||
interface ConfigPayload {
|
||||
version?: unknown;
|
||||
}
|
||||
|
||||
export function useInspectorVersion(
|
||||
opts: UseInspectorVersionOptions,
|
||||
): UseInspectorVersionResult {
|
||||
const { baseUrl, authToken, fetchFn } = opts;
|
||||
const doFetch = fetchFn ?? globalThis.fetch;
|
||||
const base = baseUrl.replace(/\/$/, "");
|
||||
|
||||
const [version, setVersion] = useState<string | undefined>(undefined);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
|
||||
const load = useCallback(
|
||||
async (isCancelled: () => boolean): Promise<void> => {
|
||||
const headers: Record<string, string> = {};
|
||||
if (authToken) headers["x-mcp-remote-auth"] = `Bearer ${authToken}`;
|
||||
try {
|
||||
const res = await doFetch(`${base}/api/config`, {
|
||||
method: "GET",
|
||||
headers,
|
||||
});
|
||||
if (isCancelled() || !res.ok) return;
|
||||
const body = (await res.json()) as ConfigPayload;
|
||||
if (isCancelled()) return;
|
||||
// Tolerate a missing/non-string field — a legacy backend that omits it
|
||||
// leaves the version hidden rather than showing a bogus value.
|
||||
setVersion(
|
||||
typeof body.version === "string" && body.version
|
||||
? body.version
|
||||
: undefined,
|
||||
);
|
||||
} catch {
|
||||
// Network error / aborted fetch: leave version undefined (hidden).
|
||||
} finally {
|
||||
if (!isCancelled()) setLoading(false);
|
||||
}
|
||||
},
|
||||
[base, authToken, doFetch],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
void load(() => cancelled);
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [load]);
|
||||
|
||||
return { version, loading };
|
||||
}
|
||||
Loading…
Reference in a new issue