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:
cliffhall 2026-07-10 19:07:31 -04:00
commit 650e5bd020
3 changed files with 397 additions and 1 deletions

View file

@ -0,0 +1,34 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { expect, within } from "storybook/test";
import { VersionBadge } from "./VersionBadge";
const meta: Meta<typeof VersionBadge> = {
title: "Elements/VersionBadge",
component: VersionBadge,
args: { version: "2.0.0" },
};
export default meta;
type Story = StoryObj<typeof VersionBadge>;
// Default: a grey `v2.0.0` pinned to the lower-right corner of the viewport.
export const Default: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const badge = await canvas.findByText("v2.0.0");
await expect(badge).toBeInTheDocument();
await expect(badge).toHaveAttribute(
"aria-label",
"Inspector version 2.0.0",
);
},
};
// No version yet (initial load / legacy backend): renders nothing.
export const Hidden: Story = {
args: { version: undefined },
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(canvas.queryByText(/^v/)).toBeNull();
},
};

View file

@ -0,0 +1,22 @@
import { describe, it, expect } from "vitest";
import { renderWithMantine, screen } from "../../../test/renderWithMantine";
import { VersionBadge } from "./VersionBadge";
describe("VersionBadge", () => {
it("renders the version with a `v` prefix and an accessible label", () => {
renderWithMantine(<VersionBadge version="2.0.0" />);
const badge = screen.getByText("v2.0.0");
expect(badge).toBeInTheDocument();
expect(badge).toHaveAttribute("aria-label", "Inspector version 2.0.0");
});
it("renders nothing when the version is undefined", () => {
renderWithMantine(<VersionBadge version={undefined} />);
expect(screen.queryByText(/^v/)).toBeNull();
});
it("renders nothing when the version is an empty string", () => {
renderWithMantine(<VersionBadge version="" />);
expect(screen.queryByText(/^v/)).toBeNull();
});
});

View file

@ -0,0 +1,25 @@
import { Text } from "@mantine/core";
export interface VersionBadgeProps {
/** The Inspector version to show, e.g. `"2.0.0"`. Renders nothing when absent. */
version?: string;
}
// The `versionBadge` variant (src/theme/Text.ts) pins it to the lower-right
// corner in grey and makes it non-interactive.
const VersionText = Text.withProps({ variant: "versionBadge" });
/**
* A small, unobtrusive build-version label fixed to the lower-right corner of
* the screen (#1639). Sourced from the root `package.json` via the backend's
* `GET /api/config`; renders nothing until the version is known (or on a legacy
* backend that omits it).
*/
export function VersionBadge({ version }: VersionBadgeProps) {
if (!version) return null;
return (
<VersionText aria-label={`Inspector version ${version}`}>
v{version}
</VersionText>
);
}