chore(web): move App.tsx's module-level block into files (#2127)

Phase 0 of #2126. Lines 239-668 of clients/web/src/App.tsx were already at
module scope — helpers, formatters, constants, and the toast subcomponents,
none of which close over component state — so the whole block moves verbatim
into files. App.tsx drops from 5,303 to 4,892 lines with no behavior change.

Placement follows the lib-vs-utils rule in AGENTS.md: pure computation to
utils/, anything touching the environment to lib/.

EMPTY_SETTINGS and the two loose toast ids deliberately do NOT land in a new
top-level src/constants.ts, which the issue suggested: the web coverage
include is a whitelist naming components/hooks/theme/lib/utils/server, so a
module there would have fallen out of the >=90 gate silently. They go under
utils/ instead.

ReAuthBannerBar is in the block but not in the issue's table — it was added
after the issue was written (#2108). It moves to components/groups/ReAuthBanner/,
beside the banner it wraps.

Every new module is at 100% on all four coverage dimensions. Two v8 ignores,
each justified in place: getAuthToken's typeof-window guard (a happy-dom
inherent path) and replayProtocolRequest's provably-dead default case.

Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
cliffhall 2026-08-26 18:07:40 -04:00
commit b6ed5072f4
8 changed files with 1232 additions and 453 deletions

View file

@ -0,0 +1,38 @@
import { List, Stack, Text } from "@mantine/core";
import { ToastCauseList, ToastLinkButton } from "./ToastPrimitives";
// Body of the "response body dropped" warning toast: a one-line summary of what
// happened, the likely causes, and a link that opens this server's settings
// (on the Options section) so the user can raise the Network Log Size if it's
// just a high-traffic server. Surfaces the otherwise-invisible rotation drop
// described in #1390.
export const FetchBodyDroppedToastMessage = ({
maxFetchRequests,
onAdjust,
}: {
maxFetchRequests: number;
onAdjust: () => void;
}) => (
<Stack gap={4}>
<Text size="sm">
A response body arrived after its Network log entry had already rotated
out (the log hit its {maxFetchRequests}-request limit), so the body
couldn&apos;t be shown. This usually indicates:
</Text>
<ToastCauseList>
<List.Item>
a chatty or misbehaving server (notification storms, rapid polling)
</List.Item>
<List.Item>an SSE/transport reconnect or retry storm</List.Item>
<List.Item>
a slow streaming call racing against high request volume
</List.Item>
<List.Item>
the Network Log Size set too low for this server&apos;s traffic
</List.Item>
</ToastCauseList>
<ToastLinkButton onClick={onAdjust}>
Adjust Network Log Size for this server
</ToastLinkButton>
</Stack>
);

View file

@ -0,0 +1,22 @@
import { Stack, Text } from "@mantine/core";
import { ToastLinkButton } from "./ToastPrimitives";
// Body of the output-schema-mismatch warning toast: a one-line summary plus a
// link that opens the full validation details in a modal (the raw error is far
// too long for a toast).
export const OutputValidationToastMessage = ({
onViewDetails,
}: {
onViewDetails: () => void;
}) => (
<Stack gap={4}>
<Text size="sm">
The tool result&apos;s structuredContent doesn&apos;t match the
tool&apos;s outputSchema. The inspector renders it anyway, but strict MCP
clients may not.
</Text>
<ToastLinkButton onClick={onViewDetails}>
View validation details
</ToastLinkButton>
</Stack>
);

View file

@ -0,0 +1,13 @@
import { Anchor, List } from "@mantine/core";
// Shared "list of likely causes" styling for the warning-toast bodies.
export const ToastCauseList = List.withProps({ size: "sm", spacing: 2 });
// The "open the relevant settings/details" link rendered at the bottom of each
// warning-toast body. Same static shape across all three toasts; each passes
// its own `onClick`.
export const ToastLinkButton = Anchor.withProps({
component: "button",
type: "button",
size: "sm",
});

View file

@ -0,0 +1,34 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { fn } from "storybook/test";
import { FetchBodyDroppedToastMessage } from "./FetchBodyDroppedToastMessage";
import { OutputValidationToastMessage } from "./OutputValidationToastMessage";
import { UrlElicitationErrorToastMessage } from "./UrlElicitationErrorToastMessage";
// The bodies rendered inside a Mantine notification. They are shown here on
// their own so the copy and the affordance can be reviewed without provoking
// the failure that raises each one.
const meta: Meta = {
title: "Elements/Toasts",
};
export default meta;
/** #1390 — a response body arrived after its Network log entry rotated out. */
export const FetchBodyDropped: StoryObj<typeof FetchBodyDroppedToastMessage> = {
render: (args) => <FetchBodyDroppedToastMessage {...args} />,
args: { maxFetchRequests: 250, onAdjust: fn() },
};
/** A tool result whose `structuredContent` doesn't match its `outputSchema`. */
export const OutputValidation: StoryObj<typeof OutputValidationToastMessage> = {
render: (args) => <OutputValidationToastMessage {...args} />,
args: { onViewDetails: fn() },
};
/** A non-spec `-32042` carrying no `elicitations`, so there is no URL to open. */
export const UrlElicitationError: StoryObj<
typeof UrlElicitationErrorToastMessage
> = {
render: (args) => <UrlElicitationErrorToastMessage {...args} />,
args: { onViewDetails: fn() },
};

View file

