inspector/core/react/useInspectorVersion.ts
cliffhall 650e5bd020 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
2026-07-10 19:07:31 -04:00

83 lines
2.8 KiB
TypeScript

/**
* 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 };
}