Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 28 additions & 4 deletions clients/web/src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,16 @@
otherwise defaults to pure white. */
--inspector-surface-body: var(--mantine-color-gray-1);
--inspector-surface-card: var(--mantine-color-white);
/* Recessed list-item surface (Protocol/Network message cards): light-grey in
light mode so the entries read as sunk into their white panel; keeps the
standard card tone in dark (overridden below). */
--inspector-surface-inset: var(--mantine-color-gray-1);
/* Server-card fill: a hair lighter than the grey app background (`gray-0` vs
the body's `gray-1`) so the cards read as faintly raised on the white
Servers box. Light mode only; dark tracks the standard card surface. */
--inspector-surface-card-server: var(--mantine-color-gray-0);
/* Entry-card surface (Protocol / Network / Task message cards): the faint
`gray-0` grey — a hair lighter than the `gray-1` app background and matching
the server cards — so the entries read as soft cards on their white panel
(their inner Code blocks are raised to white via the `inset` variant). Keeps
the standard card tone in dark (overridden below). */
--inspector-surface-inset: var(--mantine-color-gray-0);
/* Code/content surfaces match the light-grey body so they stay distinct
against the white cards (the Code block has no border of its own). Dark
mode keeps the darker `dark-9` inset below. */
Expand Down Expand Up @@ -125,6 +131,9 @@
tab bar share `gray-1`. Cards (`gray-9`) still sit a step above it. */
--inspector-surface-body: var(--mantine-color-dark-8);
--inspector-surface-card: var(--mantine-color-gray-9);
/* Dark keeps server cards at the standard card surface (the lighter grey is a
light-mode-only treatment). */
--inspector-surface-card-server: var(--inspector-surface-card);
/* Dark keeps the message cards at the standard card tone (they already
contrast against the darker panel/body), so only light mode goes grey. */
--inspector-surface-inset: var(--mantine-color-gray-9);
Expand Down Expand Up @@ -225,6 +234,21 @@ body {
background-color: var(--inspector-surface-body);
}

/* Floor the app at 1280px so the header, split panes, and footer never reflow
into a broken layout; below this the whole app scrolls horizontally instead.
Scoped to the app mount (#root) rather than `body` so it doesn't leak into
Storybook, which shares App.css but renders stories into its own root.

The `transform` makes #root the containing block for the AppShell's
position:fixed header + footer, so they honor the 1280px floor too — spanning
the full 1280 and scrolling horizontally with the content — instead of staying
pinned to a narrower viewport. Body-portalled overlays (modals, tooltips,
dropdowns) render outside #root, so they stay viewport-fixed as intended. */
#root {
min-width: 1280px;
transform: translateZ(0);
}

/* ── Animations ──────────────────────────────────────────── */

@keyframes inspector-pulse {
Expand Down
26 changes: 22 additions & 4 deletions clients/web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,6 @@ import { useServerListWritable } from "@inspector/core/react/useServerListWritab
import { useInspectorVersion } from "@inspector/core/react/useInspectorVersion.js";
import { usePendingClientRequests } from "@inspector/core/react/usePendingClientRequests.js";
import { InspectorView } from "./components/views/InspectorView/InspectorView";
import { VersionBadge } from "./components/elements/VersionBadge/VersionBadge";
import { CopyrightBadge } from "./components/elements/CopyrightBadge/CopyrightBadge";
import type {
ToolCallState,
ToolsUiState,
Expand Down Expand Up @@ -654,6 +652,14 @@ function App() {
undefined,
);

// Id of the server that just connected successfully (#1682) — the success
// mirror of `failedServerId`. Drives the green highlight + scroll-into-view on
// that ServerCard. Set on the →connected transition, cleared when a new
// connection attempt starts or the session disconnects.
const [connectedServerId, setConnectedServerId] = useState<
string | undefined
>(undefined);

// InspectorClient + per-primitive state managers. All recreated together
// whenever the user switches active servers, then destroyed when the
// next switch happens (or when the component unmounts).
Expand Down Expand Up @@ -993,6 +999,18 @@ function App() {
}
}, [connectionStatus]);

// Track the just-connected server so its card gets the green highlight +
// scroll-into-view (#1682). Unlike `failedServerId` (which must survive the
// `disconnect` event a failed connect fires), "connected" is a stable status,
// so a status-driven effect can both set and clear it: set on connect, clear
// whenever the session isn't connected (disconnect, a new attempt's
// "connecting", or an error).
useEffect(() => {
setConnectedServerId(
connectionStatus === "connected" ? activeServerId : undefined,
);
}, [connectionStatus, activeServerId]);