@ -0,0 +1,87 @@
import { describe, expect, it, vi } from "vitest";
import { screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { renderWithMantine } from "../../../test/renderWithMantine";
import { FetchBodyDroppedToastMessage } from "./FetchBodyDroppedToastMessage";
import { OutputValidationToastMessage } from "./OutputValidationToastMessage";
import { UrlElicitationErrorToastMessage } from "./UrlElicitationErrorToastMessage";
import { ToastCauseList, ToastLinkButton } from "./ToastPrimitives";
describe("ToastPrimitives", () => {
it("renders the shared cause list and link button", () => {
const onClick = vi.fn();
renderWithMantine(
<>
<ToastCauseList>
<li>a cause</li>
</ToastCauseList>
<ToastLinkButton onClick={onClick}>Do the thing</ToastLinkButton>
</>,
);
expect(screen.getByText("a cause")).toBeInTheDocument();
expect(
screen.getByRole("button", { name: "Do the thing" }),
).toBeInTheDocument();
});
});
describe("FetchBodyDroppedToastMessage", () => {
it("names the limit that dropped the body and lists the likely causes", () => {
renderWithMantine(
<FetchBodyDroppedToastMessage
maxFetchRequests={250}
onAdjust={vi.fn()}
/>,
);
expect(screen.getByText(/250/)).toBeInTheDocument();
expect(
screen.getByText(/an SSE\/transport reconnect or retry storm/),
).toBeInTheDocument();
});
it("calls onAdjust from the settings link", async () => {
const onAdjust = vi.fn();
renderWithMantine(
<FetchBodyDroppedToastMessage
maxFetchRequests={10}
onAdjust={onAdjust}
/>,
);
await userEvent.click(
screen.getByRole("button", {
name: "Adjust Network Log Size for this server",
}),
);
expect(onAdjust).toHaveBeenCalledTimes(1);
});
});
describe("OutputValidationToastMessage", () => {
it("summarizes the mismatch and opens the details modal", async () => {
const onViewDetails = vi.fn();
renderWithMantine(
<OutputValidationToastMessage onViewDetails={onViewDetails} />,
);
expect(screen.getByText(/outputSchema/)).toBeInTheDocument();
await userEvent.click(
screen.getByRole("button", { name: "View validation details" }),
);
expect(onViewDetails).toHaveBeenCalledTimes(1);
});
});
describe("UrlElicitationErrorToastMessage", () => {
it("explains the empty elicitations list and opens the details modal", async () => {
const onViewDetails = vi.fn();
renderWithMantine(
<UrlElicitationErrorToastMessage onViewDetails={onViewDetails} />,
);
expect(
screen.getByText(/listed no required\s+elicitations/),
).toBeInTheDocument();
await userEvent.click(
screen.getByRole("button", { name: "View error details" }),
);
expect(onViewDetails).toHaveBeenCalledTimes(1);
});
});

View file

@ -0,0 +1,21 @@
import { Stack, Text } from "@mantine/core";
import { ToastLinkButton } from "./ToastPrimitives";
// Body of the non-spec URLElicitationRequired toast: the server returned a
// -32042 error with no `elicitations` list, so there's no URL to open. We keep
// the toast short and link to a modal with the raw error body.
export const UrlElicitationErrorToastMessage = ({
onViewDetails,
}: {
onViewDetails: () => void;
}) => (
<Stack gap={4}>
<Text size="sm">
The server reported a URLElicitationRequired error but listed no required
elicitations, so there&apos;s nothing to open.
</Text>
<ToastLinkButton onClick={onViewDetails}>
View error details
</ToastLinkButton>
</Stack>
);

View file

@ -0,0 +1,16 @@
import { describe, expect, it } from "vitest";
import { screen } from "@testing-library/react";
import { renderWithMantine } from "../../../test/renderWithMantine";
import { ReAuthBannerBar } from "./ReAuthBannerBar";
describe("ReAuthBannerBar", () => {
it("floats the banner centered above the page", () => {
renderWithMantine(
<ReAuthBannerBar data-testid="bar">contents</ReAuthBannerBar>,
);
const bar = screen.getByTestId("bar");
expect(bar).toHaveTextContent("contents");
expect(bar.style.transform).toBe("translate(-50%, -50%)");
expect(bar.style.zIndex).toBe("200");
});
});

View file

@ -0,0 +1,38 @@
import { Paper } from "@mantine/core";
// The re-auth popup. A `Paper` so every static style is a prop; the stacking
// order goes through `styles.root` since Mantine has no `z` prop.
//
// Floats top-right rather than spanning the top as a sticky full-bleed bar.
// The bar cost the whole view a band of vertical space for what is a
// notification about one server, and it sat directly above the monitoring
// sidebar that an OAuth failure now opens (#2108) — the two things a user needs
// at once here are this affordance and those requests, so it must not push them
// around. `fixed`, not `sticky`, so scrolling the server list leaves it put.
//
// Centered, and deliberately WITHOUT an overlay. Every corner is spoken for —
// the toast stack owns bottom-right (see `main.tsx`), the right edge is the
// monitoring sidebar whose toolbar this would cover, and top-left is the
// Servers header — so anchoring it anywhere hides something. Centering is the
// one placement that reads as addressed to the whole window rather than
// attached to the wrong panel.
//
// The missing overlay is the point, not an omission: this is a notification,
// not a decision that must be made now. An OAuth failure opens the monitoring
// sidebar (#2108), and blocking the page would force a choice between reading
// those requests and keeping the affordance — dismissing is not free, since
// "Authorize again" also clears the stale OAuth state, which a plain reconnect
// does not do. So it floats above the page and leaves it usable.
//
// `transform` goes through `styles.root` for the same reason `zIndex` does:
// Mantine exposes neither as a style prop.
export const ReAuthBannerBar = Paper.withProps({
pos: "fixed",
top: "50%",
left: "50%",
w: 420,
bg: "var(--mantine-color-body)",
shadow: "xl",
radius: "md",
styles: { root: { transform: "translate(-50%, -50%)", zIndex: 200 } },
});