From 0425516fa23a665d4854a309f880a06e144800fa Mon Sep 17 00:00:00 2001 From: cliffhall Date: Thu, 27 Aug 2026 23:20:58 -0400 Subject: [PATCH 1/3] chore(web): collapse InspectorView's prop wall into domain bundles (#2130) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `` took ~130 flat props, which made ~170 of App.tsx's JSX lines a prop wall rather than a component tree. It now takes 12 domain props and the call site is 14 lines. Two conventions keep the change reviewable. No field is renamed — every field inside a bundle carries the exact name it had as a flat prop, so this is a regrouping rather than a rename. And the bundles stop at `InspectorView`: it destructures each one back into the same locals its body already used and passes the same individual props down, so nothing below the view knows they exist. Three props have no single screen and are placed deliberately: `onCompleteArgument` / `completionsSupported` are read by both the Prompts and Resources screens, so they sit in `connection` rather than being duplicated; `malformedListItems` is one array three screens filter for their own entries, so it sits in `shell`; `erroredServerId` / `connectedServerId` are connection outcomes the Servers screen renders, so they sit in `connection`. Every multi-line closure declared inline in the JSX is now a named `useCallback` above the return, including `onServerReorder`'s `.catch` and the seven `void`-discarding wrappers, which carry their `no-floating-promises` justification once rather than at each call site. The `InspectorView` double in App.test.tsx is now typed with the real `InspectorViewProps` instead of a hand-written structural mirror, so it cannot drift from the component it stands in for. Stories keep one constant per bundle, since Storybook merges args only at the top level. No behavior change. Signed-off-by: cliffhall --- clients/web/src/App.test.tsx | 215 +- clients/web/src/App.tsx | 460 +++-- .../InspectorView/InspectorView.stories.tsx | 290 +-- .../InspectorView/InspectorView.test.tsx | 1741 +++++++++++------ .../views/InspectorView/InspectorView.tsx | 676 ++----- .../components/views/InspectorView/types.ts | 379 ++++ 6 files changed, 2272 insertions(+), 1489 deletions(-) create mode 100644 clients/web/src/components/views/InspectorView/types.ts diff --git a/clients/web/src/App.test.tsx b/clients/web/src/App.test.tsx index 9e626f9f9c..49beb74342 100644 --- a/clients/web/src/App.test.tsx +++ b/clients/web/src/App.test.tsx @@ -450,122 +450,62 @@ vi.mock("./lib/publishAppDocument", () => ({ // that invoke the App's connect / call-tool / get-prompt / read-resource / // set-log-level handlers. vi.mock("./components/views/InspectorView/InspectorView", () => ({ - InspectorView: (props: { - toolCallState?: { status?: string }; - toolsUi?: { - selectedToolKey?: string; - formValues: Record; - search: string; - }; - promptsUi?: { - selectedPromptName?: string; - argumentValues: Record; - submittedFor?: string; - search: string; - }; - logsUi?: { filterText: string; visibleLevels: Record }; - 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; - search: string; - }) => void; - onPromptsUiChange: (next: { - selectedPromptName?: string; - argumentValues: Record; - submittedFor?: string; - search: string; - }) => void; - onLogsUiChange: (next: { - filterText: string; - visibleLevels: Record; - }) => void; - progressByTaskId?: Record; - onCallTool: ( - name: string, - args: Record, - runAsTask?: boolean, - ) => void; - onGetPrompt: (name: string, args: Record) => 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; - onRefreshTools: () => void; - toolsPagination: { - paginated: boolean; - canLoadMore: boolean; - loadedPages: number; - onPaginatedChange: (v: boolean) => void; - onLoadMore: () => void; - }; - }) => ( + InspectorView: (props: InspectorViewProps) => (
- {props.toolCallState?.status ?? "none"} + {props.tools.toolCallState?.status ?? "none"} - {Object.keys(props.progressByTaskId ?? {}).join(",") || "none"} + {Object.keys(props.tasks.progressByTaskId ?? {}).join(",") || "none"} - {props.toolsUi?.selectedToolKey ?? "none"} + {props.tools.toolsUi?.selectedToolKey ?? "none"} + + + {props.tools.toolsUi?.search || "none"} - {props.toolsUi?.search || "none"} - {props.promptsUi?.selectedPromptName ?? "none"} + {props.prompts.promptsUi?.selectedPromptName ?? "none"} + + + {props.logs.logsUi?.filterText || "none"} - {props.logsUi?.filterText || "none"} - {props.getPromptState?.status ?? "none"} + {props.prompts.getPromptState?.status ?? "none"} - {props.readResourceState?.status ?? "none"} + {props.resources.readResourceState?.status ?? "none"} - {props.currentLogLevel} - {props.activeTab ?? "none"} + {props.logs.currentLogLevel} + {props.shell.activeTab ?? "none"} - {props.initializeResult - ? `name:${props.initializeResult.serverInfo.name || "(empty)"}` + {props.connection.initializeResult + ? `name:${props.connection.initializeResult.serverInfo.name || "(empty)"}` : "none"} - {props.erroredServerId ?? "none"} + {props.connection.erroredServerId ?? "none"} + + + {props.connection.activeServer ?? "none"} - {props.activeServer ?? "none"} - - + {/* A second target, so a test can drive an A -> B -> A switch (#2095). */} - - + - - + - - - + + - - - + + - - + + {/* The real server grid (and its Add / Edit controls) lives inside this mocked view, so the config modal is only reachable through these callbacks — and the highlight batch only observable through this prop. */} - - + + - {(props.highlightedServerIds ?? []).join(",") || "none"} + {(props.servers.highlightedServerIds ?? []).join(",") || "none"} - {Array.from(props.pinnedProtocolIds ?? []).join(",")} + {Array.from(props.protocol.pinnedProtocolIds ?? []).join(",")} - - - + - {String(props.toolsPagination.paginated)} + {String(props.tools.toolsPagination.paginated)} - {props.toolsPagination.loadedPages} + {props.tools.toolsPagination.loadedPages} - - - - +
), })); import App from "./App"; +import type { InspectorViewProps } from "./components/views/InspectorView/InspectorView"; import { SERVER_INFO_NOT_REPORTED_LABEL } from "./components/groups/ConnectionInfoContent/ConnectionInfoContent"; import { OAUTH_CALLBACK_PATH } from "./utils/oauthFlow.js"; import { INSPECTOR_SERVERS_TAB } from "./utils/inspectorTabs.js"; diff --git a/clients/web/src/App.tsx b/clients/web/src/App.tsx index 91bf2c29f6..f005b69fc9 100644 --- a/clients/web/src/App.tsx +++ b/clients/web/src/App.tsx @@ -64,6 +64,20 @@ import { useInitialConfig } from "@inspector/core/react/useInitialConfig.js"; import { refreshingPersist } from "./lib/refreshingPersist"; import { usePendingClientRequests } from "@inspector/core/react/usePendingClientRequests.js"; import { InspectorView } from "./components/views/InspectorView/InspectorView"; +import type { + AppsPanelProps, + ConnectionProps, + ConsolePanelProps, + LogsPanelProps, + NetworkPanelProps, + PromptsPanelProps, + ProtocolPanelProps, + ResourcesPanelProps, + ServerListProps, + ShellProps, + TasksPanelProps, + ToolsPanelProps, +} from "./components/views/InspectorView/types"; import type { ReadResourceState } from "./components/screens/ResourcesScreen/ResourcesScreen"; import { AppElicitationHost } from "./components/elements/AppElicitation/AppElicitationHost"; import type { LogEntryData } from "./components/elements/LogEntry/LogEntry"; @@ -1469,6 +1483,275 @@ function App() { [pendingElicitations, inspectorClient], ); + // --- InspectorView prop bundles (#2130) ----------------------------------- + // + // The view takes one prop per domain, so the JSX below reads as a component + // tree rather than a wall of ~130 props. Everything the bundles need is + // assembled here: the handlers that used to be multi-line closures declared + // inline in the JSX are named callbacks, and the `void`-discarding wrappers + // carry their justification once rather than at a call site. + + const onServerAdd = useCallback(() => { + setHighlightedServerIds([]); + setConfigModal({ mode: "add" }); + }, []); + + const onServerImportConfig = useCallback(() => { + setHighlightedServerIds([]); + setImportConfigOpen(true); + }, []); + + const onServerImportJson = useCallback(() => { + setHighlightedServerIds([]); + setImportJsonOpen(true); + }, []); + + const onServerEdit = useCallback((id: string) => { + setConfigModal({ mode: "edit", targetId: id }); + }, []); + + const onServerClone = useCallback((id: string) => { + setHighlightedServerIds([]); + setConfigModal({ mode: "clone", targetId: id }); + }, []); + + const onServerRemove = useCallback( + (id: string) => { + const target = servers.find((s) => s.id === id); + if (target) setRemoveTarget(target); + }, + [servers], + ); + + const onServerReorderFromList = useCallback( + (orderedIds: string[]) => { + // reorderServers reverts the optimistic order via an internal refresh() + // and re-throws on failure (409 from a racing external edit, or a network + // error). Surface that to the user so the drag doesn't silently bounce + // back — matching the toast pattern every other mutation here uses. + reorderServers(orderedIds).catch((err: unknown) => { + notifications.show({ + title: "Failed to reorder servers", + message: err instanceof Error ? err.message : String(err), + color: "red", + }); + }); + }, + [reorderServers], + ); + + const openClientSettings = useCallback(() => setClientSettingsOpen(true), []); + const openConnectionInfo = useCallback( + () => setConnectionInfoModalOpen(true), + [], + ); + const openServerSettings = useCallback( + (id: string) => setSettingsModalTargetId(id), + [], + ); + + // The seven wrappers below discard a promise the callee already owns — each + // of these handlers ends in its own `catch` that surfaces the failure as a + // toast or a panel error — and the view's props are synchronous. `void` is + // the explicit discard `@typescript-eslint/no-floating-promises` requires. + const dispatchToggleConnection = useCallback( + (id: string) => { + void onToggleConnection(id); + }, + [onToggleConnection], + ); + const dispatchDisconnect = useCallback(() => { + void onDisconnect(); + }, [onDisconnect]); + const dispatchCallTool = useCallback( + (name: string, args: Record, runAsTask?: boolean) => { + void onCallTool(name, args, runAsTask); + }, + [onCallTool], + ); + const dispatchGetPrompt = useCallback( + (name: string, args: Record) => { + void onGetPrompt(name, args); + }, + [onGetPrompt], + ); + const dispatchReadResource = useCallback( + (uri: string) => { + void onReadResource(uri); + }, + [onReadResource], + ); + const dispatchCancelTask = useCallback( + (taskId: string) => { + void onCancelTask(taskId); + }, + [onCancelTask], + ); + const dispatchOpenApp = useCallback( + (name: string, args: Record) => { + void onOpenApp(name, args); + }, + [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 ( <> @@ -1484,171 +1767,18 @@ function App() { ) : null} setClientSettingsOpen(true)} - onToggleConnection={(id) => { - void onToggleConnection(id); - }} - onDisconnect={() => { - void onDisconnect(); - }} - onServerAdd={() => { - setHighlightedServerIds([]); - setConfigModal({ mode: "add" }); - }} - onServerImportConfig={() => { - setHighlightedServerIds([]); - setImportConfigOpen(true); - }} - onServerImportJson={() => { - setHighlightedServerIds([]); - setImportJsonOpen(true); - }} - onServerExport={onServerExport} - onConnectionInfo={() => setConnectionInfoModalOpen(true)} - onServerSettings={(id) => setSettingsModalTargetId(id)} - onServerEdit={(id) => setConfigModal({ mode: "edit", targetId: id })} - onServerClone={(id) => { - setHighlightedServerIds([]); - setConfigModal({ mode: "clone", targetId: id }); - }} - onServerRemove={(id) => { - const target = servers.find((s) => s.id === id); - if (target) setRemoveTarget(target); - }} - onServerReorder={(orderedIds) => { - // reorderServers reverts the optimistic order via an internal - // refresh() and re-throws on failure (409 from a racing external - // edit, or a network error). Surface that to the user so the drag - // doesn't silently bounce back — matching the toast pattern every - // other mutation here uses. - reorderServers(orderedIds).catch((err: unknown) => { - notifications.show({ - title: "Failed to reorder servers", - message: err instanceof Error ? err.message : String(err), - color: "red", - }); - }); - }} - highlightedServerIds={highlightedServerIds} - onClearHighlight={clearHighlight} - serverSupportsTaskToolCalls={ - !!capabilities?.tasks?.requests?.tools?.call || - (inspectorClient?.isTasksExtensionNegotiated() ?? false) - } - onToolsUiChange={onToolsUiChange} - onCallTool={(name, args, runAsTask) => { - void onCallTool(name, args, runAsTask); - }} - onCancelToolCall={onCancelToolCall} - onClearToolResult={onClearToolResult} - onReadResourceContents={onReadResourceContents} - onRefreshTools={onRefreshTools} - toolsPagination={toolsPaginationControls} - promptsPagination={promptsPaginationControls} - resourcesPagination={resourcesPaginationControls} - onPromptsUiChange={setUi.setPromptsUi} - onGetPrompt={(name, args) => { - void onGetPrompt(name, args); - }} - onRefreshPrompts={onRefreshPrompts} - onResourcesUiChange={setUi.setResourcesUi} - onReadResource={(uri) => { - void onReadResource(uri); - }} - onSubscribeResource={onSubscribeResource} - onUnsubscribeResource={onUnsubscribeResource} - onRefreshResources={onRefreshResources} - onCompleteArgument={onCompleteArgument} - completionsSupported={capabilities?.completions !== undefined} - subscriptionsSupported={capabilities?.resources?.subscribe === true} - onTasksUiChange={setUi.setTasksUi} - onCancelTask={(taskId) => { - void onCancelTask(taskId); - }} - onClearCompletedTasks={onClearCompletedTasks} - onRefreshTasks={onRefreshTasks} - onSetLogLevel={onSetLogLevel} - modernLogLevel={modernLogLevel} - onSetModernLogLevel={onSetModernLogLevel} - onLogsUiChange={setUi.setLogsUi} - onClearLogs={onClearLogs} - onExportLogs={onExportLogs} - onProtocolUiChange={setUi.setProtocolUi} - onClearProtocol={onClearProtocol} - onExportProtocol={onExportProtocol} - onClearProtocolSection={onClearProtocolSection} - onExportProtocolSection={onExportProtocolSection} - onReplayProtocol={onReplayProtocol} - onTogglePinProtocol={togglePinProtocol} - pinnedProtocolIds={pinnedProtocolIds} - onNetworkUiChange={setUi.setNetworkUi} - onClearNetwork={onClearNetwork} - onExportNetwork={onExportNetwork} - onConsoleUiChange={setUi.setConsoleUi} - onClearConsole={onClearConsole} - onExportConsole={onExportConsole} - onAppsUiChange={setUi.setAppsUi} - onSelectApp={onSelectApp} - onOpenApp={(name, args) => { - void onOpenApp(name, args); - }} - onCloseApp={onCloseApp} - onAppError={onAppError} - onRefreshApps={onRefreshTools} + shell={shellProps} + connection={connectionProps} + servers={serverListProps} + tools={toolsPanelProps} + prompts={promptsPanelProps} + resources={resourcesPanelProps} + apps={appsPanelProps} + tasks={tasksPanelProps} + logs={logsPanelProps} + protocol={protocolPanelProps} + network={networkPanelProps} + console={consolePanelProps} /> = { title: "Views/InspectorView", component: InspectorView, parameters: { layout: "fullscreen" }, args: { - // Data - servers: demoServers, - tools: demoTools, - prompts: demoPrompts, - resources: demoResources, - resourceTemplates: demoResourceTemplates, - subscriptions: demoSubscriptions, - logs: demoLogs, - tasks: demoTasks, - progressByTaskId: demoProgressByTaskId, - protocol: demoHistory, - network: demoNetwork, - stderrLogs: demoStderr, - - // Connection state — stories default to "disconnected"; per-story - // overrides drive the connected / error narratives. - activeServer: undefined, - connectionStatus: "disconnected", - initializeResult: undefined, - latencyMs: undefined, - - // Misc state - currentLogLevel: "info", - sandboxPath: "about:blank", - bridgeFactory: noopBridgeFactory, - appRendererRef: { current: null }, - - // Per-screen UI state (search / filter / selection), one object per screen. - toolsUi: EMPTY_TOOLS_UI, - promptsUi: EMPTY_PROMPTS_UI, - resourcesUi: EMPTY_RESOURCES_UI, - appsUi: EMPTY_APPS_UI, - tasksUi: EMPTY_TASKS_UI, - logsUi: EMPTY_LOGS_UI, - protocolUi: EMPTY_PROTOCOL_UI, - networkUi: EMPTY_NETWORK_UI, - consoleUi: EMPTY_CONSOLE_UI, - - // Callbacks — all wired to storybook spies so play functions can assert - // on dispatch. Real wiring routes these to InspectorClient methods (the - // app shell at clients/web/src/App.tsx). - onToggleTheme: fn(), - onOpenClientSettings: fn(), - onToggleConnection: fn(), - onDisconnect: fn(), - onServerAdd: fn(), - onServerImportConfig: fn(), - onServerImportJson: fn(), - onServerExport: fn(), - onConnectionInfo: fn(), - onServerSettings: fn(), - onServerEdit: fn(), - onServerClone: fn(), - onServerRemove: fn(), - onServerReorder: fn(), - serverSupportsTaskToolCalls: false, - onToolsUiChange: fn(), - onCallTool: fn(), - onRefreshTools: fn(), - toolsPagination: noopPagination, - promptsPagination: noopPagination, - resourcesPagination: noopPagination, - onPromptsUiChange: fn(), - onGetPrompt: fn(), - onRefreshPrompts: fn(), - onResourcesUiChange: fn(), - onReadResource: fn(), - onSubscribeResource: fn(), - onUnsubscribeResource: fn(), - onRefreshResources: fn(), - onTasksUiChange: fn(), - onCancelTask: fn(), - onClearCompletedTasks: fn(), - onRefreshTasks: fn(), - onSetLogLevel: fn(), - onLogsUiChange: fn(), - onClearLogs: fn(), - onExportLogs: fn(), - onProtocolUiChange: fn(), - onClearProtocol: fn(), - onExportProtocol: fn(), - onReplayProtocol: fn(), - onTogglePinProtocol: fn(), - onNetworkUiChange: fn(), - onClearNetwork: fn(), - onExportNetwork: fn(), - onConsoleUiChange: fn(), - onClearConsole: fn(), - onExportConsole: fn(), - onAppsUiChange: fn(), - onSelectApp: fn(), - onOpenApp: fn(), - onCloseApp: fn(), - onAppError: fn(), - onRefreshApps: fn(), - activeTab: "Servers", - onActiveTabChange: fn(), + shell: shellArgs, + connection: connectionArgs, + servers: serversArgs, + tools: toolsArgs, + prompts: promptsArgs, + resources: resourcesArgs, + apps: appsArgs, + tasks: tasksArgs, + logs: logsArgs, + protocol: protocolArgs, + network: networkArgs, + console: consoleArgs, }, render: (args) => { - const [activeTab, setActiveTab] = useState(args.activeTab ?? "Servers"); + const [activeTab, setActiveTab] = useState(args.shell.activeTab); return ( ); }, @@ -463,7 +525,7 @@ export const Default: Story = { export const NoServers: Story = { args: { - servers: [], + servers: { ...serversArgs, servers: [] }, }, }; @@ -480,7 +542,7 @@ const manyServers: ServerEntry[] = Array.from({ length: 24 }, (_, i) => ({ export const ManyServers: Story = { args: { - servers: manyServers, + servers: { ...serversArgs, servers: manyServers }, }, play: async ({ canvasElement }) => { // Even under enough content to overflow, the shell stays viewport-clamped @@ -509,10 +571,13 @@ export const ManyServers: Story = { // regression / storybook play function coverage. export const Connected: Story = { args: { - activeServer: demoServers[0]!.id, - connectionStatus: "connected", - initializeResult: demoInitializeResult, - latencyMs: 142, + connection: { + ...connectionArgs, + activeServer: demoServers[0]!.id, + connectionStatus: "connected", + initializeResult: demoInitializeResult, + latencyMs: 142, + }, }, }; @@ -524,10 +589,13 @@ export const ConnectionError: Story = { // connect-attempt-failure signal that gates the failure column; `network: []` // (and empty Protocol) keep it a pure stdio failure so only Console is offered. args: { - activeServer: demoServers[0]!.id, - erroredServerId: demoServers[0]!.id, - connectionStatus: "error", - network: [], - stderrLogs: demoStderr, + connection: { + ...connectionArgs, + activeServer: demoServers[0]!.id, + erroredServerId: demoServers[0]!.id, + connectionStatus: "error", + }, + network: { ...networkArgs, network: [] }, + console: { ...consoleArgs, stderrLogs: demoStderr }, }, }; diff --git a/clients/web/src/components/views/InspectorView/InspectorView.test.tsx b/clients/web/src/components/views/InspectorView/InspectorView.test.tsx index fc34c4bbaf..e152fad8c8 100644 --- a/clients/web/src/components/views/InspectorView/InspectorView.test.tsx +++ b/clients/web/src/components/views/InspectorView/InspectorView.test.tsx @@ -59,117 +59,179 @@ const noopBridgeFactory: BridgeFactory = () => close: async () => {}, }) as unknown as AppBridge; +/** + * Per-bundle overrides. Each key takes a `Partial` of that bundle, so a test + * names only the field it cares about — `makeProps({ + tools: { + tools: { tools: [t] }, + }, + })` + * — and inherits the rest of the bundle's defaults. + */ +type PropOverrides = { + [K in keyof InspectorViewProps]?: Partial; +}; + +// Merges one bundle across every supplied override layer, later layers +// winning. Kept generic (rather than a spread over `Object.assign`) so each +// bundle stays typed to its own shape. +function mergeBundle( + key: K, + layers: PropOverrides[], +): Partial { + let merged: Partial = {}; + for (const layer of layers) { + const part = layer[key]; + if (part) merged = { ...merged, ...part }; + } + return merged; +} + // Returns a fresh fixture each call so per-test spies can be asserted on // in isolation. The view is purely prop-driven; every callback is // dispatched up to the parent — these spies stand in for App.tsx's // hook-routed handlers in the real wiring. -function makeProps( - overrides: Partial = {}, -): InspectorViewProps { +// +// Takes any number of override layers so a scenario helper can supply its own +// base (see `connectedHttp` below) and still let the caller override on top. +function makeProps(...overrides: PropOverrides[]): InspectorViewProps { return { - servers: [], - activeServer: undefined, - connectionStatus: "disconnected", - initializeResult: undefined, - latencyMs: undefined, - tools: [], - prompts: [], - resources: [], - resourceTemplates: [], - toolsListChanged: false, - promptsListChanged: false, - resourcesListChanged: false, - subscriptions: [], - logs: [], - tasks: [], - protocol: [], - network: [], - stderrLogs: [], - currentLogLevel: "info", - sandboxPath: "about:blank", - bridgeFactory: noopBridgeFactory, - appRendererRef: { current: null }, - 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, - onToggleTheme: vi.fn(), - onOpenClientSettings: vi.fn(), - onToggleConnection: vi.fn(), - onDisconnect: vi.fn(), - onServerAdd: vi.fn(), - onServerImportConfig: vi.fn(), - onServerImportJson: vi.fn(), - onServerExport: vi.fn(), - onConnectionInfo: vi.fn(), - onServerSettings: vi.fn(), - onServerEdit: vi.fn(), - onServerClone: vi.fn(), - onServerRemove: vi.fn(), - onServerReorder: vi.fn(), - serverSupportsTaskToolCalls: false, - onToolsUiChange: vi.fn(), - onCallTool: vi.fn(), - onRefreshTools: vi.fn(), - toolsPagination: noopPagination, - promptsPagination: noopPagination, - resourcesPagination: noopPagination, - onPromptsUiChange: vi.fn(), - onGetPrompt: vi.fn(), - onRefreshPrompts: vi.fn(), - onResourcesUiChange: vi.fn(), - onReadResource: vi.fn(), - onSubscribeResource: vi.fn(), - onUnsubscribeResource: vi.fn(), - onRefreshResources: vi.fn(), - onTasksUiChange: vi.fn(), - onCancelTask: vi.fn(), - onClearCompletedTasks: vi.fn(), - onRefreshTasks: vi.fn(), - onSetLogLevel: vi.fn(), - onLogsUiChange: vi.fn(), - onClearLogs: vi.fn(), - onExportLogs: vi.fn(), - onProtocolUiChange: vi.fn(), - onClearProtocol: vi.fn(), - onExportProtocol: vi.fn(), - onClearProtocolSection: vi.fn(), - onExportProtocolSection: vi.fn(), - onReplayProtocol: vi.fn(), - onTogglePinProtocol: vi.fn(), - onNetworkUiChange: vi.fn(), - onClearNetwork: vi.fn(), - onExportNetwork: vi.fn(), - onConsoleUiChange: vi.fn(), - onClearConsole: vi.fn(), - onExportConsole: vi.fn(), - onAppsUiChange: vi.fn(), - onSelectApp: vi.fn(), - onOpenApp: vi.fn(), - onCloseApp: vi.fn(), - onAppError: vi.fn(), - onRefreshApps: vi.fn(), - activeTab: "Servers", - onActiveTabChange: vi.fn(), - ...overrides, + shell: { + activeTab: "Servers", + onActiveTabChange: vi.fn(), + onToggleTheme: vi.fn(), + onOpenClientSettings: vi.fn(), + ...mergeBundle("shell", overrides), + }, + connection: { + activeServer: undefined, + connectionStatus: "disconnected", + initializeResult: undefined, + latencyMs: undefined, + onToggleConnection: vi.fn(), + onDisconnect: vi.fn(), + ...mergeBundle("connection", overrides), + }, + servers: { + servers: [], + onServerAdd: vi.fn(), + onServerImportConfig: vi.fn(), + onServerImportJson: vi.fn(), + onServerExport: vi.fn(), + onConnectionInfo: vi.fn(), + onServerSettings: vi.fn(), + onServerEdit: vi.fn(), + onServerClone: vi.fn(), + onServerRemove: vi.fn(), + onServerReorder: vi.fn(), + ...mergeBundle("servers", overrides), + }, + tools: { + tools: [], + toolsListChanged: false, + toolsUi: EMPTY_TOOLS_UI, + toolsPagination: noopPagination, + serverSupportsTaskToolCalls: false, + onToolsUiChange: vi.fn(), + onCallTool: vi.fn(), + onRefreshTools: vi.fn(), + ...mergeBundle("tools", overrides), + }, + prompts: { + prompts: [], + promptsListChanged: false, + promptsUi: EMPTY_PROMPTS_UI, + promptsPagination: noopPagination, + onPromptsUiChange: vi.fn(), + onGetPrompt: vi.fn(), + onRefreshPrompts: vi.fn(), + ...mergeBundle("prompts", overrides), + }, + resources: { + resources: [], + resourceTemplates: [], + subscriptions: [], + resourcesListChanged: false, + resourcesUi: EMPTY_RESOURCES_UI, + resourcesPagination: noopPagination, + onResourcesUiChange: vi.fn(), + onReadResource: vi.fn(), + onSubscribeResource: vi.fn(), + onUnsubscribeResource: vi.fn(), + onRefreshResources: vi.fn(), + ...mergeBundle("resources", overrides), + }, + apps: { + appsUi: EMPTY_APPS_UI, + sandboxPath: "about:blank", + bridgeFactory: noopBridgeFactory, + appRendererRef: { current: null }, + onAppsUiChange: vi.fn(), + onSelectApp: vi.fn(), + onOpenApp: vi.fn(), + onCloseApp: vi.fn(), + onAppError: vi.fn(), + onRefreshApps: vi.fn(), + ...mergeBundle("apps", overrides), + }, + tasks: { + tasks: [], + tasksUi: EMPTY_TASKS_UI, + onTasksUiChange: vi.fn(), + onCancelTask: vi.fn(), + onClearCompletedTasks: vi.fn(), + onRefreshTasks: vi.fn(), + ...mergeBundle("tasks", overrides), + }, + logs: { + logs: [], + logsUi: EMPTY_LOGS_UI, + currentLogLevel: "info", + onSetLogLevel: vi.fn(), + onLogsUiChange: vi.fn(), + onClearLogs: vi.fn(), + onExportLogs: vi.fn(), + ...mergeBundle("logs", overrides), + }, + protocol: { + protocol: [], + protocolUi: EMPTY_PROTOCOL_UI, + onProtocolUiChange: vi.fn(), + onClearProtocol: vi.fn(), + onExportProtocol: vi.fn(), + onClearProtocolSection: vi.fn(), + onExportProtocolSection: vi.fn(), + onReplayProtocol: vi.fn(), + onTogglePinProtocol: vi.fn(), + ...mergeBundle("protocol", overrides), + }, + network: { + network: [], + networkUi: EMPTY_NETWORK_UI, + onNetworkUiChange: vi.fn(), + onClearNetwork: vi.fn(), + onExportNetwork: vi.fn(), + ...mergeBundle("network", overrides), + }, + console: { + stderrLogs: [], + consoleUi: EMPTY_CONSOLE_UI, + onConsoleUiChange: vi.fn(), + onClearConsole: vi.fn(), + onExportConsole: vi.fn(), + ...mergeBundle("console", overrides), + }, }; } function StatefulInspectorViewHost(props: InspectorViewProps) { - const [activeTab, setActiveTab] = useState(props.activeTab ?? "Servers"); - const [appsUi, setAppsUi] = useState(props.appsUi ?? EMPTY_APPS_UI); + const [activeTab, setActiveTab] = useState(props.shell.activeTab); + const [appsUi, setAppsUi] = useState(props.apps.appsUi); return ( ); } @@ -244,14 +306,26 @@ describe("InspectorView", () => { it("renders the server card from the input list", () => { renderWithMantine( - , + , ); expect(screen.getByText("Alpha")).toBeInTheDocument(); }); it("renders the footer row with the version and copyright (#1682)", () => { renderWithMantine( - , + , ); expect(screen.getByText("v9.9.9")).toBeInTheDocument(); expect( @@ -264,7 +338,14 @@ describe("InspectorView", () => { const user = userEvent.setup({ delay: null }); renderWithMantine( , ); await user.click(screen.getByRole("switch")); @@ -275,10 +356,16 @@ describe("InspectorView", () => { renderWithMantine( , ); @@ -295,9 +382,15 @@ describe("InspectorView", () => { renderWithMantine( , ); @@ -311,11 +404,15 @@ describe("InspectorView", () => { renderWithMantine( , ); @@ -336,12 +433,16 @@ describe("InspectorView", () => { renderWithMantine( , @@ -358,12 +459,16 @@ describe("InspectorView", () => { renderWithMantine( , @@ -381,12 +486,16 @@ describe("InspectorView", () => { renderWithMantine( , @@ -405,12 +514,16 @@ describe("InspectorView", () => { renderWithMantine( , @@ -427,10 +540,14 @@ describe("InspectorView", () => { renderWithMantine( , ); @@ -443,10 +560,14 @@ describe("InspectorView", () => { renderWithMantine( , ); @@ -462,10 +583,14 @@ describe("InspectorView", () => { renderWithMantine( , ); @@ -482,11 +607,15 @@ describe("InspectorView", () => { const { rerender } = renderWithMantine( , ); @@ -501,9 +630,13 @@ describe("InspectorView", () => { rerender( , ); @@ -530,10 +663,14 @@ describe("InspectorView", () => { renderWithMantine( , ); @@ -560,10 +697,14 @@ describe("InspectorView", () => { renderWithMantine( , ); @@ -576,13 +717,19 @@ describe("InspectorView", () => { renderWithMantine( , ); @@ -599,11 +746,17 @@ describe("InspectorView", () => { renderWithMantine( , ); @@ -615,10 +768,14 @@ describe("InspectorView", () => { renderWithMantine( , ); @@ -632,10 +789,14 @@ describe("InspectorView", () => { renderWithMantine( , ); @@ -648,11 +809,15 @@ describe("InspectorView", () => { renderWithMantine( , ); @@ -668,13 +833,19 @@ describe("InspectorView", () => { renderWithMantine( , ); @@ -701,12 +872,18 @@ describe("InspectorView", () => { renderWithMantine( , ); @@ -727,12 +904,18 @@ describe("InspectorView", () => { renderWithMantine( , ); @@ -746,11 +929,17 @@ describe("InspectorView", () => { renderWithMantine( , ); @@ -768,11 +957,17 @@ describe("InspectorView", () => { const { rerender } = renderWithMantine( , ); @@ -784,11 +979,17 @@ describe("InspectorView", () => { rerender( , ); @@ -803,21 +1004,31 @@ describe("InspectorView", () => { renderWithMantine( , @@ -843,21 +1054,29 @@ describe("InspectorView", () => { renderWithMantine( , @@ -900,22 +1119,30 @@ describe("InspectorView", () => { renderWithMantine( , @@ -935,20 +1162,28 @@ describe("InspectorView", () => { renderWithMantine( , @@ -965,11 +1200,17 @@ describe("InspectorView", () => { const { rerender } = renderWithMantine( , ); @@ -985,11 +1226,17 @@ describe("InspectorView", () => { rerender( , ); @@ -1003,14 +1250,20 @@ describe("InspectorView", () => { renderWithMantine(