// Disconnect the previous InspectorClient when it's replaced (server
// switch) or when App unmounts (HMR, tests). Without this the prior
// session's transport — a spawned stdio subprocess, an SSE stream, or
Expand Down Expand Up @@ -3821,6 +3839,8 @@ function App() {
serverListWritable={serverListWritable}
activeServer={activeServerId}
erroredServerId={failedServerId}
connectedServerId={connectedServerId}
version={inspectorVersion}
connectionStatus={connectionStatus}
connectErrorMessage={connectErrorMessage}
initializeResult={initializeResult}
Expand Down Expand Up @@ -3965,8 +3985,6 @@ function App() {
onRefreshApps={onRefreshTools}
/>
</Box>
<CopyrightBadge />
<VersionBadge version={inspectorVersion} />
<ServerConfigModal
opened={configModal !== null}
mode={configModal?.mode ?? "add"}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ import { Text } from "@mantine/core";
export const COPYRIGHT_NOTICE =
"Copyright © Model Context Protocol a Series of LF Projects, LLC.";

// The `copyrightBadge` variant (src/theme/Text.ts) pins it to the lower-right
// corner in grey, on the same row as the version badge, and non-interactive.
// The `copyrightBadge` variant (src/theme/Text.ts) styles it as a small grey
// single-line label; the footer row positions it at the right (#1682).
const CopyrightText = Text.withProps({ variant: "copyrightBadge" });

/**
* The project copyright notice, fixed to the lower-right corner of the screen
* (#1639) — the grey, non-interactive twin of the lower-left version badge,
* sharing its row.
* The project copyright notice, shown at the right of the footer row (#1682,
* superseding the fixed lower-right badge of #1639) — the grey twin of the
* left-aligned version badge, sharing the footer.
*/
export function CopyrightBadge() {
return <CopyrightText>{COPYRIGHT_NOTICE}</CopyrightText>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { ScrollArea, Stack } from "@mantine/core";
// Full-size, the monitor stream panels bound their scroll to the viewport
// minus the header and the panel's own chrome.
const FULLSIZE_MAH =
"calc(100vh - var(--app-shell-header-height, 0px) - 150px)";
"calc(100vh - var(--app-shell-header-height, 0px) - var(--app-shell-footer-height, 0px) - 150px)";

export interface EmbeddableScrollAreaProps {
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,17 @@ export const Disconnected: Story = {
},
};

// A failed connection settles the status back to "disconnected", but the
// `failed` flag surfaces it as a red "Failed" instead of the grey
// "Disconnected" (#1682).
export const Failed: Story = {
args: {
status: "disconnected",
failed: true,
showLabel: true,
},
};

export const Error: Story = {
args: {
status: "error",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,27 @@ describe("ServerStatusIndicator", () => {
expect(screen.getByText("Error")).toBeInTheDocument();
});

it("renders 'Failed' with the error color when failed, overriding the disconnected status (#1682)", () => {
const { container } = renderWithMantine(
<ServerStatusIndicator status="disconnected" failed showLabel />,
);
expect(screen.getByText("Failed")).toBeInTheDocument();
expect(screen.queryByText("Disconnected")).not.toBeInTheDocument();
// The dot uses the error color, not the disconnected grey.
const dot = container.querySelector(".mantine-Paper-root");
expect(dot?.getAttribute("style") ?? "").toContain(
"inspector-status-error",
);
});

it("does not show 'Failed' when the failed flag is unset (a deliberate disconnect)", () => {
renderWithMantine(
<ServerStatusIndicator status="disconnected" showLabel />,
);
expect(screen.getByText("Disconnected")).toBeInTheDocument();
expect(screen.queryByText("Failed")).not.toBeInTheDocument();
});

it("hides label and uses title when showLabel is false", () => {
renderWithMantine(
<ServerStatusIndicator
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ export interface ServerStatusIndicatorProps {
status: ConnectionStatus;
latencyMs?: number;
retryCount?: number;
/**
* True when the server's last connection attempt failed (#1682). The
* indicator then shows a red dot + "Failed" instead of the settled
* grey "Disconnected", so a failed connect reads distinctly from a
* deliberate disconnect. Overrides `status` for the label and color.
*/
failed?: boolean;
// Override for the default viewport-based label visibility. Useful when
// the indicator renders inside a constrained container (sidebar, card)
// where the global media query doesn't reflect available space.
Expand Down Expand Up @@ -46,16 +53,30 @@ export function ServerStatusIndicator({
status,
latencyMs,
retryCount,
failed = false,
showLabel: showLabelProp,
}: ServerStatusIndicatorProps) {
const wideViewport = useMediaQuery("(min-width: 1200px)");
// Drop the text label below 1500px. "Connected (Nms)" is the header's widest
// optional element, so shedding it first — earlier than the Disconnect control,
// which collapses at the 1280px floor (see ViewHeader) — keeps the tab row from
// crowding. Below the breakpoint the indicator is a dot only, with the label
// moved to its `title` tooltip.
const wideViewport = useMediaQuery("(min-width: 1500px)");
const showLabel = showLabelProp ?? wideViewport;
const label = getLabel(status, latencyMs, retryCount);
// A failed connection settles the status back to "disconnected"; the `failed`
// flag distinguishes it as a failure (red + "Failed") from a deliberate
// disconnect (grey), overriding the status-derived label and color.
const label = failed ? "Failed" : getLabel(status, latencyMs, retryCount);
const color = failed
? "var(--inspector-status-error)"
: statusColorVar[status];
return (
<Group gap="xs">
<Dot
bg={statusColorVar[status]}
className={status === "connecting" ? "inspector-pulse" : undefined}
bg={color}
className={
!failed && status === "connecting" ? "inspector-pulse" : undefined
}
title={showLabel ? undefined : label}
/>
{showLabel && <Text size="sm">{label}</Text>}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@ export interface VersionBadgeProps {
version?: string;
}

// The `versionBadge` variant (src/theme/Text.ts) pins it to the lower-left
// corner in grey and makes it non-interactive.
// The `versionBadge` variant (src/theme/Text.ts) styles it as a small grey
// single-line label; the footer row positions it at the left (#1682).
const VersionText = Text.withProps({ variant: "versionBadge" });

/**
* A small, unobtrusive build-version label fixed to the lower-left 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).
* A small, unobtrusive build-version label shown at the left of the footer row
* (#1682, superseding the fixed lower-left badge of #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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export interface AppControlsProps {
}

const LIST_MAX_HEIGHT =
"calc(100vh - var(--app-shell-header-height, 0px) - var(--mantine-spacing-xl) * 2 - 220px)";
"calc(100vh - var(--app-shell-header-height, 0px) - var(--app-shell-footer-height, 0px) - var(--mantine-spacing-xl) * 2 - 220px)";

const EmptyState = Text.withProps({
c: "dimmed",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,14 @@ export interface MonitoringControlsProps {
// The controls row at the top of the pinned monitoring sidebar: a segmented tab
// switcher on the left and a search box filling the rest. The column is closed
// from the single header MonitoringToggle (#1661), so there is no close button
// here.
// here. `px: xl` matches the `xl` horizontal inset the embedded screen below
// gives its panel (its `ScreenLayout` padding), so the tabs + search span the
// same width as the content and line up with its left/right edges.
const ControlsBar = Group.withProps({
wrap: "nowrap",
gap: "sm",
p: "sm",
px: "xl",
py: "sm",
});

// Search box, styled to match the `*Controls` search fields; `flex:1`/`miw:0`
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { ReactNode } from "react";
import { Divider, Stack } from "@mantine/core";
import { Stack } from "@mantine/core";
import { MonitoringControls } from "../MonitoringControls/MonitoringControls";
import { ScreenStage } from "../../elements/ScreenStage/ScreenStage";

Expand All @@ -22,9 +22,12 @@ export interface MonitoringScreenProps {

// Fills the pinned column: a fixed controls row on top and the selected screen
// filling the remaining height below (mih:0 lets the inner ScrollArea bound).
// `pt: md` gives the controls a little breathing room below the header instead
// of sitting flush against it.
const ColumnLayout = Stack.withProps({
h: "100%",
gap: 0,
pt: "md",
});

// Relative-positioned host so each tab's `ScreenStage` (absolutely positioned)
Expand Down Expand Up @@ -59,7 +62,6 @@ export function MonitoringScreen({
searchValue={searchValue}
onSearchChange={onSearchChange}
/>
<Divider />
<ScreenSlot>
{tabs.map((tab) => (
<ScreenStage key={tab} active={tab === value} fill>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
import type { FetchRequestEntry } from "@inspector/core/mcp/types.js";
import { isLongLivedStreamResponse } from "@inspector/core/mcp/fetchTracking.js";
import { ContentViewer } from "../../elements/ContentViewer/ContentViewer";
import { CopyButton } from "../../elements/CopyButton/CopyButton";
import { ExpandToggle } from "../../elements/ExpandToggle/ExpandToggle";
import { MethodBadge } from "../../elements/MethodBadge/MethodBadge";
import { CategoryBadge } from "../../elements/CategoryBadge/CategoryBadge";
Expand Down Expand Up @@ -281,6 +282,7 @@ export function NetworkEntry({
<ControlsCluster>{metaBadges}</ControlsCluster>
</HeaderRow>
<Group gap="xs" wrap="nowrap" justify="space-between">
<CopyButton value={entry.url} />
<ScrollArea
scrollbarSize={6}
flex={1}
Expand All @@ -304,6 +306,7 @@ export function NetworkEntry({
</TimestampText>
<MethodBadge method={entry.method} />
<CategoryBadge category={entry.category} />
<CopyButton value={entry.url} />
<UrlText>{entry.url}</UrlText>
</Group>
<Group gap="sm" wrap="nowrap">
Expand Down
Loading
Loading