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
Original file line number Diff line number Diff line change
Expand Up @@ -920,6 +920,76 @@ describe("InspectorView", () => {
).toBeInTheDocument();
});

it("dims the other server cards while a connection is live", () => {
const betaServer: ServerEntry = {
id: "beta",
name: "Beta",
config: { type: "stdio", command: "echo" },
connection: { status: "disconnected" },
};
renderWithMantine(
<InspectorView
{...makeProps({
servers: [sampleServer, betaServer],
activeServer: "alpha",
connectionStatus: "connected",
initializeResult: connectedInit,
})}
/>,
);
// The non-active card is inert while alpha holds a live session, so the
// user can't start a second connection mid-session.
const betaCard = screen.getByText("Beta").closest(".mantine-Card-root");
expect(betaCard?.getAttribute("aria-disabled")).toBe("true");
});

it("re-enables the other server cards when the active connection goes to error (#1521)", () => {
const betaServer: ServerEntry = {
id: "beta",
name: "Beta",
config: { type: "stdio", command: "echo" },
connection: { status: "disconnected" },
};
// Live session on alpha → beta starts out dimmed/inert.
const { rerender } = renderWithMantine(
<InspectorView
{...makeProps({
servers: [sampleServer, betaServer],
activeServer: "alpha",
connectionStatus: "connected",
initializeResult: connectedInit,
})}
/>,
);
expect(
screen
.getByText("Beta")
.closest(".mantine-Card-root")
?.getAttribute("aria-disabled"),
).toBe("true");

// alpha's connection errors. App does NOT clear `activeServer` here — a
// terminal `error` fires no InspectorClient `disconnect` event — so the
// id still points at alpha. The other cards must re-enable anyway; only a
// *live* session should dim them.
rerender(
<InspectorView
{...makeProps({
servers: [sampleServer, betaServer],
activeServer: "alpha",
connectionStatus: "error",
initializeResult: undefined,
})}
/>,
);
expect(
screen
.getByText("Beta")
.closest(".mantine-Card-root")
?.getAttribute("aria-disabled"),
).toBeNull();
});

describe("listChanged indicator wiring (#1402)", () => {
// The indicator only mounts on the active screen, so each case connects,
// navigates to the target tab, and asserts the "List updated" affordance.
Expand Down
21 changes: 20 additions & 1 deletion clients/web/src/components/views/InspectorView/InspectorView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import type {
MessageEntry,
ServerEntry,
} from "@inspector/core/mcp/types.js";
import { isTerminalStatus } from "@inspector/core/mcp/types.js";
import { isAppTool } from "@inspector/core/mcp/apps.js";
import { ViewHeader } from "../../groups/ViewHeader/ViewHeader";
import { ServerListScreen } from "../../screens/ServerListScreen/ServerListScreen";
Expand Down Expand Up @@ -576,6 +577,24 @@ export function InspectorView({
[serversInput, activeServer, connectionStatus, initializeResult],
);

// The other server cards are dimmed/`inert` only while a connection is
// actively live (connecting or connected), so the user can't kick off a
// second connection mid-session. Once the active session settles into a
// terminal state — `disconnected` or `error` — the rest must re-enable
// (#1521). A connect-time handshake failure (and a mid-session transport
// `onerror`) resolves to `error` *without* firing the InspectorClient
// `disconnect` event that App uses to clear `activeServerId`, so the id
// lingers; gating the dim source on liveness here is what un-dims the
// others. The errored card itself still shows its real status via the
// merged `servers` list above (keyed off the real `activeServer`), so
// passing `undefined` here lifts only the *other* cards' dimming, not the
// error indicator on the active one. `isTerminalStatus` (the #1490 teardown
// convention) is the single source of truth for "session is over" so this
// gate can't silently desync from a future status addition.
const dimCardsAgainst = isTerminalStatus(connectionStatus)
? undefined
: activeServer;

return (
// padding={0}: each screen fills `calc(100dvh - header)` and supplies its
// own `xl` padding, so Main must contribute only the fixed-header offset.
Expand Down Expand Up @@ -612,7 +631,7 @@ export function InspectorView({
<ServerListScreen
servers={servers}
writable={serverListWritable}
activeServer={activeServer}
activeServer={dimCardsAgainst}
onAddManually={onServerAdd}
onImportConfig={onServerImportConfig}
onImportServerJson={onServerImportJson}
Expand Down
Loading