Merge pull request #2182 from modelcontextprotocol/v2/chore/2130-inspectorview-prop-bundles
chore(web): collapse InspectorView's prop wall into domain bundles
This commit is contained in:
commit
18fe35478b
6 changed files with 2496 additions and 1489 deletions
|
|
@ -450,122 +450,67 @@ vi.mock("./lib/publishAppDocument", () => ({
|
|||
// that invoke the App's connect / call-tool / get-prompt / read-resource /
|
||||
// set-log-level handlers.
|
||||
vi.mock("./components/views/InspectorView/InspectorView", () => ({
|
||||
InspectorView: (props: {
|
||||
toolCallState?: { status?: string };
|
||||
toolsUi?: {
|
||||
selectedToolKey?: string;
|
||||
formValues: Record<string, unknown>;
|
||||
search: string;
|
||||
};
|
||||
promptsUi?: {
|
||||
selectedPromptName?: string;
|
||||
argumentValues: Record<string, string>;
|
||||
submittedFor?: string;
|
||||
search: string;
|
||||
};
|
||||
logsUi?: { filterText: string; visibleLevels: Record<string, boolean> };
|
||||
getPromptState?: { status?: string };
|
||||
readResourceState?: { status?: string };
|
||||
currentLogLevel?: string;
|
||||
activeTab?: string;
|
||||
activeServer?: string;
|
||||
erroredServerId?: string;
|
||||
initializeResult?: { serverInfo: { name: string; version: string } };
|
||||
onActiveTabChange: (tab: string) => void;
|
||||
onConnectionInfo: () => void;
|
||||
onToggleConnection: (id: string) => void;
|
||||
onToolsUiChange: (next: {
|
||||
selectedToolKey?: string;
|
||||
formValues: Record<string, unknown>;
|
||||
search: string;
|
||||
}) => void;
|
||||
onPromptsUiChange: (next: {
|
||||
selectedPromptName?: string;
|
||||
argumentValues: Record<string, string>;
|
||||
submittedFor?: string;
|
||||
search: string;
|
||||
}) => void;
|
||||
onLogsUiChange: (next: {
|
||||
filterText: string;
|
||||
visibleLevels: Record<string, boolean>;
|
||||
}) => void;
|
||||
progressByTaskId?: Record<string, unknown>;
|
||||
onCallTool: (
|
||||
name: string,
|
||||
args: Record<string, unknown>,
|
||||
runAsTask?: boolean,
|
||||
) => void;
|
||||
onGetPrompt: (name: string, args: Record<string, string>) => void;
|
||||
onReadResource: (uri: string) => void;
|
||||
onSetLogLevel: (level: string) => void;
|
||||
onCancelTask: (taskId: string) => void;
|
||||
onCancelToolCall: () => void;
|
||||
onClearCompletedTasks: () => void;
|
||||
onRefreshTasks: () => void;
|
||||
onServerSettings: (id: string) => void;
|
||||
onServerEdit: (id: string) => void;
|
||||
onServerAdd: () => void;
|
||||
highlightedServerIds?: string[];
|
||||
onClearProtocol: () => void;
|
||||
onReplayProtocol: (id: string) => void;
|
||||
onTogglePinProtocol: (id: string) => void;
|
||||
pinnedProtocolIds?: Set<string>;
|
||||
onRefreshTools: () => void;
|
||||
toolsPagination: {
|
||||
paginated: boolean;
|
||||
canLoadMore: boolean;
|
||||
loadedPages: number;
|
||||
onPaginatedChange: (v: boolean) => void;
|
||||
onLoadMore: () => void;
|
||||
};
|
||||
}) => (
|
||||
InspectorView: (props: InspectorViewProps) => (
|
||||
<div>
|
||||
<span data-testid="tool-status">
|
||||
{props.toolCallState?.status ?? "none"}
|
||||
{props.tools.toolCallState?.status ?? "none"}
|
||||
</span>
|
||||
<span data-testid="task-progress-keys">
|
||||
{Object.keys(props.progressByTaskId ?? {}).join(",") || "none"}
|
||||
{Object.keys(props.tasks.progressByTaskId ?? {}).join(",") || "none"}
|
||||
</span>
|
||||
<span data-testid="selected-tool">
|
||||
{props.toolsUi?.selectedToolKey ?? "none"}
|
||||
{props.tools.toolsUi?.selectedToolKey ?? "none"}
|
||||
</span>
|
||||
<span data-testid="tool-search">
|
||||
{props.tools.toolsUi?.search || "none"}
|
||||
</span>
|
||||
<span data-testid="tool-search">{props.toolsUi?.search || "none"}</span>
|
||||
<span data-testid="selected-prompt">
|
||||
{props.promptsUi?.selectedPromptName ?? "none"}
|
||||
{props.prompts.promptsUi?.selectedPromptName ?? "none"}
|
||||
</span>
|
||||
<span data-testid="log-filter">
|
||||
{props.logs.logsUi?.filterText || "none"}
|
||||
</span>
|
||||
<span data-testid="log-filter">{props.logsUi?.filterText || "none"}</span>
|
||||
<span data-testid="prompt-status">
|
||||
{props.getPromptState?.status ?? "none"}
|
||||
{props.prompts.getPromptState?.status ?? "none"}
|
||||
</span>
|
||||
<span data-testid="resource-status">
|
||||
{props.readResourceState?.status ?? "none"}
|
||||
{props.resources.readResourceState?.status ?? "none"}
|
||||
</span>
|
||||
<span data-testid="log-level">{props.currentLogLevel}</span>
|
||||
<span data-testid="active-tab">{props.activeTab ?? "none"}</span>
|
||||
<span data-testid="log-level">{props.logs.currentLogLevel}</span>
|
||||
<span data-testid="active-tab">{props.shell.activeTab ?? "none"}</span>
|
||||
<span data-testid="init-result">
|
||||
{props.initializeResult
|
||||
? `name:${props.initializeResult.serverInfo.name || "(empty)"}`
|
||||
{props.connection.initializeResult
|
||||
? `name:${props.connection.initializeResult.serverInfo.name || "(empty)"}`
|
||||
: "none"}
|
||||
</span>
|
||||
<span data-testid="errored-server">
|
||||
{props.erroredServerId ?? "none"}
|
||||
{props.connection.erroredServerId ?? "none"}
|
||||
</span>
|
||||
<span data-testid="active-server">{props.activeServer ?? "none"}</span>
|
||||
<button onClick={() => props.onActiveTabChange("Servers")}>
|
||||
<span data-testid="active-server">
|
||||
{props.connection.activeServer ?? "none"}
|
||||
</span>
|
||||
<button onClick={() => props.shell.onActiveTabChange("Servers")}>
|
||||
switch-servers-tab
|
||||
</button>
|
||||
<button onClick={() => props.onToggleConnection("A")}>connect</button>
|
||||
<button onClick={() => props.connection.onToggleConnection("A")}>
|
||||
connect
|
||||
</button>
|
||||
{/* A second target, so a test can drive an A -> B -> A switch (#2095). */}
|
||||
<button onClick={() => props.onToggleConnection("B")}>connect-b</button>
|
||||
<button onClick={() => props.onConnectionInfo()}>
|
||||
<button onClick={() => props.connection.onToggleConnection("B")}>
|
||||
connect-b
|
||||
</button>
|
||||
{/* The header's explicit Disconnect, which routes to the standalone
|
||||
`onDisconnect` rather than through the toggle. */}
|
||||
<button onClick={() => props.connection.onDisconnect()}>
|
||||
disconnect
|
||||
</button>
|
||||
<button onClick={() => props.servers.onConnectionInfo("A")}>
|
||||
open-connection-info
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
props.onToolsUiChange({
|
||||
formValues: {},
|
||||
search: "",
|
||||
...props.toolsUi,
|
||||
props.tools.onToolsUiChange({
|
||||
...props.tools.toolsUi,
|
||||
selectedToolKey: "0:get_acts",
|
||||
})
|
||||
}
|
||||
|
|
@ -574,10 +519,8 @@ vi.mock("./components/views/InspectorView/InspectorView", () => ({
|
|||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
props.onToolsUiChange({
|
||||
formValues: {},
|
||||
search: "",
|
||||
...props.toolsUi,
|
||||
props.tools.onToolsUiChange({
|
||||
...props.tools.toolsUi,
|
||||
selectedToolKey: "1:other_tool",
|
||||
})
|
||||
}
|
||||
|
|
@ -586,9 +529,8 @@ vi.mock("./components/views/InspectorView/InspectorView", () => ({
|
|||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
props.onToolsUiChange({
|
||||
formValues: {},
|
||||
...props.toolsUi,
|
||||
props.tools.onToolsUiChange({
|
||||
...props.tools.toolsUi,
|
||||
search: "act",
|
||||
})
|
||||
}
|
||||
|
|
@ -597,10 +539,8 @@ vi.mock("./components/views/InspectorView/InspectorView", () => ({
|
|||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
props.onPromptsUiChange({
|
||||
argumentValues: {},
|
||||
search: "",
|
||||
...props.promptsUi,
|
||||
props.prompts.onPromptsUiChange({
|
||||
...props.prompts.promptsUi,
|
||||
selectedPromptName: "greet",
|
||||
})
|
||||
}
|
||||
|
|
@ -609,70 +549,99 @@ vi.mock("./components/views/InspectorView/InspectorView", () => ({
|
|||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
props.onLogsUiChange({
|
||||
visibleLevels: {},
|
||||
...props.logsUi,
|
||||
props.logs.onLogsUiChange({
|
||||
...props.logs.logsUi,
|
||||
filterText: "err",
|
||||
})
|
||||
}
|
||||
>
|
||||
set-log-filter
|
||||
</button>
|
||||
<button onClick={() => props.onCallTool("get_acts", {})}>call</button>
|
||||
<button onClick={() => props.onCallTool("get_acts", {}, true)}>
|
||||
<button onClick={() => props.tools.onCallTool("get_acts", {})}>
|
||||
call
|
||||
</button>
|
||||
<button onClick={() => props.tools.onCallTool("get_acts", {}, true)}>
|
||||
call-as-task
|
||||
</button>
|
||||
<button onClick={() => props.onCancelTask("task-1")}>cancel-task</button>
|
||||
<button onClick={() => props.onCancelToolCall()}>cancel-tool-call</button>
|
||||
<button onClick={() => props.onClearCompletedTasks()}>
|
||||
<button onClick={() => props.tasks.onCancelTask("task-1")}>
|
||||
cancel-task
|
||||
</button>
|
||||
<button onClick={() => props.tools.onCancelToolCall?.()}>
|
||||
cancel-tool-call
|
||||
</button>
|
||||
<button onClick={() => props.tasks.onClearCompletedTasks()}>
|
||||
clear-completed
|
||||
</button>
|
||||
<button onClick={() => props.onRefreshTasks()}>refresh-tasks</button>
|
||||
<button onClick={() => props.onGetPrompt("greet", {})}>get-prompt</button>
|
||||
<button onClick={() => props.onReadResource("res://x")}>
|
||||
<button onClick={() => props.tasks.onRefreshTasks()}>
|
||||
refresh-tasks
|
||||
</button>
|
||||
<button onClick={() => props.prompts.onGetPrompt("greet", {})}>
|
||||
get-prompt
|
||||
</button>
|
||||
<button onClick={() => props.resources.onReadResource("res://x")}>
|
||||
read-resource
|
||||
</button>
|
||||
<button onClick={() => props.onSetLogLevel("debug")}>set-level</button>
|
||||
<button onClick={() => props.onServerSettings("A")}>open-settings</button>
|
||||
<button onClick={() => props.logs.onSetLogLevel("debug")}>
|
||||
set-level
|
||||
</button>
|
||||
<button onClick={() => props.servers.onServerSettings("A")}>
|
||||
open-settings
|
||||
</button>
|
||||
{/* The real server grid (and its Add / Edit controls) lives inside this
|
||||
mocked view, so the config modal is only reachable through these
|
||||
callbacks — and the highlight batch only observable through this prop. */}
|
||||
<button onClick={() => props.onServerEdit("A")}>edit-server</button>
|
||||
<button onClick={() => props.onServerAdd()}>add-server</button>
|
||||
<button onClick={() => props.servers.onServerEdit("A")}>
|
||||
edit-server
|
||||
</button>
|
||||
<button onClick={() => props.servers.onServerAdd()}>add-server</button>
|
||||
{/* The real drag-and-drop reorder lives inside this mocked view, so the
|
||||
callback is only reachable through a control like this one. */}
|
||||
<button onClick={() => props.servers.onServerReorder(["B", "A"])}>
|
||||
reorder-servers
|
||||
</button>
|
||||
<span data-testid="highlighted-servers">
|
||||
{(props.highlightedServerIds ?? []).join(",") || "none"}
|
||||
{(props.servers.highlightedServerIds ?? []).join(",") || "none"}
|
||||
</span>
|
||||
<span data-testid="pinned-history">
|
||||
{Array.from(props.pinnedProtocolIds ?? []).join(",")}
|
||||
{Array.from(props.protocol.pinnedProtocolIds ?? []).join(",")}
|
||||
</span>
|
||||
<button onClick={() => props.onTogglePinProtocol("hist-1")}>
|
||||
<button onClick={() => props.protocol.onTogglePinProtocol("hist-1")}>
|
||||
toggle-pin
|
||||
</button>
|
||||
<button onClick={() => props.onReplayProtocol("hist-1")}>
|
||||
<button onClick={() => props.protocol.onReplayProtocol("hist-1")}>
|
||||
replay-history
|
||||
</button>
|
||||
<button onClick={() => props.onClearProtocol()}>clear-history</button>
|
||||
<button onClick={() => props.protocol.onClearProtocol()}>
|
||||
clear-history
|
||||
</button>
|
||||
<span data-testid="tools-paginated">
|
||||
{String(props.toolsPagination.paginated)}
|
||||
{String(props.tools.toolsPagination.paginated)}
|
||||
</span>
|
||||
<span data-testid="tools-loaded-pages">
|
||||
{props.toolsPagination.loadedPages}
|
||||
{props.tools.toolsPagination.loadedPages}
|
||||
</span>
|
||||
<button onClick={() => props.toolsPagination.onPaginatedChange(true)}>
|
||||
<button
|
||||
onClick={() => props.tools.toolsPagination.onPaginatedChange(true)}
|
||||
>
|
||||
paginated-on
|
||||
</button>
|
||||
<button onClick={() => props.toolsPagination.onPaginatedChange(false)}>
|
||||
<button
|
||||
onClick={() => props.tools.toolsPagination.onPaginatedChange(false)}
|
||||
>
|
||||
paginated-off
|
||||
</button>
|
||||
<button onClick={() => props.toolsPagination.onLoadMore()}>
|
||||
<button onClick={() => props.tools.toolsPagination.onLoadMore()}>
|
||||
load-more-tools
|
||||
</button>
|
||||
<button onClick={() => props.onRefreshTools()}>refresh-tools</button>
|
||||
<button onClick={() => props.tools.onRefreshTools()}>
|
||||
refresh-tools
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
import App from "./App";
|
||||
import type { InspectorViewProps } from "./components/views/InspectorView/InspectorView";
|
||||
import { SERVER_INFO_NOT_REPORTED_LABEL } from "./components/groups/ConnectionInfoContent/ConnectionInfoContent";
|
||||
import { OAUTH_CALLBACK_PATH } from "./utils/oauthFlow.js";
|
||||
import { INSPECTOR_SERVERS_TAB } from "./utils/inspectorTabs.js";
|
||||
|
|
@ -3842,6 +3811,30 @@ describe("App background command rejections (#2049)", () => {
|
|||
return user;
|
||||
};
|
||||
|
||||
// A complete `useServers` return with one injected mutator, so the mock stays
|
||||
// type-checked against the hook's contract rather than spread from a call.
|
||||
const serversWithReorder = (
|
||||
reorderServers: ReturnType<typeof useServers>["reorderServers"],
|
||||
): ReturnType<typeof useServers> => ({
|
||||
servers: [
|
||||
{
|
||||
id: "A",
|
||||
name: "PlotRocket",
|
||||
config: { type: "streamable-http", url: "https://api.example.com/mcp" },
|
||||
connection: { status: "disconnected" },
|
||||
},
|
||||
],
|
||||
loading: false,
|
||||
error: undefined,
|
||||
refresh: vi.fn().mockResolvedValue(undefined),
|
||||
addServer: vi.fn().mockResolvedValue(undefined),
|
||||
updateServer: vi.fn().mockResolvedValue(undefined),
|
||||
updateServerSettings: vi.fn().mockResolvedValue(undefined),
|
||||
removeServer: vi.fn().mockResolvedValue(undefined),
|
||||
reorderServers,
|
||||
importSource: vi.fn().mockResolvedValue({ servers: {} }),
|
||||
});
|
||||
|
||||
it("does not leak an unhandled rejection when an all-pages Refresh fails", async () => {
|
||||
vi.mocked(useManagedTools).mockReturnValue({
|
||||
tools: [],
|
||||
|
|
@ -3919,6 +3912,156 @@ describe("App background command rejections (#2049)", () => {
|
|||
rejections.stop();
|
||||
}
|
||||
});
|
||||
|
||||
// `onToggleConnection` and `onDisconnect` both close a live session with
|
||||
// `try { await disconnect() } finally { finalizeExplicitDisconnect() }` — a
|
||||
// `finally`, not a `catch` — so a transport that fails to close rejects out
|
||||
// of the handler. App discards neither: both are terminated with a `.catch`
|
||||
// that toasts, because a bare `void` would turn an ordinary click into a
|
||||
// global unhandled rejection with nothing shown to the user (#2130 review).
|
||||
const CLOSE_FAILURE = new Error("close boom");
|
||||
|
||||
it("toasts a failed transport close from the connection toggle instead of leaking it", async () => {
|
||||
const rejections = captureUnhandledRejections();
|
||||
try {
|
||||
const user = await connect();
|
||||
const client = clientInstances[0] as EventTarget & {
|
||||
disconnect: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
client.disconnect.mockRejectedValueOnce(CLOSE_FAILURE);
|
||||
|
||||
// Second click on the same, now-connected server takes the disconnect
|
||||
// branch of the toggle.
|
||||
await user.click(screen.getByText("connect"));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(notificationsMock.show).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
title: "Failed to change the connection",
|
||||
message: "close boom",
|
||||
color: "red",
|
||||
}),
|
||||
),
|
||||
);
|
||||
await settleRejections();
|
||||
expect(rejections.seen).toEqual([]);
|
||||
} finally {
|
||||
rejections.stop();
|
||||
}
|
||||
});
|
||||
|
||||
// The four command handlers await `handleCommandScopedAuthRecovery` from
|
||||
// *inside* their catch blocks, and a rejection thrown from a catch is not
|
||||
// caught by that same catch — so it escapes the handler entirely. That helper
|
||||
// awaits `checkAuthChallengeSatisfied`, which reaches the backend and can
|
||||
// reject. Before #2130's review this escaped a bare `void` as a global
|
||||
// unhandled rejection with nothing shown to the user.
|
||||
it("toasts an auth-recovery failure that escapes a tool call instead of leaking it", async () => {
|
||||
const rejections = captureUnhandledRejections();
|
||||
try {
|
||||
const user = await connect();
|
||||
const client = clientInstances[0] as EventTarget & {
|
||||
callTool: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
client.callTool.mockRejectedValueOnce(
|
||||
new AuthRecoveryRequiredError(
|
||||
new URL("https://as.example.com/authorize"),
|
||||
{ reason: "unauthorized" },
|
||||
),
|
||||
);
|
||||
// The recovery helper's first await is the one that breaks.
|
||||
rejectNextChallengeCheck(new Error("challenge check failed"));
|
||||
|
||||
await user.click(screen.getByText("call"));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(notificationsMock.show).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
title: "Tool call failed",
|
||||
message: "challenge check failed",
|
||||
color: "red",
|
||||
}),
|
||||
),
|
||||
);
|
||||
await settleRejections();
|
||||
expect(rejections.seen).toEqual([]);
|
||||
} finally {
|
||||
rejections.stop();
|
||||
}
|
||||
});
|
||||
|
||||
// The reorder handler was lifted out of the JSX into a named callback with a
|
||||
// `.catch` that toasts (#2130). `reorderServers` reverts its own optimistic
|
||||
// ordering and re-throws — on a 409 from a racing external edit, or a network
|
||||
// error — so without the toast the drag just silently bounces back.
|
||||
it("forwards a reorder to the server list", async () => {
|
||||
const reorderServers = vi.fn().mockResolvedValue(undefined);
|
||||
vi.mocked(useServers).mockReturnValue(serversWithReorder(reorderServers));
|
||||
const user = userEvent.setup();
|
||||
renderWithMantine(<App />);
|
||||
|
||||
await user.click(screen.getByText("reorder-servers"));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(reorderServers).toHaveBeenCalledWith(["B", "A"]),
|
||||
);
|
||||
expect(notificationsMock.show).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("toasts a failed reorder instead of leaking it", async () => {
|
||||
const reorderServers = vi
|
||||
.fn()
|
||||
.mockRejectedValue(new Error("stale server list"));
|
||||
vi.mocked(useServers).mockReturnValue(serversWithReorder(reorderServers));
|
||||
const rejections = captureUnhandledRejections();
|
||||
try {
|
||||
const user = userEvent.setup();
|
||||
renderWithMantine(<App />);
|
||||
|
||||
await user.click(screen.getByText("reorder-servers"));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(notificationsMock.show).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
title: "Failed to reorder servers",
|
||||
message: "stale server list",
|
||||
color: "red",
|
||||
}),
|
||||
),
|
||||
);
|
||||
await settleRejections();
|
||||
expect(rejections.seen).toEqual([]);
|
||||
} finally {
|
||||
rejections.stop();
|
||||
}
|
||||
});
|
||||
|
||||
it("toasts a failed transport close from the explicit Disconnect instead of leaking it", async () => {
|
||||
const rejections = captureUnhandledRejections();
|
||||
try {
|
||||
const user = await connect();
|
||||
const client = clientInstances[0] as EventTarget & {
|
||||
disconnect: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
client.disconnect.mockRejectedValueOnce(CLOSE_FAILURE);
|
||||
|
||||
await user.click(screen.getByText("disconnect"));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(notificationsMock.show).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
title: "Failed to disconnect",
|
||||
message: "close boom",
|
||||
color: "red",
|
||||
}),
|
||||
),
|
||||
);
|
||||
await settleRejections();
|
||||
expect(rejections.seen).toEqual([]);
|
||||
} finally {
|
||||
rejections.stop();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/** A complete `useManagedResources` return, so the mock stays type-checked against the hook's contract. */
|
||||
|
|
|
|||
|
|
@ -64,6 +64,20 @@ import { useInitialConfig } from "@inspector/core/react/useInitialConfig.js";
|
|||
import { refreshingPersist } from "./lib/refreshingPersist";
|
||||
import { usePendingClientRequests } from "@inspector/core/react/usePendingClientRequests.js";
|
||||
import { InspectorView } from "./components/views/InspectorView/InspectorView";
|
||||
import type {
|
||||
AppsPanelProps,
|
||||
ConnectionProps,
|
||||
ConsolePanelProps,
|
||||
LogsPanelProps,
|
||||
NetworkPanelProps,
|
||||
PromptsPanelProps,
|
||||
ProtocolPanelProps,
|
||||
ResourcesPanelProps,
|
||||
ServerListProps,
|
||||
ShellProps,
|
||||
TasksPanelProps,
|
||||
ToolsPanelProps,
|
||||
} from "./components/views/InspectorView/types";
|
||||
import type { ReadResourceState } from "./components/screens/ResourcesScreen/ResourcesScreen";
|
||||
import { AppElicitationHost } from "./components/elements/AppElicitation/AppElicitationHost";
|
||||
import type { LogEntryData } from "./components/elements/LogEntry/LogEntry";
|
||||
|
|
@ -108,6 +122,21 @@ import { FetchBodyDroppedToastMessage } from "./components/elements/Toasts/Fetch
|
|||
import { OutputValidationToastMessage } from "./components/elements/Toasts/OutputValidationToastMessage";
|
||||
import { ReAuthBannerBar } from "./components/groups/ReAuthBanner/ReAuthBannerBar";
|
||||
|
||||
/**
|
||||
* Terminates a dispatched handler's promise by reporting the failure, for the
|
||||
* `InspectorView` action wrappers below. Module scope because it closes over
|
||||
* nothing — the title is the only thing that varies per action.
|
||||
*/
|
||||
function reportDispatchFailure(title: string) {
|
||||
return (err: unknown) => {
|
||||
notifications.show({
|
||||
title,
|
||||
message: err instanceof Error ? err.message : String(err),
|
||||
color: "red",
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function App() {
|
||||
const { onToggleTheme } = useThemeToggle();
|
||||
|
||||
|
|
@ -1469,6 +1498,304 @@ function App() {
|
|||
[pendingElicitations, inspectorClient],
|
||||
);
|
||||
|
||||
// --- InspectorView prop bundles (#2130) -----------------------------------
|
||||
//
|
||||
// The view takes one prop per domain, so the JSX below reads as a component
|
||||
// tree rather than a wall of ~130 props. Everything the bundles need is
|
||||
// assembled here: the handlers that used to be multi-line closures declared
|
||||
// inline in the JSX are named callbacks, and the `void`-discarding wrappers
|
||||
// carry their justification once rather than at a call site.
|
||||
|
||||
const onServerAdd = useCallback(() => {
|
||||
setHighlightedServerIds([]);
|
||||
setConfigModal({ mode: "add" });
|
||||
}, []);
|
||||
|
||||
const onServerImportConfig = useCallback(() => {
|
||||
setHighlightedServerIds([]);
|
||||
setImportConfigOpen(true);
|
||||
}, []);
|
||||
|
||||
const onServerImportJson = useCallback(() => {
|
||||
setHighlightedServerIds([]);
|
||||
setImportJsonOpen(true);
|
||||
}, []);
|
||||
|
||||
const onServerEdit = useCallback((id: string) => {
|
||||
setConfigModal({ mode: "edit", targetId: id });
|
||||
}, []);
|
||||
|
||||
const onServerClone = useCallback((id: string) => {
|
||||
setHighlightedServerIds([]);
|
||||
setConfigModal({ mode: "clone", targetId: id });
|
||||
}, []);
|
||||
|
||||
const onServerRemove = useCallback(
|
||||
(id: string) => {
|
||||
const target = servers.find((s) => s.id === id);
|
||||
if (target) setRemoveTarget(target);
|
||||
},
|
||||
[servers],
|
||||
);
|
||||
|
||||
const onServerReorderFromList = useCallback(
|
||||
(orderedIds: string[]) => {
|
||||
// reorderServers reverts the optimistic order via an internal refresh()
|
||||
// and re-throws on failure (409 from a racing external edit, or a network
|
||||
// error). Surface that to the user so the drag doesn't silently bounce
|
||||
// back — matching the toast pattern every other mutation here uses.
|
||||
reorderServers(orderedIds).catch((err: unknown) => {
|
||||
notifications.show({
|
||||
title: "Failed to reorder servers",
|
||||
message: err instanceof Error ? err.message : String(err),
|
||||
color: "red",
|
||||
});
|
||||
});
|
||||
},
|
||||
[reorderServers],
|
||||
);
|
||||
|
||||
const openClientSettings = useCallback(() => setClientSettingsOpen(true), []);
|
||||
const openConnectionInfo = useCallback(
|
||||
() => setConnectionInfoModalOpen(true),
|
||||
[],
|
||||
);
|
||||
const openServerSettings = useCallback(
|
||||
(id: string) => setSettingsModalTargetId(id),
|
||||
[],
|
||||
);
|
||||
|
||||
// Every `InspectorView` action prop is synchronous while the handler behind
|
||||
// it is async, so each wrapper below has to terminate its promise somehow.
|
||||
// They all do it the same way: a reporting `.catch`, never a bare `void`.
|
||||
//
|
||||
// The uniformity is the point. Deciding per handler whether it "owns its
|
||||
// failures" is a judgement this PR got wrong twice (#2130 review) — the
|
||||
// handlers really do catch their ordinary errors, but a rejection can still
|
||||
// escape by a route the reading misses:
|
||||
//
|
||||
// - `onToggleConnection` awaits `initialConfigSettledRef` and constructs
|
||||
// the client via `setupClientForServer` outside its own try/catch, and
|
||||
// its already-connected branch is a `try/finally` that lets a failed
|
||||
// transport close propagate. `onDisconnect` is that `try/finally` alone.
|
||||
// - `onCallTool` / `onGetPrompt` / `onReadResource` / `onOpenApp` await
|
||||
// `handleCommandScopedAuthRecovery` from *inside* their catch blocks, and
|
||||
// a rejection thrown from a catch is not caught by that same catch. That
|
||||
// helper awaits `checkAuthChallengeSatisfied` and `pushRemoteAuthState`,
|
||||
// both of which reach the backend and can reject.
|
||||
//
|
||||
// A handler that does surface its own failure resolves, so it never reaches
|
||||
// this `.catch` and there is no double-toast; what lands here is only the
|
||||
// escape, which would otherwise be a global unhandled rejection with nothing
|
||||
// shown to the user.
|
||||
const dispatchToggleConnection = useCallback(
|
||||
(id: string) => {
|
||||
onToggleConnection(id).catch(
|
||||
reportDispatchFailure("Failed to change the connection"),
|
||||
);
|
||||
},
|
||||
[onToggleConnection],
|
||||
);
|
||||
const dispatchDisconnect = useCallback(() => {
|
||||
onDisconnect().catch(reportDispatchFailure("Failed to disconnect"));
|
||||
}, [onDisconnect]);
|
||||
const dispatchCallTool = useCallback(
|
||||
(name: string, args: Record<string, unknown>, runAsTask?: boolean) => {
|
||||
onCallTool(name, args, runAsTask).catch(
|
||||
reportDispatchFailure("Tool call failed"),
|
||||
);
|
||||
},
|
||||
[onCallTool],
|
||||
);
|
||||
const dispatchGetPrompt = useCallback(
|
||||
(name: string, args: Record<string, string>) => {
|
||||
onGetPrompt(name, args).catch(
|
||||
reportDispatchFailure("Failed to get prompt"),
|
||||
);
|
||||
},
|
||||
[onGetPrompt],
|
||||
);
|
||||
const dispatchReadResource = useCallback(
|
||||
(uri: string) => {
|
||||
onReadResource(uri).catch(
|
||||
reportDispatchFailure("Failed to read resource"),
|
||||
);
|
||||
},
|
||||
[onReadResource],
|
||||
);
|
||||
const dispatchCancelTask = useCallback(
|
||||
(taskId: string) => {
|
||||
onCancelTask(taskId).catch(
|
||||
reportDispatchFailure("Failed to cancel task"),
|
||||
);
|
||||
},
|
||||
[onCancelTask],
|
||||
);
|
||||
const dispatchOpenApp = useCallback(
|
||||
(name: string, args: Record<string, unknown>) => {
|
||||
onOpenApp(name, args).catch(reportDispatchFailure("Failed to open app"));
|
||||
},
|
||||
[onOpenApp],
|
||||
);
|
||||
|
||||
const shellProps: ShellProps = {
|
||||
deepLink,
|
||||
deepLinkStatus,
|
||||
version: inspectorVersion,
|
||||
malformedListItems: shownMalformedListItems,
|
||||
activeTab,
|
||||
onActiveTabChange: setActiveTab,
|
||||
onToggleTheme,
|
||||
onOpenClientSettings: openClientSettings,
|
||||
};
|
||||
|
||||
const connectionProps: ConnectionProps = {
|
||||
activeServer: activeServerId,
|
||||
erroredServerId: failedServerId,
|
||||
connectedServerId,
|
||||
connectionStatus,
|
||||
connectErrorMessage,
|
||||
initializeResult,
|
||||
latencyMs,
|
||||
protocolEra,
|
||||
onCompleteArgument,
|
||||
completionsSupported: capabilities?.completions !== undefined,
|
||||
onToggleConnection: dispatchToggleConnection,
|
||||
onDisconnect: dispatchDisconnect,
|
||||
};
|
||||
|
||||
const serverListProps: ServerListProps = {
|
||||
servers,
|
||||
serverListWritable,
|
||||
highlightedServerIds,
|
||||
onClearHighlight: clearHighlight,
|
||||
onServerAdd,
|
||||
onServerImportConfig,
|
||||
onServerImportJson,
|
||||
onServerExport,
|
||||
onConnectionInfo: openConnectionInfo,
|
||||
onServerSettings: openServerSettings,
|
||||
onServerEdit,
|
||||
onServerClone,
|
||||
onServerRemove,
|
||||
onServerReorder: onServerReorderFromList,
|
||||
};
|
||||
|
||||
const toolsPanelProps: ToolsPanelProps = {
|
||||
tools,
|
||||
excludedTools,
|
||||
toolsListChanged,
|
||||
toolsLoadError: toolsPagination.error,
|
||||
toolsUi: ui.toolsUi,
|
||||
toolCallState,
|
||||
toolsPagination: toolsPaginationControls,
|
||||
serverSupportsTaskToolCalls:
|
||||
!!capabilities?.tasks?.requests?.tools?.call ||
|
||||
(inspectorClient?.isTasksExtensionNegotiated() ?? false),
|
||||
onToolsUiChange,
|
||||
onCallTool: dispatchCallTool,
|
||||
onCancelToolCall,
|
||||
onClearToolResult,
|
||||
onRefreshTools,
|
||||
onReadResourceContents,
|
||||
};
|
||||
|
||||
const promptsPanelProps: PromptsPanelProps = {
|
||||
prompts,
|
||||
promptsListChanged,
|
||||
promptsLoadError: promptsPagination.error,
|
||||
promptsUi: ui.promptsUi,
|
||||
getPromptState,
|
||||
promptsPagination: promptsPaginationControls,
|
||||
onPromptsUiChange: setUi.setPromptsUi,
|
||||
onGetPrompt: dispatchGetPrompt,
|
||||
onRefreshPrompts,
|
||||
};
|
||||
|
||||
const resourcesPanelProps: ResourcesPanelProps = {
|
||||
resources,
|
||||
resourceTemplates,
|
||||
subscriptions,
|
||||
subscriptionStreamState,
|
||||
subscriptionsSupported: capabilities?.resources?.subscribe === true,
|
||||
resourcesListChanged,
|
||||
resourcesLoadError: resourcesPagination.error ?? resourceTemplatesLoadError,
|
||||
resourcesUi: ui.resourcesUi,
|
||||
readResourceState: effectiveReadResourceState,
|
||||
resourcesPagination: resourcesPaginationControls,
|
||||
onResourcesUiChange: setUi.setResourcesUi,
|
||||
onReadResource: dispatchReadResource,
|
||||
onSubscribeResource,
|
||||
onUnsubscribeResource,
|
||||
onRefreshResources,
|
||||
};
|
||||
|
||||
const appsPanelProps: AppsPanelProps = {
|
||||
appsUi: ui.appsUi,
|
||||
sandboxPath: sandboxUrl,
|
||||
bridgeFactory: sandboxBridgeFactory,
|
||||
appRendererRef,
|
||||
onAppsUiChange: setUi.setAppsUi,
|
||||
onSelectApp,
|
||||
onOpenApp: dispatchOpenApp,
|
||||
onCloseApp,
|
||||
onAppError,
|
||||
// The Apps tab is a filtered view of the tools list, so refreshing it is
|
||||
// refreshing tools.
|
||||
onRefreshApps: onRefreshTools,
|
||||
};
|
||||
|
||||
const tasksPanelProps: TasksPanelProps = {
|
||||
tasks,
|
||||
progressByTaskId,
|
||||
tasksUi: ui.tasksUi,
|
||||
onTasksUiChange: setUi.setTasksUi,
|
||||
onCancelTask: dispatchCancelTask,
|
||||
onClearCompletedTasks,
|
||||
onRefreshTasks,
|
||||
};
|
||||
|
||||
const logsPanelProps: LogsPanelProps = {
|
||||
logs,
|
||||
logsUi: ui.logsUi,
|
||||
currentLogLevel,
|
||||
modernLogLevel,
|
||||
onSetLogLevel,
|
||||
onSetModernLogLevel,
|
||||
onLogsUiChange: setUi.setLogsUi,
|
||||
onClearLogs,
|
||||
onExportLogs,
|
||||
};
|
||||
|
||||
const protocolPanelProps: ProtocolPanelProps = {
|
||||
protocol: protocolEntries,
|
||||
protocolUi: ui.protocolUi,
|
||||
pinnedProtocolIds,
|
||||
onProtocolUiChange: setUi.setProtocolUi,
|
||||
onClearProtocol,
|
||||
onExportProtocol,
|
||||
onClearProtocolSection,
|
||||
onExportProtocolSection,
|
||||
onReplayProtocol,
|
||||
onTogglePinProtocol: togglePinProtocol,
|
||||
};
|
||||
|
||||
const networkPanelProps: NetworkPanelProps = {
|
||||
network: fetchRequests,
|
||||
networkUi: ui.networkUi,
|
||||
onNetworkUiChange: setUi.setNetworkUi,
|
||||
onClearNetwork,
|
||||
onExportNetwork,
|
||||
};
|
||||
|
||||
const consolePanelProps: ConsolePanelProps = {
|
||||
stderrLogs,
|
||||
consoleUi: ui.consoleUi,
|
||||
onConsoleUiChange: setUi.setConsoleUi,
|
||||
onClearConsole,
|
||||
onExportConsole,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box>
|
||||
|
|
@ -1484,171 +1811,18 @@ function App() {
|
|||
</ReAuthBannerBar>
|
||||
) : null}
|
||||
<InspectorView
|
||||
deepLink={deepLink}
|
||||
deepLinkStatus={deepLinkStatus}
|
||||
servers={servers}
|
||||
serverListWritable={serverListWritable}
|
||||
activeServer={activeServerId}
|
||||
erroredServerId={failedServerId}
|
||||
connectedServerId={connectedServerId}
|
||||
version={inspectorVersion}
|
||||
connectionStatus={connectionStatus}
|
||||
connectErrorMessage={connectErrorMessage}
|
||||
initializeResult={initializeResult}
|
||||
latencyMs={latencyMs}
|
||||
tools={tools}
|
||||
excludedTools={excludedTools}
|
||||
malformedListItems={shownMalformedListItems}
|
||||
prompts={prompts}
|
||||
resources={resources}
|
||||
resourceTemplates={resourceTemplates}
|
||||
toolsListChanged={toolsListChanged}
|
||||
promptsListChanged={promptsListChanged}
|
||||
resourcesListChanged={resourcesListChanged}
|
||||
toolsLoadError={toolsPagination.error}
|
||||
promptsLoadError={promptsPagination.error}
|
||||
resourcesLoadError={
|
||||
resourcesPagination.error ?? resourceTemplatesLoadError
|
||||
}
|
||||
subscriptions={subscriptions}
|
||||
subscriptionStreamState={subscriptionStreamState}
|
||||
logs={logs}
|
||||
tasks={tasks}
|
||||
progressByTaskId={progressByTaskId}
|
||||
protocol={protocolEntries}
|
||||
protocolEra={protocolEra}
|
||||
network={fetchRequests}
|
||||
stderrLogs={stderrLogs}
|
||||
toolCallState={toolCallState}
|
||||
getPromptState={getPromptState}
|
||||
readResourceState={effectiveReadResourceState}
|
||||
toolsUi={ui.toolsUi}
|
||||
promptsUi={ui.promptsUi}
|
||||
resourcesUi={ui.resourcesUi}
|
||||
appsUi={ui.appsUi}
|
||||
tasksUi={ui.tasksUi}
|
||||
logsUi={ui.logsUi}
|
||||
protocolUi={ui.protocolUi}
|
||||
networkUi={ui.networkUi}
|
||||
consoleUi={ui.consoleUi}
|
||||
activeTab={activeTab}
|
||||
onActiveTabChange={setActiveTab}
|
||||
currentLogLevel={currentLogLevel}
|
||||
sandboxPath={sandboxUrl}
|
||||
bridgeFactory={sandboxBridgeFactory}
|
||||
appRendererRef={appRendererRef}
|
||||
onToggleTheme={onToggleTheme}
|
||||
onOpenClientSettings={() => setClientSettingsOpen(true)}
|
||||
onToggleConnection={(id) => {
|
||||
void onToggleConnection(id);
|
||||
}}
|
||||
onDisconnect={() => {
|
||||
void onDisconnect();
|
||||
}}
|
||||
onServerAdd={() => {
|
||||
setHighlightedServerIds([]);
|
||||
setConfigModal({ mode: "add" });
|
||||
}}
|
||||
onServerImportConfig={() => {
|
||||
setHighlightedServerIds([]);
|
||||
setImportConfigOpen(true);
|
||||
}}
|
||||
onServerImportJson={() => {
|
||||
setHighlightedServerIds([]);
|
||||
setImportJsonOpen(true);
|
||||
}}
|
||||
onServerExport={onServerExport}
|
||||
onConnectionInfo={() => setConnectionInfoModalOpen(true)}
|
||||
onServerSettings={(id) => setSettingsModalTargetId(id)}
|
||||
onServerEdit={(id) => setConfigModal({ mode: "edit", targetId: id })}
|
||||
onServerClone={(id) => {
|
||||
setHighlightedServerIds([]);
|
||||
setConfigModal({ mode: "clone", targetId: id });
|
||||
}}
|
||||
onServerRemove={(id) => {
|
||||
const target = servers.find((s) => s.id === id);
|
||||
if (target) setRemoveTarget(target);
|
||||
}}
|
||||
onServerReorder={(orderedIds) => {
|
||||
// reorderServers reverts the optimistic order via an internal
|
||||
// refresh() and re-throws on failure (409 from a racing external
|
||||
// edit, or a network error). Surface that to the user so the drag
|
||||
// doesn't silently bounce back — matching the toast pattern every
|
||||
// other mutation here uses.
|
||||
reorderServers(orderedIds).catch((err: unknown) => {
|
||||
notifications.show({
|
||||
title: "Failed to reorder servers",
|
||||
message: err instanceof Error ? err.message : String(err),
|
||||
color: "red",
|
||||
});
|
||||
});
|
||||
}}
|
||||
highlightedServerIds={highlightedServerIds}
|
||||
onClearHighlight={clearHighlight}
|
||||
serverSupportsTaskToolCalls={
|
||||
!!capabilities?.tasks?.requests?.tools?.call ||
|
||||
(inspectorClient?.isTasksExtensionNegotiated() ?? false)
|
||||
}
|
||||
onToolsUiChange={onToolsUiChange}
|
||||
onCallTool={(name, args, runAsTask) => {
|
||||
void onCallTool(name, args, runAsTask);
|
||||
}}
|
||||
onCancelToolCall={onCancelToolCall}
|
||||
onClearToolResult={onClearToolResult}
|
||||
onReadResourceContents={onReadResourceContents}
|
||||
onRefreshTools={onRefreshTools}
|
||||
toolsPagination={toolsPaginationControls}
|
||||
promptsPagination={promptsPaginationControls}
|
||||
resourcesPagination={resourcesPaginationControls}
|
||||
onPromptsUiChange={setUi.setPromptsUi}
|
||||
onGetPrompt={(name, args) => {
|
||||
void onGetPrompt(name, args);
|
||||
}}
|
||||
onRefreshPrompts={onRefreshPrompts}
|
||||
onResourcesUiChange={setUi.setResourcesUi}
|
||||
onReadResource={(uri) => {
|
||||
void onReadResource(uri);
|
||||
}}
|
||||
onSubscribeResource={onSubscribeResource}
|
||||
onUnsubscribeResource={onUnsubscribeResource}
|
||||
onRefreshResources={onRefreshResources}
|
||||
onCompleteArgument={onCompleteArgument}
|
||||
completionsSupported={capabilities?.completions !== undefined}
|
||||
subscriptionsSupported={capabilities?.resources?.subscribe === true}
|
||||
onTasksUiChange={setUi.setTasksUi}
|
||||
onCancelTask={(taskId) => {
|
||||
void onCancelTask(taskId);
|
||||
}}
|
||||
onClearCompletedTasks={onClearCompletedTasks}
|
||||
onRefreshTasks={onRefreshTasks}
|
||||
onSetLogLevel={onSetLogLevel}
|
||||
modernLogLevel={modernLogLevel}
|
||||
onSetModernLogLevel={onSetModernLogLevel}
|
||||
onLogsUiChange={setUi.setLogsUi}
|
||||
onClearLogs={onClearLogs}
|
||||
onExportLogs={onExportLogs}
|
||||
onProtocolUiChange={setUi.setProtocolUi}
|
||||
onClearProtocol={onClearProtocol}
|
||||
onExportProtocol={onExportProtocol}
|
||||
onClearProtocolSection={onClearProtocolSection}
|
||||
onExportProtocolSection={onExportProtocolSection}
|
||||
onReplayProtocol={onReplayProtocol}
|
||||
onTogglePinProtocol={togglePinProtocol}
|
||||
pinnedProtocolIds={pinnedProtocolIds}
|
||||
onNetworkUiChange={setUi.setNetworkUi}
|
||||
onClearNetwork={onClearNetwork}
|
||||
onExportNetwork={onExportNetwork}
|
||||
onConsoleUiChange={setUi.setConsoleUi}
|
||||
onClearConsole={onClearConsole}
|
||||
onExportConsole={onExportConsole}
|
||||
onAppsUiChange={setUi.setAppsUi}
|
||||
onSelectApp={onSelectApp}
|
||||
onOpenApp={(name, args) => {
|
||||
void onOpenApp(name, args);
|
||||
}}
|
||||
onCloseApp={onCloseApp}
|
||||
onAppError={onAppError}
|
||||
onRefreshApps={onRefreshTools}
|
||||
shell={shellProps}
|
||||
connection={connectionProps}
|
||||
servers={serverListProps}
|
||||
tools={toolsPanelProps}
|
||||
prompts={promptsPanelProps}
|
||||
resources={resourcesPanelProps}
|
||||
apps={appsPanelProps}
|
||||
tasks={tasksPanelProps}
|
||||
logs={logsPanelProps}
|
||||
protocol={protocolPanelProps}
|
||||
network={networkPanelProps}
|
||||
console={consolePanelProps}
|
||||
/>
|
||||
</Box>
|
||||
<AppElicitationHost
|
||||
|
|
|
|||
|
|
@ -18,6 +18,20 @@ import type { Meta, StoryObj } from "@storybook/react-vite";
|
|||
import { useState } from "react";
|
||||
import { expect, fn } from "storybook/test";
|
||||
import { InspectorView } from "./InspectorView";
|
||||
import type {
|
||||
AppsPanelProps,
|
||||
ConnectionProps,
|
||||
ConsolePanelProps,
|
||||
LogsPanelProps,
|
||||
NetworkPanelProps,
|
||||
PromptsPanelProps,
|
||||
ProtocolPanelProps,
|
||||
ResourcesPanelProps,
|
||||
ServerListProps,
|
||||
ShellProps,
|
||||
TasksPanelProps,
|
||||
ToolsPanelProps,
|
||||
} from "./types";
|
||||
import { noopPagination } from "../../../test/fixtures/pagination";
|
||||
import {
|
||||
EMPTY_TOOLS_UI,
|
||||
|
|
@ -324,116 +338,164 @@ const demoInitializeResult: InitializeResult = {
|
|||
serverInfo: { name: "Local Dev Server", version: "1.2.0" },
|
||||
};
|
||||
|
||||
// One constant per domain bundle, so a story can override a single field
|
||||
// without replacing its whole bundle — Storybook merges args only at the top
|
||||
// level, and each of these IS one top-level arg now (#2130).
|
||||
const shellArgs: ShellProps = {
|
||||
activeTab: "Servers",
|
||||
onActiveTabChange: fn(),
|
||||
onToggleTheme: fn(),
|
||||
onOpenClientSettings: fn(),
|
||||
};
|
||||
|
||||
// Stories default to "disconnected"; per-story overrides drive the connected /
|
||||
// error narratives.
|
||||
const connectionArgs: ConnectionProps = {
|
||||
activeServer: undefined,
|
||||
connectionStatus: "disconnected",
|
||||
initializeResult: undefined,
|
||||
latencyMs: undefined,
|
||||
onToggleConnection: fn(),
|
||||
onDisconnect: fn(),
|
||||
};
|
||||
|
||||
// Callbacks are wired to storybook spies so play functions can assert on
|
||||
// dispatch. Real wiring routes these to InspectorClient methods (the app shell
|
||||
// at clients/web/src/App.tsx).
|
||||
const serversArgs: ServerListProps = {
|
||||
servers: demoServers,
|
||||
onServerAdd: fn(),
|
||||
onServerImportConfig: fn(),
|
||||
onServerImportJson: fn(),
|
||||
onServerExport: fn(),
|
||||
onConnectionInfo: fn(),
|
||||
onServerSettings: fn(),
|
||||
onServerEdit: fn(),
|
||||
onServerClone: fn(),
|
||||
onServerRemove: fn(),
|
||||
onServerReorder: fn(),
|
||||
};
|
||||
|
||||
const toolsArgs: ToolsPanelProps = {
|
||||
tools: demoTools,
|
||||
toolsListChanged: false,
|
||||
toolsUi: EMPTY_TOOLS_UI,
|
||||
toolsPagination: noopPagination,
|
||||
serverSupportsTaskToolCalls: false,
|
||||
onToolsUiChange: fn(),
|
||||
onCallTool: fn(),
|
||||
onRefreshTools: fn(),
|
||||
};
|
||||
|
||||
const promptsArgs: PromptsPanelProps = {
|
||||
prompts: demoPrompts,
|
||||
promptsListChanged: false,
|
||||
promptsUi: EMPTY_PROMPTS_UI,
|
||||
promptsPagination: noopPagination,
|
||||
onPromptsUiChange: fn(),
|
||||
onGetPrompt: fn(),
|
||||
onRefreshPrompts: fn(),
|
||||
};
|
||||
|
||||
const resourcesArgs: ResourcesPanelProps = {
|
||||
resources: demoResources,
|
||||
resourceTemplates: demoResourceTemplates,
|
||||
subscriptions: demoSubscriptions,
|
||||
resourcesListChanged: false,
|
||||
resourcesUi: EMPTY_RESOURCES_UI,
|
||||
resourcesPagination: noopPagination,
|
||||
onResourcesUiChange: fn(),
|
||||
onReadResource: fn(),
|
||||
onSubscribeResource: fn(),
|
||||
onUnsubscribeResource: fn(),
|
||||
onRefreshResources: fn(),
|
||||
};
|
||||
|
||||
const appsArgs: AppsPanelProps = {
|
||||
appsUi: EMPTY_APPS_UI,
|
||||
sandboxPath: "about:blank",
|
||||
bridgeFactory: noopBridgeFactory,
|
||||
appRendererRef: { current: null },
|
||||
onAppsUiChange: fn(),
|
||||
onSelectApp: fn(),
|
||||
onOpenApp: fn(),
|
||||
onCloseApp: fn(),
|
||||
onAppError: fn(),
|
||||
onRefreshApps: fn(),
|
||||
};
|
||||
|
||||
const tasksArgs: TasksPanelProps = {
|
||||
tasks: demoTasks,
|
||||
progressByTaskId: demoProgressByTaskId,
|
||||
tasksUi: EMPTY_TASKS_UI,
|
||||
onTasksUiChange: fn(),
|
||||
onCancelTask: fn(),
|
||||
onClearCompletedTasks: fn(),
|
||||
onRefreshTasks: fn(),
|
||||
};
|
||||
|
||||
const logsArgs: LogsPanelProps = {
|
||||
logs: demoLogs,
|
||||
logsUi: EMPTY_LOGS_UI,
|
||||
currentLogLevel: "info",
|
||||
onSetLogLevel: fn(),
|
||||
onLogsUiChange: fn(),
|
||||
onClearLogs: fn(),
|
||||
onExportLogs: fn(),
|
||||
};
|
||||
|
||||
const protocolArgs: ProtocolPanelProps = {
|
||||
protocol: demoHistory,
|
||||
protocolUi: EMPTY_PROTOCOL_UI,
|
||||
onProtocolUiChange: fn(),
|
||||
onClearProtocol: fn(),
|
||||
onExportProtocol: fn(),
|
||||
onClearProtocolSection: fn(),
|
||||
onExportProtocolSection: fn(),
|
||||
onReplayProtocol: fn(),
|
||||
onTogglePinProtocol: fn(),
|
||||
};
|
||||
|
||||
const networkArgs: NetworkPanelProps = {
|
||||
network: demoNetwork,
|
||||
networkUi: EMPTY_NETWORK_UI,
|
||||
onNetworkUiChange: fn(),
|
||||
onClearNetwork: fn(),
|
||||
onExportNetwork: fn(),
|
||||
};
|
||||
|
||||
const consoleArgs: ConsolePanelProps = {
|
||||
stderrLogs: demoStderr,
|
||||
consoleUi: EMPTY_CONSOLE_UI,
|
||||
onConsoleUiChange: fn(),
|
||||
onClearConsole: fn(),
|
||||
onExportConsole: fn(),
|
||||
};
|
||||
|
||||
const meta: Meta<typeof InspectorView> = {
|
||||
title: "Views/InspectorView",
|
||||
component: InspectorView,
|
||||
parameters: { layout: "fullscreen" },
|
||||
args: {
|
||||
// Data
|
||||
servers: demoServers,
|
||||
tools: demoTools,
|
||||
prompts: demoPrompts,
|
||||
resources: demoResources,
|
||||
resourceTemplates: demoResourceTemplates,
|
||||
subscriptions: demoSubscriptions,
|
||||
logs: demoLogs,
|
||||
tasks: demoTasks,
|
||||
progressByTaskId: demoProgressByTaskId,
|
||||
protocol: demoHistory,
|
||||
network: demoNetwork,
|
||||
stderrLogs: demoStderr,
|
||||
|
||||
// Connection state — stories default to "disconnected"; per-story
|
||||
// overrides drive the connected / error narratives.
|
||||
activeServer: undefined,
|
||||
connectionStatus: "disconnected",
|
||||
initializeResult: undefined,
|
||||
latencyMs: undefined,
|
||||
|
||||
// Misc state
|
||||
currentLogLevel: "info",
|
||||
sandboxPath: "about:blank",
|
||||
bridgeFactory: noopBridgeFactory,
|
||||
appRendererRef: { current: null },
|
||||
|
||||
// Per-screen UI state (search / filter / selection), one object per screen.
|
||||
toolsUi: EMPTY_TOOLS_UI,
|
||||
promptsUi: EMPTY_PROMPTS_UI,
|
||||
resourcesUi: EMPTY_RESOURCES_UI,
|
||||
appsUi: EMPTY_APPS_UI,
|
||||
tasksUi: EMPTY_TASKS_UI,
|
||||
logsUi: EMPTY_LOGS_UI,
|
||||
protocolUi: EMPTY_PROTOCOL_UI,
|
||||
networkUi: EMPTY_NETWORK_UI,
|
||||
consoleUi: EMPTY_CONSOLE_UI,
|
||||
|
||||
// Callbacks — all wired to storybook spies so play functions can assert
|
||||
// on dispatch. Real wiring routes these to InspectorClient methods (the
|
||||
// app shell at clients/web/src/App.tsx).
|
||||
onToggleTheme: fn(),
|
||||
onOpenClientSettings: fn(),
|
||||
onToggleConnection: fn(),
|
||||
onDisconnect: fn(),
|
||||
onServerAdd: fn(),
|
||||
onServerImportConfig: fn(),
|
||||
onServerImportJson: fn(),
|
||||
onServerExport: fn(),
|
||||
onConnectionInfo: fn(),
|
||||
onServerSettings: fn(),
|
||||
onServerEdit: fn(),
|
||||
onServerClone: fn(),
|
||||
onServerRemove: fn(),
|
||||
onServerReorder: fn(),
|
||||
serverSupportsTaskToolCalls: false,
|
||||
onToolsUiChange: fn(),
|
||||
onCallTool: fn(),
|
||||
onRefreshTools: fn(),
|
||||
toolsPagination: noopPagination,
|
||||
promptsPagination: noopPagination,
|
||||
resourcesPagination: noopPagination,
|
||||
onPromptsUiChange: fn(),
|
||||
onGetPrompt: fn(),
|
||||
onRefreshPrompts: fn(),
|
||||
onResourcesUiChange: fn(),
|
||||
onReadResource: fn(),
|
||||
onSubscribeResource: fn(),
|
||||
onUnsubscribeResource: fn(),
|
||||
onRefreshResources: fn(),
|
||||
onTasksUiChange: fn(),
|
||||
onCancelTask: fn(),
|
||||
onClearCompletedTasks: fn(),
|
||||
onRefreshTasks: fn(),
|
||||
onSetLogLevel: fn(),
|
||||
onLogsUiChange: fn(),
|
||||
onClearLogs: fn(),
|
||||
onExportLogs: fn(),
|
||||
onProtocolUiChange: fn(),
|
||||
onClearProtocol: fn(),
|
||||
onExportProtocol: fn(),
|
||||
onReplayProtocol: fn(),
|
||||
onTogglePinProtocol: fn(),
|
||||
onNetworkUiChange: fn(),
|
||||
onClearNetwork: fn(),
|
||||
onExportNetwork: fn(),
|
||||
onConsoleUiChange: fn(),
|
||||
onClearConsole: fn(),
|
||||
onExportConsole: fn(),
|
||||
onAppsUiChange: fn(),
|
||||
onSelectApp: fn(),
|
||||
onOpenApp: fn(),
|
||||
onCloseApp: fn(),
|
||||
onAppError: fn(),
|
||||
onRefreshApps: fn(),
|
||||
activeTab: "Servers",
|
||||
onActiveTabChange: fn(),
|
||||
shell: shellArgs,
|
||||
connection: connectionArgs,
|
||||
servers: serversArgs,
|
||||
tools: toolsArgs,
|
||||
prompts: promptsArgs,
|
||||
resources: resourcesArgs,
|
||||
apps: appsArgs,
|
||||
tasks: tasksArgs,
|
||||
logs: logsArgs,
|
||||
protocol: protocolArgs,
|
||||
network: networkArgs,
|
||||
console: consoleArgs,
|
||||
},
|
||||
render: (args) => {
|
||||
const [activeTab, setActiveTab] = useState(args.activeTab ?? "Servers");
|
||||
const [activeTab, setActiveTab] = useState(args.shell.activeTab);
|
||||
return (
|
||||
<InspectorView
|
||||
{...args}
|
||||
activeTab={activeTab}
|
||||
onActiveTabChange={setActiveTab}
|
||||
shell={{ ...args.shell, activeTab, onActiveTabChange: setActiveTab }}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
|
@ -463,7 +525,7 @@ export const Default: Story = {
|
|||
|
||||
export const NoServers: Story = {
|
||||
args: {
|
||||
servers: [],
|
||||
servers: { ...serversArgs, servers: [] },
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -480,7 +542,7 @@ const manyServers: ServerEntry[] = Array.from({ length: 24 }, (_, i) => ({
|
|||
|
||||
export const ManyServers: Story = {
|
||||
args: {
|
||||
servers: manyServers,
|
||||
servers: { ...serversArgs, servers: manyServers },
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
// Even under enough content to overflow, the shell stays viewport-clamped
|
||||
|
|
@ -509,10 +571,13 @@ export const ManyServers: Story = {
|
|||
// regression / storybook play function coverage.
|
||||
export const Connected: Story = {
|
||||
args: {
|
||||
activeServer: demoServers[0]!.id,
|
||||
connectionStatus: "connected",
|
||||
initializeResult: demoInitializeResult,
|
||||
latencyMs: 142,
|
||||
connection: {
|
||||
...connectionArgs,
|
||||
activeServer: demoServers[0]!.id,
|
||||
connectionStatus: "connected",
|
||||
initializeResult: demoInitializeResult,
|
||||
latencyMs: 142,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -524,10 +589,13 @@ export const ConnectionError: Story = {
|
|||
// connect-attempt-failure signal that gates the failure column; `network: []`
|
||||
// (and empty Protocol) keep it a pure stdio failure so only Console is offered.
|
||||
args: {
|
||||
activeServer: demoServers[0]!.id,
|
||||
erroredServerId: demoServers[0]!.id,
|
||||
connectionStatus: "error",
|
||||
network: [],
|
||||
stderrLogs: demoStderr,
|
||||
connection: {
|
||||
...connectionArgs,
|
||||
activeServer: demoServers[0]!.id,
|
||||
erroredServerId: demoServers[0]!.id,
|
||||
connectionStatus: "error",
|
||||
},
|
||||
network: { ...networkArgs, network: [] },
|
||||
console: { ...consoleArgs, stderrLogs: demoStderr },
|
||||
},
|
||||
};
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,14 +1,4 @@
|
|||
import {
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
type ReactNode,
|
||||
type Ref,
|
||||
} from "react";
|
||||
import type { MalformedListItem } from "@inspector/core/mcp";
|
||||
import type { ReplayParamsOverride } from "../../../lib/protocolReplay";
|
||||
import type { DeepLink, DeepLinkParseStatus } from "../../../utils/deepLink";
|
||||
import { useEffect, useMemo, useRef, useState, type ReactNode } from "react";
|
||||
import {
|
||||
AppShell,
|
||||
Flex,
|
||||
|
|
@ -18,81 +8,38 @@ import {
|
|||
type MantineTransition,
|
||||
} from "@mantine/core";
|
||||
import { useLocalStorage } from "@mantine/hooks";
|
||||
import type {
|
||||
Implementation,
|
||||
InitializeResult,
|
||||
LoggingLevel,
|
||||
Prompt,
|
||||
ProtocolEra,
|
||||
ReadResourceResult,
|
||||
Resource,
|
||||
ResourceTemplateType as ResourceTemplate,
|
||||
Task,
|
||||
Tool,
|
||||
} from "@modelcontextprotocol/client";
|
||||
import type {
|
||||
ConnectionStatus,
|
||||
ExcludedTool,
|
||||
FetchRequestEntry,
|
||||
InspectorResourceSubscription,
|
||||
MessageEntry,
|
||||
ResourceSubscriptionStreamState,
|
||||
ServerEntry,
|
||||
StderrLogEntry,
|
||||
} from "@inspector/core/mcp/types.js";
|
||||
import type { Implementation, Tool } from "@modelcontextprotocol/client";
|
||||
import type { ServerEntry } from "@inspector/core/mcp/types.js";
|
||||
import { isTerminalStatus } from "@inspector/core/mcp/types.js";
|
||||
import { isAppTool } from "@inspector/core/mcp/apps.js";
|
||||
import { TASKS_EXTENSION_KEY } from "@inspector/core/mcp/modernTaskSchemas.js";
|
||||
import { ViewHeader } from "../../groups/ViewHeader/ViewHeader";
|
||||
import { VersionBadge } from "../../elements/VersionBadge/VersionBadge";
|
||||
import { CopyrightBadge } from "../../elements/CopyrightBadge/CopyrightBadge";
|
||||
import type { ListPaginationControlsProps } from "../../elements/ListPaginationControls/ListPaginationControls";
|
||||
import { ServerListScreen } from "../../screens/ServerListScreen/ServerListScreen";
|
||||
import {
|
||||
ToolsScreen,
|
||||
type ToolCallState,
|
||||
type ToolsUiState,
|
||||
} from "../../screens/ToolsScreen/ToolsScreen";
|
||||
import {
|
||||
AppsScreen,
|
||||
type AppsUiState,
|
||||
} from "../../screens/AppsScreen/AppsScreen";
|
||||
import { ToolsScreen } from "../../screens/ToolsScreen/ToolsScreen";
|
||||
import { AppsScreen } from "../../screens/AppsScreen/AppsScreen";
|
||||
import { PromptsScreen } from "../../screens/PromptsScreen/PromptsScreen";
|
||||
import { ResourcesScreen } from "../../screens/ResourcesScreen/ResourcesScreen";
|
||||
import { LoggingScreen } from "../../screens/LoggingScreen/LoggingScreen";
|
||||
import { TasksScreen } from "../../screens/TasksScreen/TasksScreen";
|
||||
import { ProtocolScreen } from "../../screens/ProtocolScreen/ProtocolScreen";
|
||||
import { NetworkScreen } from "../../screens/NetworkScreen/NetworkScreen";
|
||||
import { ConsoleScreen } from "../../screens/ConsoleScreen/ConsoleScreen";
|
||||
import type {
|
||||
AppRendererHandle,
|
||||
BridgeFactory,
|
||||
} from "../../elements/AppRenderer/AppRenderer";
|
||||
import {
|
||||
PromptsScreen,
|
||||
type GetPromptState,
|
||||
type PromptsUiState,
|
||||
} from "../../screens/PromptsScreen/PromptsScreen";
|
||||
import {
|
||||
ResourcesScreen,
|
||||
type ReadResourceState,
|
||||
type ResourcesUiState,
|
||||
} from "../../screens/ResourcesScreen/ResourcesScreen";
|
||||
import {
|
||||
LoggingScreen,
|
||||
type LogsUiState,
|
||||
} from "../../screens/LoggingScreen/LoggingScreen";
|
||||
import type { LogEntryData } from "../../elements/LogEntry/LogEntry";
|
||||
import {
|
||||
TasksScreen,
|
||||
type TasksUiState,
|
||||
} from "../../screens/TasksScreen/TasksScreen";
|
||||
import type { TaskProgress } from "../../groups/TaskCard/TaskCard";
|
||||
import {
|
||||
ProtocolScreen,
|
||||
type ProtocolUiState,
|
||||
} from "../../screens/ProtocolScreen/ProtocolScreen";
|
||||
import {
|
||||
NetworkScreen,
|
||||
type NetworkUiState,
|
||||
} from "../../screens/NetworkScreen/NetworkScreen";
|
||||
import {
|
||||
ConsoleScreen,
|
||||
type ConsoleUiState,
|
||||
} from "../../screens/ConsoleScreen/ConsoleScreen";
|
||||
AppsPanelProps,
|
||||
ConnectionProps,
|
||||
ConsolePanelProps,
|
||||
LogsPanelProps,
|
||||
NetworkPanelProps,
|
||||
PromptsPanelProps,
|
||||
ProtocolPanelProps,
|
||||
ResourcesPanelProps,
|
||||
ServerListProps,
|
||||
ShellProps,
|
||||
TasksPanelProps,
|
||||
ToolsPanelProps,
|
||||
} from "./types";
|
||||
import type { SortDirection } from "../../elements/SortToggle/SortToggle";
|
||||
import { ScreenStage } from "../../elements/ScreenStage/ScreenStage";
|
||||
import { MonitoringScreen } from "../../groups/MonitoringScreen/MonitoringScreen";
|
||||
|
|
@ -360,402 +307,193 @@ const MonitorColumnGroup = Group.withProps({
|
|||
flex: "0 0 auto",
|
||||
});
|
||||
|
||||
/**
|
||||
* `InspectorView` takes one prop per domain rather than a wall of ~130 flat
|
||||
* ones (#2130). Each bundle's shape lives in `./types`; the fields inside keep
|
||||
* the names they had as flat props, so this is a regrouping rather than a
|
||||
* rename.
|
||||
*
|
||||
* The bundles stop here: the function below destructures each one back into
|
||||
* the same locals it already used, and the screens beneath receive the same
|
||||
* individual props they always did.
|
||||
*/
|
||||
export interface InspectorViewProps {
|
||||
/**
|
||||
* Validated deep-link parameters from the page URL. When present and
|
||||
* `openApp` is set, the parent switches to the Apps tab and pre-selects that
|
||||
* app (with `appArgs` as the form values) once the connection is up and the
|
||||
* app list contains it. The connect itself is driven by the parent.
|
||||
*/
|
||||
deepLink?: DeepLink;
|
||||
/**
|
||||
* Outcome of parsing the initial-URL deep link, surfaced as `data-deeplink`
|
||||
* on the `connection-status` testid. Distinguishes "no deep link" from
|
||||
* "rejected" (token mismatch / bad serverUrl) — both leave `data-status`
|
||||
* idle, so an automated driver otherwise cannot tell them apart.
|
||||
*/
|
||||
deepLinkStatus?: DeepLinkParseStatus;
|
||||
|
||||
// Server list (static config; runtime connection state comes from the
|
||||
// separate fields below and is merged into each card by this component).
|
||||
servers: ServerEntry[];
|
||||
/**
|
||||
* Whether the server list is writable (catalog) or read-only (a `--config`
|
||||
* session file / ad-hoc launch). When false, the Servers screen hides all
|
||||
* catalog mutation controls. Defaults to true.
|
||||
*/
|
||||
serverListWritable?: boolean;
|
||||
|
||||
// Connection state — driven by the parent via `useInspectorClient`.
|
||||
activeServer?: string;
|
||||
/**
|
||||
* Id of the server whose last connection attempt failed (#1621). Its card in
|
||||
* the Servers screen draws a red border until another server is connected or
|
||||
* a new connection is attempted. Independent of `activeServer`, which the
|
||||
* parent clears on the failure's `disconnect` event.
|
||||
*
|
||||
* It is also the sole signal that opens the monitoring sidebar onto the
|
||||
* failure's diagnostics (#2108). That used to be gated on the `"error"`
|
||||
* connection status, which an OAuth failure never reaches — the parent drives
|
||||
* that leg and tears the client down itself, settling the session at
|
||||
* `"disconnected"`. So the parent must set this for *every* connect-attempt
|
||||
* failure, auth legs included, and clear it as each new attempt starts.
|
||||
*/
|
||||
erroredServerId?: string;
|
||||
/**
|
||||
* Id of the server that just connected successfully (#1682). Its card draws
|
||||
* the green highlight border and scrolls into view once the monitoring
|
||||
* sidebar has opened — the success mirror of `erroredServerId`.
|
||||
*/
|
||||
connectedServerId?: string;
|
||||
/**
|
||||
* The Inspector build version (root `package.json`), shown at the left of the
|
||||
* footer row (#1682). Absent on a legacy backend that omits it — the version
|
||||
* label then renders nothing.
|
||||
*/
|
||||
version?: string;
|
||||
connectionStatus: ConnectionStatus;
|
||||
/**
|
||||
* Last connection-level error message (handshake failure, OAuth start
|
||||
* failure, deep-link automation failure). Surfaced as `data-error-message`
|
||||
* on the header's `connection-status` testid so an automated driver can read
|
||||
* *why* a connect failed without scraping a transient toast.
|
||||
*/
|
||||
connectErrorMessage?: string;
|
||||
initializeResult?: InitializeResult;
|
||||
latencyMs?: number;
|
||||
|
||||
// Primitive lists, log streams, task state — all sourced from the
|
||||
// per-primitive `useManaged*` / `useMessageLog` hooks in the parent.
|
||||
tools: Tool[];
|
||||
/** Tools excluded from `tools/list` for invalid `x-mcp-header` annotations
|
||||
* (SEP-2243); shown in the Tools sidebar with the reason (#1632). */
|
||||
excludedTools?: ExcludedTool[];
|
||||
/**
|
||||
* Entries dropped from a list result as malformed, across every list method.
|
||||
* Each list panel filters for its own and warns about them (#1909).
|
||||
*/
|
||||
malformedListItems?: MalformedListItem[];
|
||||
prompts: Prompt[];
|
||||
resources: Resource[];
|
||||
resourceTemplates: ResourceTemplate[];
|
||||
subscriptions: InspectorResourceSubscription[];
|
||||
/**
|
||||
* Modern-era `subscriptions/listen` stream state (#1630). Drives the
|
||||
* Resources screen's stream badge/dot; `active: false` (or omitted) on the
|
||||
* legacy era.
|
||||
*/
|
||||
subscriptionStreamState?: ResourceSubscriptionStreamState;
|
||||
|
||||
// "List changed since last refresh" flags, sourced from the managed-state
|
||||
// layer (#1402). They light the per-screen list-changed indicator. Apps is a
|
||||
// filtered view of tools, so it shares the tools flag.
|
||||
toolsListChanged: boolean;
|
||||
promptsListChanged: boolean;
|
||||
resourcesListChanged: boolean;
|
||||
|
||||
// Last list-load failure per screen, sourced from the same managed-state
|
||||
// layer. Rendered above the sidebar list so a failed load (including the
|
||||
// connect-time one) can't read as "this server has none" (#1953).
|
||||
toolsLoadError?: Error | null;
|
||||
promptsLoadError?: Error | null;
|
||||
/**
|
||||
* The Resources sidebar lists resources AND templates behind a single
|
||||
* Refresh, so App passes whichever of the two loads failed.
|
||||
*/
|
||||
resourcesLoadError?: Error | null;
|
||||
logs: LogEntryData[];
|
||||
tasks: Task[];
|
||||
progressByTaskId?: Record<string, TaskProgress>;
|
||||
protocol: MessageEntry[];
|
||||
/** Negotiated protocol era (SEP §7.8), for the Protocol view's era badge. */
|
||||
protocolEra?: ProtocolEra;
|
||||
network: FetchRequestEntry[];
|
||||
/** Captured stdio stderr (the Console screen). Empty for HTTP servers. (#1621) */
|
||||
stderrLogs: StderrLogEntry[];
|
||||
|
||||
// Per-screen "operation in flight" states (panel-level; optional because
|
||||
// the underlying screens accept them as optional).
|
||||
toolCallState?: ToolCallState;
|
||||
getPromptState?: GetPromptState;
|
||||
readResourceState?: ReadResourceState;
|
||||
|
||||
// Per-screen selection / search / filter state, one object per screen. Owned
|
||||
// by the parent (App) so it persists across tab navigation within a live
|
||||
// session — the screens unmount on tab switch, so screen-local state would be
|
||||
// lost (#1417). Each is paired with an `on{Screen}UiChange` setter below.
|
||||
toolsUi: ToolsUiState;
|
||||
promptsUi: PromptsUiState;
|
||||
resourcesUi: ResourcesUiState;
|
||||
appsUi: AppsUiState;
|
||||
tasksUi: TasksUiState;
|
||||
logsUi: LogsUiState;
|
||||
protocolUi: ProtocolUiState;
|
||||
networkUi: NetworkUiState;
|
||||
consoleUi: ConsoleUiState;
|
||||
|
||||
/** Active inspector tab (lifted to App for OAuth resume). */
|
||||
activeTab: string;
|
||||
onActiveTabChange: (tab: string) => void;
|
||||
|
||||
// Logging level. The MCP `logging/setLevel` request has no echo
|
||||
// notification, so the parent keeps the optimistic current value.
|
||||
currentLogLevel: LoggingLevel;
|
||||
|
||||
// MCP Apps sandbox. The parent's web environment provides the sandbox iframe
|
||||
// URL (undefined when the sandbox controller is unavailable), the per-app
|
||||
// bridge factory, and the renderer handle the parent uses to push tool
|
||||
// input/result into the running app and tear it down.
|
||||
sandboxPath?: string;
|
||||
bridgeFactory: BridgeFactory;
|
||||
appRendererRef: Ref<AppRendererHandle>;
|
||||
|
||||
// Protocol pinning. Optional because pin state isn't persisted yet (#1244
|
||||
// is single-PR; persistence is a separate concern).
|
||||
pinnedProtocolIds?: Set<string>;
|
||||
|
||||
// Theme toggle (lives in the parent so the color scheme can also flow
|
||||
// into other top-level UI later).
|
||||
onToggleTheme: () => void;
|
||||
/** Open install-level client settings (client.json / EMA IdP). */
|
||||
onOpenClientSettings: () => void;
|
||||
|
||||
// Connection lifecycle (dispatched to `useInspectorClient.connect/disconnect`).
|
||||
onToggleConnection: (id: string) => void;
|
||||
onDisconnect: () => void;
|
||||
|
||||
// Server list actions.
|
||||
onServerAdd: () => void;
|
||||
onServerImportConfig: () => void;
|
||||
onServerImportJson: () => void;
|
||||
/** Download the current server list as a canonical `mcp.json` file. */
|
||||
onServerExport: () => void;
|
||||
onConnectionInfo: (id: string) => void;
|
||||
onServerSettings: (id: string) => void;
|
||||
onServerEdit: (id: string) => void;
|
||||
onServerClone: (id: string) => void;
|
||||
onServerRemove: (id: string) => void;
|
||||
/** Persist a new server ordering (drag-and-drop / keyboard reorder). */
|
||||
onServerReorder: (orderedIds: string[]) => void;
|
||||
/** Ids of freshly-added servers to highlight on the list (first is scrolled to). */
|
||||
highlightedServerIds?: string[];
|
||||
/** Clears the freshly-added highlight for a server (on click of its card). */
|
||||
onClearHighlight?: (id: string) => void;
|
||||
|
||||
// Per-primitive actions (route to `inspectorClient` methods / hook refresh).
|
||||
// Each `on{Screen}UiChange` persists that screen's lifted UI state (#1417).
|
||||
/** Whether the connected server advertises task-augmented tool calls. */
|
||||
serverSupportsTaskToolCalls: boolean;
|
||||
onToolsUiChange: (next: ToolsUiState) => void;
|
||||
onCallTool: (
|
||||
name: string,
|
||||
args: Record<string, unknown>,
|
||||
runAsTask?: boolean,
|
||||
) => void;
|
||||
onCancelToolCall?: () => void;
|
||||
onClearToolResult?: () => void;
|
||||
onRefreshTools: () => void;
|
||||
/** Pagination controls for the Tools list (#1721). */
|
||||
toolsPagination: ListPaginationControlsProps;
|
||||
/** Pagination controls for the Prompts list (#1721). */
|
||||
promptsPagination: ListPaginationControlsProps;
|
||||
/** Pagination controls for the Resources list (#1721). */
|
||||
resourcesPagination: ListPaginationControlsProps;
|
||||
/**
|
||||
* Read-on-demand handler for `resource_link` blocks in a tool result.
|
||||
* Returns the linked resource's contents so the result panel can inline them.
|
||||
*/
|
||||
onReadResourceContents?: (uri: string) => Promise<ReadResourceResult>;
|
||||
|
||||
onPromptsUiChange: (next: PromptsUiState) => void;
|
||||
onGetPrompt: (name: string, args: Record<string, string>) => void;
|
||||
onCopyPromptMessages?: () => void;
|
||||
onRefreshPrompts: () => void;
|
||||
|
||||
onResourcesUiChange: (next: ResourcesUiState) => void;
|
||||
onReadResource: (uri: string) => void;
|
||||
onSubscribeResource: (uri: string) => void;
|
||||
onUnsubscribeResource: (uri: string) => void;
|
||||
onRefreshResources: () => void;
|
||||
onCompleteArgument?: (
|
||||
ref:
|
||||
| { type: "ref/resource"; uri: string }
|
||||
| { type: "ref/prompt"; name: string },
|
||||
argumentName: string,
|
||||
argumentValue: string,
|
||||
context: Record<string, string>,
|
||||
) => Promise<string[]>;
|
||||
completionsSupported?: boolean;
|
||||
/**
|
||||
* Whether the connected server advertises the `resources.subscribe`
|
||||
* capability. When false, the Resources screen hides the Subscribe/
|
||||
* Unsubscribe button and the Subscriptions accordion section.
|
||||
*/
|
||||
subscriptionsSupported?: boolean;
|
||||
|
||||
onTasksUiChange: (next: TasksUiState) => void;
|
||||
onCancelTask: (taskId: string) => void;
|
||||
onClearCompletedTasks: () => void;
|
||||
onRefreshTasks: () => void;
|
||||
|
||||
onSetLogLevel: (level: LoggingLevel) => void;
|
||||
/**
|
||||
* Modern-era per-request log level currently stamped, or `null` when opted
|
||||
* out (#1629). On modern connections the Logs sidebar shows a per-request
|
||||
* opt-in control instead of the legacy `logging/setLevel` selector.
|
||||
*/
|
||||
modernLogLevel?: LoggingLevel | null;
|
||||
/** Set (or clear, with `null`) the modern per-request log level. */
|
||||
onSetModernLogLevel?: (level: LoggingLevel | null) => void;
|
||||
onLogsUiChange: (next: LogsUiState) => void;
|
||||
onClearLogs: () => void;
|
||||
onExportLogs: () => void;
|
||||
|
||||
onProtocolUiChange: (next: ProtocolUiState) => void;
|
||||
onClearProtocol: () => void;
|
||||
onExportProtocol: () => void;
|
||||
onClearProtocolSection: (section: "pinned" | "history") => void;
|
||||
onExportProtocolSection: (section: "pinned" | "history") => void;
|
||||
onReplayProtocol: (id: string, overrideParams?: ReplayParamsOverride) => void;
|
||||
onTogglePinProtocol: (id: string) => void;
|
||||
|
||||
onNetworkUiChange: (next: NetworkUiState) => void;
|
||||
onClearNetwork: () => void;
|
||||
onExportNetwork: () => void;
|
||||
|
||||
onConsoleUiChange: (next: ConsoleUiState) => void;
|
||||
onClearConsole: () => void;
|
||||
onExportConsole: () => void;
|
||||
|
||||
onAppsUiChange: (next: AppsUiState) => void;
|
||||
onSelectApp: (name: string) => void;
|
||||
onOpenApp: (name: string, args: Record<string, unknown>) => void;
|
||||
onCloseApp: () => void;
|
||||
onAppError: (err: Error) => void;
|
||||
onRefreshApps: () => void;
|
||||
/** App chrome: active tab, theme/settings entry points, footer, deep link. */
|
||||
shell: ShellProps;
|
||||
/** Live connection state plus the capability facts several screens read. */
|
||||
connection: ConnectionProps;
|
||||
/** The server catalog and every action on it. */
|
||||
servers: ServerListProps;
|
||||
tools: ToolsPanelProps;
|
||||
prompts: PromptsPanelProps;
|
||||
resources: ResourcesPanelProps;
|
||||
apps: AppsPanelProps;
|
||||
tasks: TasksPanelProps;
|
||||
logs: LogsPanelProps;
|
||||
protocol: ProtocolPanelProps;
|
||||
network: NetworkPanelProps;
|
||||
console: ConsolePanelProps;
|
||||
}
|
||||
|
||||
export function InspectorView({
|
||||
deepLink,
|
||||
deepLinkStatus,
|
||||
servers: serversInput,
|
||||
serverListWritable = true,
|
||||
activeServer,
|
||||
erroredServerId,
|
||||
connectedServerId,
|
||||
version,
|
||||
connectionStatus,
|
||||
connectErrorMessage,
|
||||
initializeResult,
|
||||
latencyMs,
|
||||
tools,
|
||||
excludedTools = [],
|
||||
malformedListItems = [],
|
||||
prompts,
|
||||
resources,
|
||||
resourceTemplates,
|
||||
toolsListChanged,
|
||||
promptsListChanged,
|
||||
resourcesListChanged,
|
||||
toolsLoadError,
|
||||
promptsLoadError,
|
||||
resourcesLoadError,
|
||||
subscriptions,
|
||||
subscriptionStreamState,
|
||||
logs,
|
||||
tasks,
|
||||
progressByTaskId,
|
||||
protocol,
|
||||
protocolEra,
|
||||
network,
|
||||
stderrLogs,
|
||||
toolCallState,
|
||||
getPromptState,
|
||||
readResourceState,
|
||||
toolsUi,
|
||||
promptsUi,
|
||||
resourcesUi,
|
||||
appsUi,
|
||||
tasksUi,
|
||||
logsUi,
|
||||
protocolUi,
|
||||
networkUi,
|
||||
consoleUi,
|
||||
currentLogLevel,
|
||||
sandboxPath,
|
||||
bridgeFactory,
|
||||
appRendererRef,
|
||||
pinnedProtocolIds,
|
||||
onToggleTheme,
|
||||
onOpenClientSettings,
|
||||
onToggleConnection,
|
||||
onDisconnect,
|
||||
onServerAdd,
|
||||
onServerImportConfig,
|
||||
onServerImportJson,
|
||||
onServerExport,
|
||||
onConnectionInfo,
|
||||
onServerSettings,
|
||||
onServerEdit,
|
||||
onServerClone,
|
||||
onServerRemove,
|
||||
onServerReorder,
|
||||
highlightedServerIds,
|
||||
onClearHighlight,
|
||||
serverSupportsTaskToolCalls,
|
||||
onToolsUiChange,
|
||||
onCallTool,
|
||||
onCancelToolCall,
|
||||
onClearToolResult,
|
||||
onReadResourceContents,
|
||||
onRefreshTools,
|
||||
toolsPagination,
|
||||
promptsPagination,
|
||||
resourcesPagination,
|
||||
onPromptsUiChange,
|
||||
onGetPrompt,
|
||||
onCopyPromptMessages,
|
||||
onRefreshPrompts,
|
||||
onResourcesUiChange,
|
||||
onReadResource,
|
||||
onSubscribeResource,
|
||||
onUnsubscribeResource,
|
||||
onRefreshResources,
|
||||
onCompleteArgument,
|
||||
completionsSupported,
|
||||
subscriptionsSupported,
|
||||
onTasksUiChange,
|
||||
onCancelTask,
|
||||
onClearCompletedTasks,
|
||||
onRefreshTasks,
|
||||
onSetLogLevel,
|
||||
modernLogLevel = null,
|
||||
onSetModernLogLevel,
|
||||
onLogsUiChange,
|
||||
onClearLogs,
|
||||
onExportLogs,
|
||||
onProtocolUiChange,
|
||||
onClearProtocol,
|
||||
onExportProtocol,
|
||||
onClearProtocolSection,
|
||||
onExportProtocolSection,
|
||||
onReplayProtocol,
|
||||
onTogglePinProtocol,
|
||||
onNetworkUiChange,
|
||||
onClearNetwork,
|
||||
onExportNetwork,
|
||||
onConsoleUiChange,
|
||||
onClearConsole,
|
||||
onExportConsole,
|
||||
onAppsUiChange,
|
||||
onSelectApp,
|
||||
onOpenApp,
|
||||
onCloseApp,
|
||||
onAppError,
|
||||
onRefreshApps,
|
||||
activeTab: activeTabProp,
|
||||
onActiveTabChange,
|
||||
shell,
|
||||
connection,
|
||||
servers: serverList,
|
||||
tools: toolsPanel,
|
||||
prompts: promptsPanel,
|
||||
resources: resourcesPanel,
|
||||
apps: appsPanel,
|
||||
tasks: tasksPanel,
|
||||
logs: logsPanel,
|
||||
protocol: protocolPanel,
|
||||
network: networkPanel,
|
||||
console: consolePanel,
|
||||
}: InspectorViewProps) {
|
||||
// Unpack the bundles back into the locals the body already uses, so the
|
||||
// grouping is confined to the prop boundary.
|
||||
const {
|
||||
deepLink,
|
||||
deepLinkStatus,
|
||||
version,
|
||||
malformedListItems = [],
|
||||
activeTab: activeTabProp,
|
||||
onActiveTabChange,
|
||||
onToggleTheme,
|
||||
onOpenClientSettings,
|
||||
} = shell;
|
||||
const {
|
||||
activeServer,
|
||||
erroredServerId,
|
||||
connectedServerId,
|
||||
connectionStatus,
|
||||
connectErrorMessage,
|
||||
initializeResult,
|
||||
latencyMs,
|
||||
protocolEra,
|
||||
onCompleteArgument,
|
||||
completionsSupported,
|
||||
onToggleConnection,
|
||||
onDisconnect,
|
||||
} = connection;
|
||||
const {
|
||||
servers: serversInput,
|
||||
serverListWritable = true,
|
||||
highlightedServerIds,
|
||||
onClearHighlight,
|
||||
onServerAdd,
|
||||
onServerImportConfig,
|
||||
onServerImportJson,
|
||||
onServerExport,
|
||||
onConnectionInfo,
|
||||
onServerSettings,
|
||||
onServerEdit,
|
||||
onServerClone,
|
||||
onServerRemove,
|
||||
onServerReorder,
|
||||
} = serverList;
|
||||
const {
|
||||
tools,
|
||||
excludedTools = [],
|
||||
toolsListChanged,
|
||||
toolsLoadError,
|
||||
toolsUi,
|
||||
toolCallState,
|
||||
toolsPagination,
|
||||
serverSupportsTaskToolCalls,
|
||||
onToolsUiChange,
|
||||
onCallTool,
|
||||
onCancelToolCall,
|
||||
onClearToolResult,
|
||||
onRefreshTools,
|
||||
onReadResourceContents,
|
||||
} = toolsPanel;
|
||||
const {
|
||||
prompts,
|
||||
promptsListChanged,
|
||||
promptsLoadError,
|
||||
promptsUi,
|
||||
getPromptState,
|
||||
promptsPagination,
|
||||
onPromptsUiChange,
|
||||
onGetPrompt,
|
||||
onCopyPromptMessages,
|
||||
onRefreshPrompts,
|
||||
} = promptsPanel;
|
||||
const {
|
||||
resources,
|
||||
resourceTemplates,
|
||||
subscriptions,
|
||||
subscriptionStreamState,
|
||||
subscriptionsSupported,
|
||||
resourcesListChanged,
|
||||
resourcesLoadError,
|
||||
resourcesUi,
|
||||
readResourceState,
|
||||
resourcesPagination,
|
||||
onResourcesUiChange,
|
||||
onReadResource,
|
||||
onSubscribeResource,
|
||||
onUnsubscribeResource,
|
||||
onRefreshResources,
|
||||
} = resourcesPanel;
|
||||
const {
|
||||
appsUi,
|
||||
sandboxPath,
|
||||
bridgeFactory,
|
||||
appRendererRef,
|
||||
onAppsUiChange,
|
||||
onSelectApp,
|
||||
onOpenApp,
|
||||
onCloseApp,
|
||||
onAppError,
|
||||
onRefreshApps,
|
||||
} = appsPanel;
|
||||
const {
|
||||
tasks,
|
||||
progressByTaskId,
|
||||
tasksUi,
|
||||
onTasksUiChange,
|
||||
onCancelTask,
|
||||
onClearCompletedTasks,
|
||||
onRefreshTasks,
|
||||
} = tasksPanel;
|
||||
const {
|
||||
logs,
|
||||
logsUi,
|
||||
currentLogLevel,
|
||||
modernLogLevel = null,
|
||||
onSetLogLevel,
|
||||
onSetModernLogLevel,
|
||||
onLogsUiChange,
|
||||
onClearLogs,
|
||||
onExportLogs,
|
||||
} = logsPanel;
|
||||
const {
|
||||
protocol,
|
||||
protocolUi,
|
||||
pinnedProtocolIds,
|
||||
onProtocolUiChange,
|
||||
onClearProtocol,
|
||||
onExportProtocol,
|
||||
onClearProtocolSection,
|
||||
onExportProtocolSection,
|
||||
onReplayProtocol,
|
||||
onTogglePinProtocol,
|
||||
} = protocolPanel;
|
||||
const {
|
||||
network,
|
||||
networkUi,
|
||||
onNetworkUiChange,
|
||||
onClearNetwork,
|
||||
onExportNetwork,
|
||||
} = networkPanel;
|
||||
const {
|
||||
stderrLogs,
|
||||
consoleUi,
|
||||
onConsoleUiChange,
|
||||
onClearConsole,
|
||||
onExportConsole,
|
||||
} = consolePanel;
|
||||
// UI-only state. Connection state, primitive lists, and all action
|
||||
// dispatching live in the parent; this component only owns view-local
|
||||
// toggles (sort direction, list compact). Tab selection is lifted (#1417).
|
||||
|
|
|
|||
379
clients/web/src/components/views/InspectorView/types.ts
Normal file
379
clients/web/src/components/views/InspectorView/types.ts
Normal file
|
|
@ -0,0 +1,379 @@
|
|||
// Domain prop bundles for `InspectorView` (#2130).
|
||||
//
|
||||
// The view used to take ~130 flat props, which made App.tsx's JSX a prop wall
|
||||
// rather than a component tree. Each bundle below groups one domain's data,
|
||||
// UI state, and actions into a single object, so the call site reads as the
|
||||
// tree it is and a hook can hand over exactly one bundle.
|
||||
//
|
||||
// Two conventions keep the change reviewable and keep it from rippling
|
||||
// downward:
|
||||
//
|
||||
// - **Field names are unchanged.** Every field carries the exact name it had
|
||||
// as a flat prop, so the bundles are a regrouping, not a rename.
|
||||
// - **The bundles stop here.** `InspectorView` destructures them back into
|
||||
// the same locals and passes them down to the screens as before, so
|
||||
// nothing below the view knows they exist.
|
||||
import type { Ref } from "react";
|
||||
import type {
|
||||
InitializeResult,
|
||||
LoggingLevel,
|
||||
Prompt,
|
||||
ProtocolEra,
|
||||
ReadResourceResult,
|
||||
Resource,
|
||||
ResourceTemplateType as ResourceTemplate,
|
||||
Task,
|
||||
Tool,
|
||||
} from "@modelcontextprotocol/client";
|
||||
import type { MalformedListItem } from "@inspector/core/mcp";
|
||||
import type {
|
||||
ConnectionStatus,
|
||||
ExcludedTool,
|
||||
FetchRequestEntry,
|
||||
InspectorResourceSubscription,
|
||||
MessageEntry,
|
||||
ResourceSubscriptionStreamState,
|
||||
ServerEntry,
|
||||
StderrLogEntry,
|
||||
} from "@inspector/core/mcp/types.js";
|
||||
import type { ReplayParamsOverride } from "../../../lib/protocolReplay";
|
||||
import type { DeepLink, DeepLinkParseStatus } from "../../../utils/deepLink";
|
||||
import type { ListPaginationControlsProps } from "../../elements/ListPaginationControls/ListPaginationControls";
|
||||
import type {
|
||||
AppRendererHandle,
|
||||
BridgeFactory,
|
||||
} from "../../elements/AppRenderer/AppRenderer";
|
||||
import type { LogEntryData } from "../../elements/LogEntry/LogEntry";
|
||||
import type { TaskProgress } from "../../groups/TaskCard/TaskCard";
|
||||
import type {
|
||||
ToolCallState,
|
||||
ToolsUiState,
|
||||
} from "../../screens/ToolsScreen/ToolsScreen";
|
||||
import type { AppsUiState } from "../../screens/AppsScreen/AppsScreen";
|
||||
import type {
|
||||
GetPromptState,
|
||||
PromptsUiState,
|
||||
} from "../../screens/PromptsScreen/PromptsScreen";
|
||||
import type {
|
||||
ReadResourceState,
|
||||
ResourcesUiState,
|
||||
} from "../../screens/ResourcesScreen/ResourcesScreen";
|
||||
import type { LogsUiState } from "../../screens/LoggingScreen/LoggingScreen";
|
||||
import type { TasksUiState } from "../../screens/TasksScreen/TasksScreen";
|
||||
import type { ProtocolUiState } from "../../screens/ProtocolScreen/ProtocolScreen";
|
||||
import type { NetworkUiState } from "../../screens/NetworkScreen/NetworkScreen";
|
||||
import type { ConsoleUiState } from "../../screens/ConsoleScreen/ConsoleScreen";
|
||||
|
||||
/**
|
||||
* App chrome and the few things that belong to no single screen: the active
|
||||
* tab, the theme/client-settings entry points, the footer version, the
|
||||
* deep-link parameters, and the malformed-list-item log that three separate
|
||||
* screens each filter for their own entries.
|
||||
*/
|
||||
export interface ShellProps {
|
||||
/**
|
||||
* Validated deep-link parameters from the page URL. When present and
|
||||
* `openApp` is set, the parent switches to the Apps tab and pre-selects that
|
||||
* app (with `appArgs` as the form values) once the connection is up and the
|
||||
* app list contains it. The connect itself is driven by the parent.
|
||||
*/
|
||||
deepLink?: DeepLink;
|
||||
/**
|
||||
* Outcome of parsing the initial-URL deep link, surfaced as `data-deeplink`
|
||||
* on the `connection-status` testid. Distinguishes "no deep link" from
|
||||
* "rejected" (token mismatch / bad serverUrl) — both leave `data-status`
|
||||
* idle, so an automated driver otherwise cannot tell them apart.
|
||||
*/
|
||||
deepLinkStatus?: DeepLinkParseStatus;
|
||||
/**
|
||||
* The Inspector build version (root `package.json`), shown at the left of the
|
||||
* footer row (#1682). Absent on a legacy backend that omits it — the version
|
||||
* label then renders nothing.
|
||||
*/
|
||||
version?: string;
|
||||
/**
|
||||
* Entries dropped from a list result as malformed, across every list method.
|
||||
* Each list panel filters for its own and warns about them (#1909). It spans
|
||||
* three screens, so it has no single domain owner.
|
||||
*/
|
||||
malformedListItems?: MalformedListItem[];
|
||||
/** Active inspector tab (lifted to App for OAuth resume). */
|
||||
activeTab: string;
|
||||
onActiveTabChange: (tab: string) => void;
|
||||
/**
|
||||
* Theme toggle (lives in the parent so the color scheme can also flow into
|
||||
* other top-level UI later).
|
||||
*/
|
||||
onToggleTheme: () => void;
|
||||
/** Open install-level client settings (client.json / EMA IdP). */
|
||||
onOpenClientSettings: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Live connection state and the capability facts more than one screen reads.
|
||||
* Driven by the parent via `useInspectorClient`.
|
||||
*/
|
||||
export interface ConnectionProps {
|
||||
activeServer?: string;
|
||||
/**
|
||||
* Id of the server whose last connection attempt failed (#1621). Its card in
|
||||
* the Servers screen draws a red border until another server is connected or
|
||||
* a new connection is attempted. Independent of `activeServer`, which the
|
||||
* parent clears on the failure's `disconnect` event.
|
||||
*
|
||||
* It is also the sole signal that opens the monitoring sidebar onto the
|
||||
* failure's diagnostics (#2108). That used to be gated on the `"error"`
|
||||
* connection status, which an OAuth failure never reaches — the parent drives
|
||||
* that leg and tears the client down itself, settling the session at
|
||||
* `"disconnected"`. So the parent must set this for *every* connect-attempt
|
||||
* failure, auth legs included, and clear it as each new attempt starts.
|
||||
*/
|
||||
erroredServerId?: string;
|
||||
/**
|
||||
* Id of the server that just connected successfully (#1682). Its card draws
|
||||
* the green highlight border and scrolls into view once the monitoring
|
||||
* sidebar has opened — the success mirror of `erroredServerId`.
|
||||
*/
|
||||
connectedServerId?: string;
|
||||
connectionStatus: ConnectionStatus;
|
||||
/**
|
||||
* Last connection-level error message (handshake failure, OAuth start
|
||||
* failure, deep-link automation failure). Surfaced as `data-error-message`
|
||||
* on the header's `connection-status` testid so an automated driver can read
|
||||
* *why* a connect failed without scraping a transient toast.
|
||||
*/
|
||||
connectErrorMessage?: string;
|
||||
initializeResult?: InitializeResult;
|
||||
latencyMs?: number;
|
||||
/** Negotiated protocol era (SEP §7.8), for the Protocol view's era badge. */
|
||||
protocolEra?: ProtocolEra;
|
||||
/**
|
||||
* Argument completion, shared by the Prompts and Resources screens — hence
|
||||
* here rather than in either one.
|
||||
*/
|
||||
onCompleteArgument?: (
|
||||
ref:
|
||||
| { type: "ref/resource"; uri: string }
|
||||
| { type: "ref/prompt"; name: string },
|
||||
argumentName: string,
|
||||
argumentValue: string,
|
||||
context: Record<string, string>,
|
||||
) => Promise<string[]>;
|
||||
completionsSupported?: boolean;
|
||||
// Connection lifecycle (dispatched to `useInspectorClient.connect/disconnect`).
|
||||
onToggleConnection: (id: string) => void;
|
||||
onDisconnect: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* The server catalog and every action on it. Static config only — the runtime
|
||||
* connection state comes from {@link ConnectionProps} and is merged into each
|
||||
* card by the view.
|
||||
*/
|
||||
export interface ServerListProps {
|
||||
servers: ServerEntry[];
|
||||
/**
|
||||
* Whether the server list is writable (catalog) or read-only (a `--config`
|
||||
* session file / ad-hoc launch). When false, the Servers screen hides all
|
||||
* catalog mutation controls. Defaults to true.
|
||||
*/
|
||||
serverListWritable?: boolean;
|
||||
/** Ids of freshly-added servers to highlight on the list (first is scrolled to). */
|
||||
highlightedServerIds?: string[];
|
||||
/** Clears the freshly-added highlight for a server (on click of its card). */
|
||||
onClearHighlight?: (id: string) => void;
|
||||
onServerAdd: () => void;
|
||||
onServerImportConfig: () => void;
|
||||
onServerImportJson: () => void;
|
||||
/** Download the current server list as a canonical `mcp.json` file. */
|
||||
onServerExport: () => void;
|
||||
onConnectionInfo: (id: string) => void;
|
||||
onServerSettings: (id: string) => void;
|
||||
onServerEdit: (id: string) => void;
|
||||
onServerClone: (id: string) => void;
|
||||
onServerRemove: (id: string) => void;
|
||||
/** Persist a new server ordering (drag-and-drop / keyboard reorder). */
|
||||
onServerReorder: (orderedIds: string[]) => void;
|
||||
}
|
||||
|
||||
/** The Tools screen: its list, lifted UI state, and actions. */
|
||||
export interface ToolsPanelProps {
|
||||
tools: Tool[];
|
||||
/**
|
||||
* Tools excluded from `tools/list` for invalid `x-mcp-header` annotations
|
||||
* (SEP-2243); shown in the Tools sidebar with the reason (#1632).
|
||||
*/
|
||||
excludedTools?: ExcludedTool[];
|
||||
/** "List changed since last refresh", from the managed-state layer (#1402). */
|
||||
toolsListChanged: boolean;
|
||||
/**
|
||||
* Last list-load failure, rendered above the sidebar list so a failed load
|
||||
* (including the connect-time one) can't read as "this server has none"
|
||||
* (#1953).
|
||||
*/
|
||||
toolsLoadError?: Error | null;
|
||||
/**
|
||||
* Lifted selection / search / filter state. Owned by the parent so it
|
||||
* persists across tab navigation within a live session — the screens unmount
|
||||
* on tab switch, so screen-local state would be lost (#1417).
|
||||
*/
|
||||
toolsUi: ToolsUiState;
|
||||
/** Panel-level "operation in flight" state. */
|
||||
toolCallState?: ToolCallState;
|
||||
/** Pagination controls for the Tools list (#1721). */
|
||||
toolsPagination: ListPaginationControlsProps;
|
||||
/** Whether the connected server advertises task-augmented tool calls. */
|
||||
serverSupportsTaskToolCalls: boolean;
|
||||
onToolsUiChange: (next: ToolsUiState) => void;
|
||||
onCallTool: (
|
||||
name: string,
|
||||
args: Record<string, unknown>,
|
||||
runAsTask?: boolean,
|
||||
) => void;
|
||||
onCancelToolCall?: () => void;
|
||||
onClearToolResult?: () => void;
|
||||
onRefreshTools: () => void;
|
||||
/**
|
||||
* Read-on-demand handler for `resource_link` blocks in a tool result.
|
||||
* Returns the linked resource's contents so the result panel can inline them.
|
||||
*/
|
||||
onReadResourceContents?: (uri: string) => Promise<ReadResourceResult>;
|
||||
}
|
||||
|
||||
/** The Prompts screen: its list, lifted UI state, and actions. */
|
||||
export interface PromptsPanelProps {
|
||||
prompts: Prompt[];
|
||||
promptsListChanged: boolean;
|
||||
promptsLoadError?: Error | null;
|
||||
promptsUi: PromptsUiState;
|
||||
getPromptState?: GetPromptState;
|
||||
/** Pagination controls for the Prompts list (#1721). */
|
||||
promptsPagination: ListPaginationControlsProps;
|
||||
onPromptsUiChange: (next: PromptsUiState) => void;
|
||||
onGetPrompt: (name: string, args: Record<string, string>) => void;
|
||||
onCopyPromptMessages?: () => void;
|
||||
onRefreshPrompts: () => void;
|
||||
}
|
||||
|
||||
/** The Resources screen: resources, templates, subscriptions, and actions. */
|
||||
export interface ResourcesPanelProps {
|
||||
resources: Resource[];
|
||||
resourceTemplates: ResourceTemplate[];
|
||||
subscriptions: InspectorResourceSubscription[];
|
||||
/**
|
||||
* Modern-era `subscriptions/listen` stream state (#1630). Drives the
|
||||
* Resources screen's stream badge/dot; `active: false` (or omitted) on the
|
||||
* legacy era.
|
||||
*/
|
||||
subscriptionStreamState?: ResourceSubscriptionStreamState;
|
||||
/**
|
||||
* Whether the connected server advertises the `resources.subscribe`
|
||||
* capability. When false, the Resources screen hides the Subscribe/
|
||||
* Unsubscribe button and the Subscriptions accordion section.
|
||||
*/
|
||||
subscriptionsSupported?: boolean;
|
||||
resourcesListChanged: boolean;
|
||||
/**
|
||||
* The Resources sidebar lists resources AND templates behind a single
|
||||
* Refresh, so App passes whichever of the two loads failed.
|
||||
*/
|
||||
resourcesLoadError?: Error | null;
|
||||
resourcesUi: ResourcesUiState;
|
||||
readResourceState?: ReadResourceState;
|
||||
/** Pagination controls for the Resources list (#1721). */
|
||||
resourcesPagination: ListPaginationControlsProps;
|
||||
onResourcesUiChange: (next: ResourcesUiState) => void;
|
||||
onReadResource: (uri: string) => void;
|
||||
onSubscribeResource: (uri: string) => void;
|
||||
onUnsubscribeResource: (uri: string) => void;
|
||||
onRefreshResources: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* The Apps screen and its sandbox wiring. The parent's web environment
|
||||
* provides the sandbox iframe URL (undefined when the sandbox controller is
|
||||
* unavailable), the per-app bridge factory, and the renderer handle the parent
|
||||
* uses to push tool input/result into the running app and tear it down.
|
||||
*/
|
||||
export interface AppsPanelProps {
|
||||
appsUi: AppsUiState;
|
||||
sandboxPath?: string;
|
||||
bridgeFactory: BridgeFactory;
|
||||
appRendererRef: Ref<AppRendererHandle>;
|
||||
onAppsUiChange: (next: AppsUiState) => void;
|
||||
onSelectApp: (name: string) => void;
|
||||
onOpenApp: (name: string, args: Record<string, unknown>) => void;
|
||||
onCloseApp: () => void;
|
||||
onAppError: (err: Error) => void;
|
||||
onRefreshApps: () => void;
|
||||
}
|
||||
|
||||
/** The Tasks monitor: the task list, its progress map, and actions. */
|
||||
export interface TasksPanelProps {
|
||||
tasks: Task[];
|
||||
progressByTaskId?: Record<string, TaskProgress>;
|
||||
tasksUi: TasksUiState;
|
||||
onTasksUiChange: (next: TasksUiState) => void;
|
||||
onCancelTask: (taskId: string) => void;
|
||||
onClearCompletedTasks: () => void;
|
||||
onRefreshTasks: () => void;
|
||||
}
|
||||
|
||||
/** The Logs monitor, including both eras' log-level controls. */
|
||||
export interface LogsPanelProps {
|
||||
logs: LogEntryData[];
|
||||
logsUi: LogsUiState;
|
||||
/**
|
||||
* The MCP `logging/setLevel` request has no echo notification, so the parent
|
||||
* keeps the optimistic current value.
|
||||
*/
|
||||
currentLogLevel: LoggingLevel;
|
||||
/**
|
||||
* Modern-era per-request log level currently stamped, or `null` when opted
|
||||
* out (#1629). On modern connections the Logs sidebar shows a per-request
|
||||
* opt-in control instead of the legacy `logging/setLevel` selector.
|
||||
*/
|
||||
modernLogLevel?: LoggingLevel | null;
|
||||
onSetLogLevel: (level: LoggingLevel) => void;
|
||||
/** Set (or clear, with `null`) the modern per-request log level. */
|
||||
onSetModernLogLevel?: (level: LoggingLevel | null) => void;
|
||||
onLogsUiChange: (next: LogsUiState) => void;
|
||||
onClearLogs: () => void;
|
||||
onExportLogs: () => void;
|
||||
}
|
||||
|
||||
/** The Protocol monitor: the message log, pinning, replay, and exports. */
|
||||
export interface ProtocolPanelProps {
|
||||
protocol: MessageEntry[];
|
||||
protocolUi: ProtocolUiState;
|
||||
/**
|
||||
* Pinned message ids. Optional because pin state isn't persisted yet (#1244
|
||||
* is single-PR; persistence is a separate concern).
|
||||
*/
|
||||
pinnedProtocolIds?: Set<string>;
|
||||
onProtocolUiChange: (next: ProtocolUiState) => void;
|
||||
onClearProtocol: () => void;
|
||||
onExportProtocol: () => void;
|
||||
onClearProtocolSection: (section: "pinned" | "history") => void;
|
||||
onExportProtocolSection: (section: "pinned" | "history") => void;
|
||||
onReplayProtocol: (id: string, overrideParams?: ReplayParamsOverride) => void;
|
||||
onTogglePinProtocol: (id: string) => void;
|
||||
}
|
||||
|
||||
/** The Network monitor: captured HTTP traffic and its actions. */
|
||||
export interface NetworkPanelProps {
|
||||
network: FetchRequestEntry[];
|
||||
networkUi: NetworkUiState;
|
||||
onNetworkUiChange: (next: NetworkUiState) => void;
|
||||
onClearNetwork: () => void;
|
||||
onExportNetwork: () => void;
|
||||
}
|
||||
|
||||
/** The Console monitor: captured stdio stderr. Empty for HTTP servers (#1621). */
|
||||
export interface ConsolePanelProps {
|
||||
stderrLogs: StderrLogEntry[];
|
||||
consoleUi: ConsoleUiState;
|
||||
onConsoleUiChange: (next: ConsoleUiState) => void;
|
||||
onClearConsole: () => void;
|
||||
onExportConsole: () => void;
|
||||
}
|
||||
Loading…
Reference in a new issue