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 /
|
// that invoke the App's connect / call-tool / get-prompt / read-resource /
|
||||||
// set-log-level handlers.
|
// set-log-level handlers.
|
||||||
vi.mock("./components/views/InspectorView/InspectorView", () => ({
|
vi.mock("./components/views/InspectorView/InspectorView", () => ({
|
||||||
InspectorView: (props: {
|
InspectorView: (props: InspectorViewProps) => (
|
||||||
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;
|
|
||||||
};
|
|
||||||
}) => (
|
|
||||||
<div>
|
<div>
|
||||||
<span data-testid="tool-status">
|
<span data-testid="tool-status">
|
||||||
{props.toolCallState?.status ?? "none"}
|
{props.tools.toolCallState?.status ?? "none"}
|
||||||
</span>
|
</span>
|
||||||
<span data-testid="task-progress-keys">
|
<span data-testid="task-progress-keys">
|
||||||
{Object.keys(props.progressByTaskId ?? {}).join(",") || "none"}
|
{Object.keys(props.tasks.progressByTaskId ?? {}).join(",") || "none"}
|
||||||
</span>
|
</span>
|
||||||
<span data-testid="selected-tool">
|
<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>
|
||||||
<span data-testid="tool-search">{props.toolsUi?.search || "none"}</span>
|
|
||||||
<span data-testid="selected-prompt">
|
<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>
|
||||||
<span data-testid="log-filter">{props.logsUi?.filterText || "none"}</span>
|
|
||||||
<span data-testid="prompt-status">
|
<span data-testid="prompt-status">
|
||||||
{props.getPromptState?.status ?? "none"}
|
{props.prompts.getPromptState?.status ?? "none"}
|
||||||
</span>
|
</span>
|
||||||
<span data-testid="resource-status">
|
<span data-testid="resource-status">
|
||||||
{props.readResourceState?.status ?? "none"}
|
{props.resources.readResourceState?.status ?? "none"}
|
||||||
</span>
|
</span>
|
||||||
<span data-testid="log-level">{props.currentLogLevel}</span>
|
<span data-testid="log-level">{props.logs.currentLogLevel}</span>
|
||||||
<span data-testid="active-tab">{props.activeTab ?? "none"}</span>
|
<span data-testid="active-tab">{props.shell.activeTab ?? "none"}</span>
|
||||||
<span data-testid="init-result">
|
<span data-testid="init-result">
|
||||||
{props.initializeResult
|
{props.connection.initializeResult
|
||||||
? `name:${props.initializeResult.serverInfo.name || "(empty)"}`
|
? `name:${props.connection.initializeResult.serverInfo.name || "(empty)"}`
|
||||||
: "none"}
|
: "none"}
|
||||||
</span>
|
</span>
|
||||||
<span data-testid="errored-server">
|
<span data-testid="errored-server">
|
||||||
{props.erroredServerId ?? "none"}
|
{props.connection.erroredServerId ?? "none"}
|
||||||
</span>
|
</span>
|
||||||
<span data-testid="active-server">{props.activeServer ?? "none"}</span>
|
<span data-testid="active-server">
|
||||||
<button onClick={() => props.onActiveTabChange("Servers")}>
|
{props.connection.activeServer ?? "none"}
|
||||||
|
</span>
|
||||||
|
<button onClick={() => props.shell.onActiveTabChange("Servers")}>
|
||||||
switch-servers-tab
|
switch-servers-tab
|
||||||
</button>
|
</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). */}
|
{/* 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.connection.onToggleConnection("B")}>
|
||||||
<button onClick={() => props.onConnectionInfo()}>
|
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
|
open-connection-info
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
props.onToolsUiChange({
|
props.tools.onToolsUiChange({
|
||||||
formValues: {},
|
...props.tools.toolsUi,
|
||||||
search: "",
|
|
||||||
...props.toolsUi,
|
|
||||||
selectedToolKey: "0:get_acts",
|
selectedToolKey: "0:get_acts",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -574,10 +519,8 @@ vi.mock("./components/views/InspectorView/InspectorView", () => ({
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
props.onToolsUiChange({
|
props.tools.onToolsUiChange({
|
||||||
formValues: {},
|
...props.tools.toolsUi,
|
||||||
search: "",
|
|
||||||
...props.toolsUi,
|
|
||||||
selectedToolKey: "1:other_tool",
|
selectedToolKey: "1:other_tool",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -586,9 +529,8 @@ vi.mock("./components/views/InspectorView/InspectorView", () => ({
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
props.onToolsUiChange({
|
props.tools.onToolsUiChange({
|
||||||
formValues: {},
|
...props.tools.toolsUi,
|
||||||
...props.toolsUi,
|
|
||||||
search: "act",
|
search: "act",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -597,10 +539,8 @@ vi.mock("./components/views/InspectorView/InspectorView", () => ({
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
props.onPromptsUiChange({
|
props.prompts.onPromptsUiChange({
|
||||||
argumentValues: {},
|
...props.prompts.promptsUi,
|
||||||
search: "",
|
|
||||||
...props.promptsUi,
|
|
||||||
selectedPromptName: "greet",
|
selectedPromptName: "greet",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -609,70 +549,99 @@ vi.mock("./components/views/InspectorView/InspectorView", () => ({
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
props.onLogsUiChange({
|
props.logs.onLogsUiChange({
|
||||||
visibleLevels: {},
|
...props.logs.logsUi,
|
||||||
...props.logsUi,
|
|
||||||
filterText: "err",
|
filterText: "err",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
set-log-filter
|
set-log-filter
|
||||||
</button>
|
</button>
|
||||||
<button onClick={() => props.onCallTool("get_acts", {})}>call</button>
|
<button onClick={() => props.tools.onCallTool("get_acts", {})}>
|
||||||
<button onClick={() => props.onCallTool("get_acts", {}, true)}>
|
call
|
||||||
|
</button>
|
||||||
|
<button onClick={() => props.tools.onCallTool("get_acts", {}, true)}>
|
||||||
call-as-task
|
call-as-task
|
||||||
</button>
|
</button>
|
||||||
<button onClick={() => props.onCancelTask("task-1")}>cancel-task</button>
|
<button onClick={() => props.tasks.onCancelTask("task-1")}>
|
||||||
<button onClick={() => props.onCancelToolCall()}>cancel-tool-call</button>
|
cancel-task
|
||||||
<button onClick={() => props.onClearCompletedTasks()}>
|
</button>
|
||||||
|
<button onClick={() => props.tools.onCancelToolCall?.()}>
|
||||||
|
cancel-tool-call
|
||||||
|
</button>
|
||||||
|
<button onClick={() => props.tasks.onClearCompletedTasks()}>
|
||||||
clear-completed
|
clear-completed
|
||||||
</button>
|
</button>
|
||||||
<button onClick={() => props.onRefreshTasks()}>refresh-tasks</button>
|
<button onClick={() => props.tasks.onRefreshTasks()}>
|
||||||
<button onClick={() => props.onGetPrompt("greet", {})}>get-prompt</button>
|
refresh-tasks
|
||||||
<button onClick={() => props.onReadResource("res://x")}>
|
</button>
|
||||||
|
<button onClick={() => props.prompts.onGetPrompt("greet", {})}>
|
||||||
|
get-prompt
|
||||||
|
</button>
|
||||||
|
<button onClick={() => props.resources.onReadResource("res://x")}>
|
||||||
read-resource
|
read-resource
|
||||||
</button>
|
</button>
|
||||||
<button onClick={() => props.onSetLogLevel("debug")}>set-level</button>
|
<button onClick={() => props.logs.onSetLogLevel("debug")}>
|
||||||
<button onClick={() => props.onServerSettings("A")}>open-settings</button>
|
set-level
|
||||||
|
</button>
|
||||||
|
<button onClick={() => props.servers.onServerSettings("A")}>
|
||||||
|
open-settings
|
||||||
|
</button>
|
||||||
{/* The real server grid (and its Add / Edit controls) lives inside this
|
{/* The real server grid (and its Add / Edit controls) lives inside this
|
||||||
mocked view, so the config modal is only reachable through these
|
mocked view, so the config modal is only reachable through these
|
||||||
callbacks — and the highlight batch only observable through this prop. */}
|
callbacks — and the highlight batch only observable through this prop. */}
|
||||||
<button onClick={() => props.onServerEdit("A")}>edit-server</button>
|
<button onClick={() => props.servers.onServerEdit("A")}>
|
||||||
<button onClick={() => props.onServerAdd()}>add-server</button>
|
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">
|
<span data-testid="highlighted-servers">
|
||||||
{(props.highlightedServerIds ?? []).join(",") || "none"}
|
{(props.servers.highlightedServerIds ?? []).join(",") || "none"}
|
||||||
</span>
|
</span>
|
||||||
<span data-testid="pinned-history">
|
<span data-testid="pinned-history">
|
||||||
{Array.from(props.pinnedProtocolIds ?? []).join(",")}
|
{Array.from(props.protocol.pinnedProtocolIds ?? []).join(",")}
|
||||||
</span>
|
</span>
|
||||||
<button onClick={() => props.onTogglePinProtocol("hist-1")}>
|
<button onClick={() => props.protocol.onTogglePinProtocol("hist-1")}>
|
||||||
toggle-pin
|
toggle-pin
|
||||||
</button>
|
</button>
|
||||||
<button onClick={() => props.onReplayProtocol("hist-1")}>
|
<button onClick={() => props.protocol.onReplayProtocol("hist-1")}>
|
||||||
replay-history
|
replay-history
|
||||||
</button>
|
</button>
|
||||||
<button onClick={() => props.onClearProtocol()}>clear-history</button>
|
<button onClick={() => props.protocol.onClearProtocol()}>
|
||||||
|
clear-history
|
||||||
|
</button>
|
||||||
<span data-testid="tools-paginated">
|
<span data-testid="tools-paginated">
|
||||||
{String(props.toolsPagination.paginated)}
|
{String(props.tools.toolsPagination.paginated)}
|
||||||
</span>
|
</span>
|
||||||
<span data-testid="tools-loaded-pages">
|
<span data-testid="tools-loaded-pages">
|
||||||
{props.toolsPagination.loadedPages}
|
{props.tools.toolsPagination.loadedPages}
|
||||||
</span>
|
</span>
|
||||||
<button onClick={() => props.toolsPagination.onPaginatedChange(true)}>
|
<button
|
||||||
|
onClick={() => props.tools.toolsPagination.onPaginatedChange(true)}
|
||||||
|
>
|
||||||
paginated-on
|
paginated-on
|
||||||
</button>
|
</button>
|
||||||
<button onClick={() => props.toolsPagination.onPaginatedChange(false)}>
|
<button
|
||||||
|
onClick={() => props.tools.toolsPagination.onPaginatedChange(false)}
|
||||||
|
>
|
||||||
paginated-off
|
paginated-off
|
||||||
</button>
|
</button>
|
||||||
<button onClick={() => props.toolsPagination.onLoadMore()}>
|
<button onClick={() => props.tools.toolsPagination.onLoadMore()}>
|
||||||
load-more-tools
|
load-more-tools
|
||||||
</button>
|
</button>
|
||||||
<button onClick={() => props.onRefreshTools()}>refresh-tools</button>
|
<button onClick={() => props.tools.onRefreshTools()}>
|
||||||
|
refresh-tools
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import App from "./App";
|
import App from "./App";
|
||||||
|
import type { InspectorViewProps } from "./components/views/InspectorView/InspectorView";
|
||||||
import { SERVER_INFO_NOT_REPORTED_LABEL } from "./components/groups/ConnectionInfoContent/ConnectionInfoContent";
|
import { SERVER_INFO_NOT_REPORTED_LABEL } from "./components/groups/ConnectionInfoContent/ConnectionInfoContent";
|
||||||
import { OAUTH_CALLBACK_PATH } from "./utils/oauthFlow.js";
|
import { OAUTH_CALLBACK_PATH } from "./utils/oauthFlow.js";
|
||||||
import { INSPECTOR_SERVERS_TAB } from "./utils/inspectorTabs.js";
|
import { INSPECTOR_SERVERS_TAB } from "./utils/inspectorTabs.js";
|
||||||
|
|
@ -3842,6 +3811,30 @@ describe("App background command rejections (#2049)", () => {
|
||||||
return user;
|
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 () => {
|
it("does not leak an unhandled rejection when an all-pages Refresh fails", async () => {
|
||||||
vi.mocked(useManagedTools).mockReturnValue({
|
vi.mocked(useManagedTools).mockReturnValue({
|
||||||
tools: [],
|
tools: [],
|
||||||
|
|
@ -3919,6 +3912,156 @@ describe("App background command rejections (#2049)", () => {
|
||||||
rejections.stop();
|
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. */
|
/** 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 { refreshingPersist } from "./lib/refreshingPersist";
|
||||||
import { usePendingClientRequests } from "@inspector/core/react/usePendingClientRequests.js";
|
import { usePendingClientRequests } from "@inspector/core/react/usePendingClientRequests.js";
|
||||||
import { InspectorView } from "./components/views/InspectorView/InspectorView";
|
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 type { ReadResourceState } from "./components/screens/ResourcesScreen/ResourcesScreen";
|
||||||
import { AppElicitationHost } from "./components/elements/AppElicitation/AppElicitationHost";
|
import { AppElicitationHost } from "./components/elements/AppElicitation/AppElicitationHost";
|
||||||
import type { LogEntryData } from "./components/elements/LogEntry/LogEntry";
|
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 { OutputValidationToastMessage } from "./components/elements/Toasts/OutputValidationToastMessage";
|
||||||
import { ReAuthBannerBar } from "./components/groups/ReAuthBanner/ReAuthBannerBar";
|
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() {
|
function App() {
|
||||||
const { onToggleTheme } = useThemeToggle();
|
const { onToggleTheme } = useThemeToggle();
|
||||||
|
|
||||||
|
|
@ -1469,6 +1498,304 @@ function App() {
|
||||||
[pendingElicitations, inspectorClient],
|
[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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Box>
|
<Box>
|
||||||
|
|
@ -1484,171 +1811,18 @@ function App() {
|
||||||
</ReAuthBannerBar>
|
</ReAuthBannerBar>
|
||||||
) : null}
|
) : null}
|
||||||
<InspectorView
|
<InspectorView
|
||||||
deepLink={deepLink}
|
shell={shellProps}
|
||||||
deepLinkStatus={deepLinkStatus}
|
connection={connectionProps}
|
||||||
servers={servers}
|
servers={serverListProps}
|
||||||
serverListWritable={serverListWritable}
|
tools={toolsPanelProps}
|
||||||
activeServer={activeServerId}
|
prompts={promptsPanelProps}
|
||||||
erroredServerId={failedServerId}
|
resources={resourcesPanelProps}
|
||||||
connectedServerId={connectedServerId}
|
apps={appsPanelProps}
|
||||||
version={inspectorVersion}
|
tasks={tasksPanelProps}
|
||||||
connectionStatus={connectionStatus}
|
logs={logsPanelProps}
|
||||||
connectErrorMessage={connectErrorMessage}
|
protocol={protocolPanelProps}
|
||||||
initializeResult={initializeResult}
|
network={networkPanelProps}
|
||||||
latencyMs={latencyMs}
|
console={consolePanelProps}
|
||||||
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}
|
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<AppElicitationHost
|
<AppElicitationHost
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,20 @@ import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { expect, fn } from "storybook/test";
|
import { expect, fn } from "storybook/test";
|
||||||
import { InspectorView } from "./InspectorView";
|
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 { noopPagination } from "../../../test/fixtures/pagination";
|
||||||
import {
|
import {
|
||||||
EMPTY_TOOLS_UI,
|
EMPTY_TOOLS_UI,
|
||||||
|
|
@ -324,116 +338,164 @@ const demoInitializeResult: InitializeResult = {
|
||||||
serverInfo: { name: "Local Dev Server", version: "1.2.0" },
|
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> = {
|
const meta: Meta<typeof InspectorView> = {
|
||||||
title: "Views/InspectorView",
|
title: "Views/InspectorView",
|
||||||
component: InspectorView,
|
component: InspectorView,
|
||||||
parameters: { layout: "fullscreen" },
|
parameters: { layout: "fullscreen" },
|
||||||
args: {
|
args: {
|
||||||
// Data
|
shell: shellArgs,
|
||||||
servers: demoServers,
|
connection: connectionArgs,
|
||||||
tools: demoTools,
|
servers: serversArgs,
|
||||||
prompts: demoPrompts,
|
tools: toolsArgs,
|
||||||
resources: demoResources,
|
prompts: promptsArgs,
|
||||||
resourceTemplates: demoResourceTemplates,
|
resources: resourcesArgs,
|
||||||
subscriptions: demoSubscriptions,
|
apps: appsArgs,
|
||||||
logs: demoLogs,
|
tasks: tasksArgs,
|
||||||
tasks: demoTasks,
|
logs: logsArgs,
|
||||||
progressByTaskId: demoProgressByTaskId,
|
protocol: protocolArgs,
|
||||||
protocol: demoHistory,
|
network: networkArgs,
|
||||||
network: demoNetwork,
|
console: consoleArgs,
|
||||||
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(),
|
|
||||||
},
|
},
|
||||||
render: (args) => {
|
render: (args) => {
|
||||||
const [activeTab, setActiveTab] = useState(args.activeTab ?? "Servers");
|
const [activeTab, setActiveTab] = useState(args.shell.activeTab);
|
||||||
return (
|
return (
|
||||||
<InspectorView
|
<InspectorView
|
||||||
{...args}
|
{...args}
|
||||||
activeTab={activeTab}
|
shell={{ ...args.shell, activeTab, onActiveTabChange: setActiveTab }}
|
||||||
onActiveTabChange={setActiveTab}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -463,7 +525,7 @@ export const Default: Story = {
|
||||||
|
|
||||||
export const NoServers: Story = {
|
export const NoServers: Story = {
|
||||||
args: {
|
args: {
|
||||||
servers: [],
|
servers: { ...serversArgs, servers: [] },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -480,7 +542,7 @@ const manyServers: ServerEntry[] = Array.from({ length: 24 }, (_, i) => ({
|
||||||
|
|
||||||
export const ManyServers: Story = {
|
export const ManyServers: Story = {
|
||||||
args: {
|
args: {
|
||||||
servers: manyServers,
|
servers: { ...serversArgs, servers: manyServers },
|
||||||
},
|
},
|
||||||
play: async ({ canvasElement }) => {
|
play: async ({ canvasElement }) => {
|
||||||
// Even under enough content to overflow, the shell stays viewport-clamped
|
// Even under enough content to overflow, the shell stays viewport-clamped
|
||||||
|
|
@ -509,10 +571,13 @@ export const ManyServers: Story = {
|
||||||
// regression / storybook play function coverage.
|
// regression / storybook play function coverage.
|
||||||
export const Connected: Story = {
|
export const Connected: Story = {
|
||||||
args: {
|
args: {
|
||||||
activeServer: demoServers[0]!.id,
|
connection: {
|
||||||
connectionStatus: "connected",
|
...connectionArgs,
|
||||||
initializeResult: demoInitializeResult,
|
activeServer: demoServers[0]!.id,
|
||||||
latencyMs: 142,
|
connectionStatus: "connected",
|
||||||
|
initializeResult: demoInitializeResult,
|
||||||
|
latencyMs: 142,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -524,10 +589,13 @@ export const ConnectionError: Story = {
|
||||||
// connect-attempt-failure signal that gates the failure column; `network: []`
|
// connect-attempt-failure signal that gates the failure column; `network: []`
|
||||||
// (and empty Protocol) keep it a pure stdio failure so only Console is offered.
|
// (and empty Protocol) keep it a pure stdio failure so only Console is offered.
|
||||||
args: {
|
args: {
|
||||||
activeServer: demoServers[0]!.id,
|
connection: {
|
||||||
erroredServerId: demoServers[0]!.id,
|
...connectionArgs,
|
||||||
connectionStatus: "error",
|
activeServer: demoServers[0]!.id,
|
||||||
network: [],
|
erroredServerId: demoServers[0]!.id,
|
||||||
stderrLogs: demoStderr,
|
connectionStatus: "error",
|
||||||
|
},
|
||||||
|
network: { ...networkArgs, network: [] },
|
||||||
|
console: { ...consoleArgs, stderrLogs: demoStderr },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,14 +1,4 @@
|
||||||
import {
|
import { useEffect, useMemo, useRef, useState, type ReactNode } from "react";
|
||||||
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 {
|
import {
|
||||||
AppShell,
|
AppShell,
|
||||||
Flex,
|
Flex,
|
||||||
|
|
@ -18,81 +8,38 @@ import {
|
||||||
type MantineTransition,
|
type MantineTransition,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { useLocalStorage } from "@mantine/hooks";
|
import { useLocalStorage } from "@mantine/hooks";
|
||||||
import type {
|
import type { Implementation, Tool } from "@modelcontextprotocol/client";
|
||||||
Implementation,
|
import type { ServerEntry } from "@inspector/core/mcp/types.js";
|
||||||
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 { isTerminalStatus } from "@inspector/core/mcp/types.js";
|
import { isTerminalStatus } from "@inspector/core/mcp/types.js";
|
||||||
import { isAppTool } from "@inspector/core/mcp/apps.js";
|
import { isAppTool } from "@inspector/core/mcp/apps.js";
|
||||||
import { TASKS_EXTENSION_KEY } from "@inspector/core/mcp/modernTaskSchemas.js";
|
import { TASKS_EXTENSION_KEY } from "@inspector/core/mcp/modernTaskSchemas.js";
|
||||||
import { ViewHeader } from "../../groups/ViewHeader/ViewHeader";
|
import { ViewHeader } from "../../groups/ViewHeader/ViewHeader";
|
||||||
import { VersionBadge } from "../../elements/VersionBadge/VersionBadge";
|
import { VersionBadge } from "../../elements/VersionBadge/VersionBadge";
|
||||||
import { CopyrightBadge } from "../../elements/CopyrightBadge/CopyrightBadge";
|
import { CopyrightBadge } from "../../elements/CopyrightBadge/CopyrightBadge";
|
||||||
import type { ListPaginationControlsProps } from "../../elements/ListPaginationControls/ListPaginationControls";
|
|
||||||
import { ServerListScreen } from "../../screens/ServerListScreen/ServerListScreen";
|
import { ServerListScreen } from "../../screens/ServerListScreen/ServerListScreen";
|
||||||
import {
|
import { ToolsScreen } from "../../screens/ToolsScreen/ToolsScreen";
|
||||||
ToolsScreen,
|
import { AppsScreen } from "../../screens/AppsScreen/AppsScreen";
|
||||||
type ToolCallState,
|
import { PromptsScreen } from "../../screens/PromptsScreen/PromptsScreen";
|
||||||
type ToolsUiState,
|
import { ResourcesScreen } from "../../screens/ResourcesScreen/ResourcesScreen";
|
||||||
} from "../../screens/ToolsScreen/ToolsScreen";
|
import { LoggingScreen } from "../../screens/LoggingScreen/LoggingScreen";
|
||||||
import {
|
import { TasksScreen } from "../../screens/TasksScreen/TasksScreen";
|
||||||
AppsScreen,
|
import { ProtocolScreen } from "../../screens/ProtocolScreen/ProtocolScreen";
|
||||||
type AppsUiState,
|
import { NetworkScreen } from "../../screens/NetworkScreen/NetworkScreen";
|
||||||
} from "../../screens/AppsScreen/AppsScreen";
|
import { ConsoleScreen } from "../../screens/ConsoleScreen/ConsoleScreen";
|
||||||
import type {
|
import type {
|
||||||
AppRendererHandle,
|
AppsPanelProps,
|
||||||
BridgeFactory,
|
ConnectionProps,
|
||||||
} from "../../elements/AppRenderer/AppRenderer";
|
ConsolePanelProps,
|
||||||
import {
|
LogsPanelProps,
|
||||||
PromptsScreen,
|
NetworkPanelProps,
|
||||||
type GetPromptState,
|
PromptsPanelProps,
|
||||||
type PromptsUiState,
|
ProtocolPanelProps,
|
||||||
} from "../../screens/PromptsScreen/PromptsScreen";
|
ResourcesPanelProps,
|
||||||
import {
|
ServerListProps,
|
||||||
ResourcesScreen,
|
ShellProps,
|
||||||
type ReadResourceState,
|
TasksPanelProps,
|
||||||
type ResourcesUiState,
|
ToolsPanelProps,
|
||||||
} from "../../screens/ResourcesScreen/ResourcesScreen";
|
} from "./types";
|
||||||
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";
|
|
||||||
import type { SortDirection } from "../../elements/SortToggle/SortToggle";
|
import type { SortDirection } from "../../elements/SortToggle/SortToggle";
|
||||||
import { ScreenStage } from "../../elements/ScreenStage/ScreenStage";
|
import { ScreenStage } from "../../elements/ScreenStage/ScreenStage";
|
||||||
import { MonitoringScreen } from "../../groups/MonitoringScreen/MonitoringScreen";
|
import { MonitoringScreen } from "../../groups/MonitoringScreen/MonitoringScreen";
|
||||||
|
|
@ -360,402 +307,193 @@ const MonitorColumnGroup = Group.withProps({
|
||||||
flex: "0 0 auto",
|
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 {
|
export interface InspectorViewProps {
|
||||||
/**
|
/** App chrome: active tab, theme/settings entry points, footer, deep link. */
|
||||||
* Validated deep-link parameters from the page URL. When present and
|
shell: ShellProps;
|
||||||
* `openApp` is set, the parent switches to the Apps tab and pre-selects that
|
/** Live connection state plus the capability facts several screens read. */
|
||||||
* app (with `appArgs` as the form values) once the connection is up and the
|
connection: ConnectionProps;
|
||||||
* app list contains it. The connect itself is driven by the parent.
|
/** The server catalog and every action on it. */
|
||||||
*/
|
servers: ServerListProps;
|
||||||
deepLink?: DeepLink;
|
tools: ToolsPanelProps;
|
||||||
/**
|
prompts: PromptsPanelProps;
|
||||||
* Outcome of parsing the initial-URL deep link, surfaced as `data-deeplink`
|
resources: ResourcesPanelProps;
|
||||||
* on the `connection-status` testid. Distinguishes "no deep link" from
|
apps: AppsPanelProps;
|
||||||
* "rejected" (token mismatch / bad serverUrl) — both leave `data-status`
|
tasks: TasksPanelProps;
|
||||||
* idle, so an automated driver otherwise cannot tell them apart.
|
logs: LogsPanelProps;
|
||||||
*/
|
protocol: ProtocolPanelProps;
|
||||||
deepLinkStatus?: DeepLinkParseStatus;
|
network: NetworkPanelProps;
|
||||||
|
console: ConsolePanelProps;
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function InspectorView({
|
export function InspectorView({
|
||||||
deepLink,
|
shell,
|
||||||
deepLinkStatus,
|
connection,
|
||||||
servers: serversInput,
|
servers: serverList,
|
||||||
serverListWritable = true,
|
tools: toolsPanel,
|
||||||
activeServer,
|
prompts: promptsPanel,
|
||||||
erroredServerId,
|
resources: resourcesPanel,
|
||||||
connectedServerId,
|
apps: appsPanel,
|
||||||
version,
|
tasks: tasksPanel,
|
||||||
connectionStatus,
|
logs: logsPanel,
|
||||||
connectErrorMessage,
|
protocol: protocolPanel,
|
||||||
initializeResult,
|
network: networkPanel,
|
||||||
latencyMs,
|
console: consolePanel,
|
||||||
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,
|
|
||||||
}: InspectorViewProps) {
|
}: 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
|
// UI-only state. Connection state, primitive lists, and all action
|
||||||
// dispatching live in the parent; this component only owns view-local
|
// dispatching live in the parent; this component only owns view-local
|
||||||
// toggles (sort direction, list compact). Tab selection is lifted (#1417).
|
// 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