From 0babbfa6a9a2c8475cb7338f6a19b79ef21e8d5b Mon Sep 17 00:00:00 2001 From: cliffhall Date: Sat, 25 Apr 2026 18:38:45 -0400 Subject: [PATCH] Remove ConnectedView and UnconnectedView, superseded by InspectorView The two prior shell views took `children` and routed connection state by having two distinct mounts. `InspectorView` collapses both into a single state-machine view that switches `ViewHeader` modes internally, so the old shells have no remaining call sites. Spec section 4 (Views) rewritten to describe `InspectorView` instead. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../ConnectedView/ConnectedView.stories.tsx | 714 ------------------ .../views/ConnectedView/ConnectedView.tsx | 48 -- .../UnconnectedView.stories.tsx | 207 ----- .../views/UnconnectedView/UnconnectedView.tsx | 19 - specification/v2_ux_interfaces.md | 88 +-- 5 files changed, 34 insertions(+), 1042 deletions(-) delete mode 100644 clients/web/src/components/views/ConnectedView/ConnectedView.stories.tsx delete mode 100644 clients/web/src/components/views/ConnectedView/ConnectedView.tsx delete mode 100644 clients/web/src/components/views/UnconnectedView/UnconnectedView.stories.tsx delete mode 100644 clients/web/src/components/views/UnconnectedView/UnconnectedView.tsx diff --git a/clients/web/src/components/views/ConnectedView/ConnectedView.stories.tsx b/clients/web/src/components/views/ConnectedView/ConnectedView.stories.tsx deleted file mode 100644 index ac68c027..00000000 --- a/clients/web/src/components/views/ConnectedView/ConnectedView.stories.tsx +++ /dev/null @@ -1,714 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react-vite"; -import { fn } from "storybook/test"; -import { Text } from "@mantine/core"; -import { ConnectedView } from "./ConnectedView.js"; -import { ToolsScreen } from "../../screens/ToolsScreen/ToolsScreen"; -import { ResourcesScreen } from "../../screens/ResourcesScreen/ResourcesScreen"; -import { PromptsScreen } from "../../screens/PromptsScreen/PromptsScreen"; -import { LoggingScreen } from "../../screens/LoggingScreen/LoggingScreen"; -import { TasksScreen } from "../../screens/TasksScreen/TasksScreen"; -import { HistoryScreen } from "../../screens/HistoryScreen/HistoryScreen"; - -import type { LoggingLevel } from "@modelcontextprotocol/sdk/types.js"; -import type { LogEntryData } from "../../elements/LogEntry/LogEntry"; - -const allTabs = ["Tools", "Resources", "Prompts", "Logs", "Tasks", "History"]; - -const logMessages: { level: LoggingLevel; data: string; logger?: string }[] = [ - { level: "info", data: "Server started on port 3000" }, - { - level: "debug", - data: "Loading configuration from /etc/mcp/config.json", - logger: "config", - }, - { - level: "warning", - data: "Deprecated API endpoint called: /v1/tools", - logger: "http", - }, - { level: "info", data: "Client connected: inspector-web-ui" }, - { - level: "error", - data: "Failed to read resource: file not found at /data/missing.txt", - logger: "resources", - }, - { - level: "info", - data: "Tool execution completed: search_files (245ms)", - logger: "tools", - }, - { - level: "debug", - data: "Parsing JSON-RPC request body", - logger: "transport", - }, - { - level: "info", - data: "Listing available tools for client session", - logger: "tools", - }, - { - level: "warning", - data: "Slow query detected: 1200ms for resource lookup", - logger: "db", - }, - { - level: "info", - data: "Resource subscription added: file:///config.json", - logger: "resources", - }, - { - level: "debug", - data: "Heartbeat ping received from client", - logger: "session", - }, - { - level: "error", - data: "Permission denied reading /etc/secrets.env", - logger: "resources", - }, - { - level: "info", - data: "Prompt 'summarize' resolved with 2 messages", - logger: "prompts", - }, - { - level: "debug", - data: "Cache hit for resource: db://users/42", - logger: "cache", - }, - { - level: "info", - data: "Tool 'create_record' executed successfully in 89ms", - logger: "tools", - }, - { - level: "warning", - data: "Client reconnection attempt #3", - logger: "session", - }, - { - level: "info", - data: "New resource detected: file:///data/output.csv", - logger: "resources", - }, - { - level: "debug", - data: "Serializing response payload (2.4KB)", - logger: "transport", - }, - { - level: "error", - data: "Timeout waiting for tool response after 30s", - logger: "tools", - }, - { - level: "info", - data: "Session initialized with capabilities: tools, resources, prompts", - }, - { - level: "debug", - data: "Validating input schema for tool 'batch_process'", - logger: "tools", - }, - { - level: "info", - data: "Resource template resolved: db://tables/users/rows/15", - logger: "resources", - }, - { - level: "warning", - data: "Memory usage at 78% — consider increasing limits", - logger: "system", - }, - { - level: "info", - data: "Client disconnected gracefully", - logger: "session", - }, - { - level: "debug", - data: "Flushing log buffer to disk (128 entries)", - logger: "logging", - }, -]; - -function pad(n: number): string { - return n.toString().padStart(2, "0"); -} - -const sampleLogEntries: LogEntryData[] = Array.from({ length: 50 }, (_, i) => { - const src = logMessages[i % logMessages.length]; - return { - receivedAt: new Date(`2026-03-17T10:00:${pad(i + 1)}Z`), - params: { level: src.level, data: src.data, logger: src.logger }, - }; -}); - -const meta: Meta = { - title: "Views/ConnectedView", - component: ConnectedView, - parameters: { layout: "fullscreen" }, - args: { - serverInfo: { name: "my-mcp-server", version: "1.0.0" }, - status: "connected", - latencyMs: 23, - availableTabs: allTabs, - activeTab: "Tools", - onTabChange: fn(), - onDisconnect: fn(), - onToggleTheme: fn(), - }, -}; - -export default meta; -type Story = StoryObj; - -export const ToolsActive: Story = { - args: { - activeTab: "Tools", - children: ( - - ), - }, -}; - -export const ResourcesActive: Story = { - args: { - activeTab: "Resources", - children: ( - - ), - }, -}; - -export const PromptsActive: Story = { - args: { - activeTab: "Prompts", - children: ( - - ), - }, -}; - -export const LoggingActive: Story = { - args: { - activeTab: "Logs", - children: ( - - ), - }, -}; - -export const TasksActive: Story = { - args: { - activeTab: "Tasks", - children: ( - - ), - }, -}; - -export const HistoryActive: Story = { - args: { - activeTab: "History", - children: ( - - ), - }, -}; - -export const LimitedTabs: Story = { - args: { - availableTabs: ["Tools", "Resources", "Prompts"], - children: Tools screen content, - }, -}; - -export const LongServerName: Story = { - args: { - serverInfo: { - name: "my-very-long-server-name-that-might-overflow-is", - version: "1.0.0", - }, - children: Tools screen content, - }, -}; diff --git a/clients/web/src/components/views/ConnectedView/ConnectedView.tsx b/clients/web/src/components/views/ConnectedView/ConnectedView.tsx deleted file mode 100644 index 2d74c621..00000000 --- a/clients/web/src/components/views/ConnectedView/ConnectedView.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import type { ReactNode } from "react"; -import { AppShell } from "@mantine/core"; -import type { Implementation } from "@modelcontextprotocol/sdk/types.js"; -import type { ConnectionStatus } from "@inspector/core/mcp/types.js"; -import { ViewHeader } from "../../groups/ViewHeader/ViewHeader"; - -export interface ConnectedLayoutProps { - serverInfo: Implementation; - status: ConnectionStatus; - latencyMs?: number; - activeTab: string; - availableTabs: string[]; - onTabChange: (tab: string) => void; - onDisconnect: () => void; - onToggleTheme: () => void; - children: ReactNode; -} - -export function ConnectedView({ - serverInfo, - status, - latencyMs, - activeTab, - availableTabs, - onTabChange, - onDisconnect, - onToggleTheme, - children, -}: ConnectedLayoutProps) { - return ( - - - - - {children} - - ); -} diff --git a/clients/web/src/components/views/UnconnectedView/UnconnectedView.stories.tsx b/clients/web/src/components/views/UnconnectedView/UnconnectedView.stories.tsx deleted file mode 100644 index ffac57f3..00000000 --- a/clients/web/src/components/views/UnconnectedView/UnconnectedView.stories.tsx +++ /dev/null @@ -1,207 +0,0 @@ -import { useState } from "react"; -import type { InspectorServerSettings } from "@inspector/core/mcp/types.js"; -import type { Meta, StoryObj } from "@storybook/react-vite"; -import { fn } from "storybook/test"; -import { UnconnectedView } from "./UnconnectedView.js"; -import { - ServerListScreen, - type ServerEntry, -} from "../../screens/ServerListScreen/ServerListScreen"; -import { ServerSettingsModal } from "../../groups/ServerSettingsModal/ServerSettingsModal"; - -const meta: Meta = { - title: "Views/UnconnectedView", - component: UnconnectedView, - parameters: { layout: "fullscreen" }, - args: { - onToggleTheme: fn(), - }, -}; - -export default meta; -type Story = StoryObj; - -const sharedScreenCallbacks = { - onAddManually: fn(), - onImportConfig: fn(), - onImportServerJson: fn(), - onToggleConnection: fn(), - onServerInfo: fn(), - onSettings: fn(), - onEdit: fn(), - onClone: fn(), - onRemove: fn(), -}; - -function makeStdioServer( - name: string, - version: string, - command: string, -): ServerEntry { - return { - id: crypto.randomUUID(), - name, - config: { command }, - info: { name, version }, - connection: { status: "disconnected" }, - }; -} - -function makeHttpServer( - name: string, - version: string, - url: string, -): ServerEntry { - return { - id: crypto.randomUUID(), - name, - config: { type: "streamable-http", url }, - info: { name, version }, - connection: { status: "disconnected" }, - }; -} - -export const Empty: Story = { - args: { - children: , - }, -}; - -export const WithServers: Story = { - args: { - children: ( - - ), - }, -}; - -function SettingsModalStory() { - const [settings, setSettings] = useState({ - headers: [ - { key: "Authorization", value: "Bearer token-abc-123" }, - { key: "X-Request-Id", value: "req-456" }, - ], - metadata: [{ key: "userId", value: "user-789" }], - connectionTimeout: 30000, - requestTimeout: 60000, - oauthClientId: "my-client-id", - oauthClientSecret: "super-secret-value", - oauthScopes: "read write", - }); - - return ( - - - - - ); -} - -export const WithSettingsModal: Story = { - render: () => , -}; - -export const ManyServers: Story = { - args: { - children: ( - - ), - }, -}; diff --git a/clients/web/src/components/views/UnconnectedView/UnconnectedView.tsx b/clients/web/src/components/views/UnconnectedView/UnconnectedView.tsx deleted file mode 100644 index b95dfaea..00000000 --- a/clients/web/src/components/views/UnconnectedView/UnconnectedView.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import type { ReactNode } from "react"; -import { AppShell } from "@mantine/core"; -import { ViewHeader } from "../../groups/ViewHeader/ViewHeader"; - -export interface HomeLayoutProps { - children: ReactNode; - onToggleTheme: () => void; -} - -export function UnconnectedView({ children, onToggleTheme }: HomeLayoutProps) { - return ( - - - - - {children} - - ); -} diff --git a/specification/v2_ux_interfaces.md b/specification/v2_ux_interfaces.md index 59e09c17..049379ab 100644 --- a/specification/v2_ux_interfaces.md +++ b/specification/v2_ux_interfaces.md @@ -775,69 +775,49 @@ themselves — every callback passed down must originate in a core hook. ## Section 4 — Views (`clients/web/src/components/views/`) -> Outer shell components that decide which screen to render based on -> connection state (`ConnectedView`, `UnconnectedView`). They receive the full -> Inspector core context and route to the appropriate screen. +> Outer shell component that holds inspector-level state (active server, +> connection status, current tab) and renders the appropriate screen. +> Receives all screen data via props so a single view can drive the whole +> application. -### ConnectedView +### InspectorView -- **Location**: `views/ConnectedView/ConnectedView.tsx` -- **Purpose**: Outer `AppShell` wrapper shown once a server session is live; renders `ViewHeader` in connected mode and slots the active screen into the main region via `children`. -- **Current props**: - - `serverName: string` - - `status: "connected" | "connecting" | "failed"` - - `latencyMs?: number` - - `activeTab: string` +- **Location**: `views/InspectorView/InspectorView.tsx` +- **Purpose**: Single state-machine view that owns inspector-level state, switches `ViewHeader` between connected and unconnected modes, and renders the active screen based on the current tab. Replaces the prior `ConnectedView` / `UnconnectedView` pair (which awkwardly took `children`). +- **Internal state**: + - `activeServer: string | undefined` + - `connectionStatus: ConnectionStatus` + - `initializeResult: InitializeResult | undefined` + - `latencyMs: number | undefined` + - `activeTab: string` (defaults to `"Servers"`; expands to all tabs once a server is connected) - `availableTabs: string[]` - - `onTabChange: (tab: string) => void` - - `onDisconnect: () => void` - - `onToggleTheme: () => void` - - `children: ReactNode` -- **MCP schema touch points**: - - `Implementation` (server identity → `serverName`) from `InitializeResult.serverInfo`. - - `ServerCapabilities` from `InitializeResult.capabilities` — drives which tabs are available (tools/prompts/resources/logging/completions). - - `InitializeResult` as a whole is the natural handshake object to accept instead of loose strings. -- **Target props**: - - `serverInfo: Implementation` - - `capabilities: ServerCapabilities` - - `connectionStatus: "connected" | "connecting" | "failed"` (app state, owned by core) - - `latencyMs?: number` (app state) - - `activeTab: InspectorTab` (app state; enum derived from capabilities) - - `availableTabs: InspectorTab[]` (app state; derived from `capabilities`) - - `onTabChange: (tab: InspectorTab) => void` - - `onDisconnect: () => void` - - `onToggleTheme: () => void` - - `children: ReactNode` -- **Callbacks → core hook**: - - `onDisconnect` → `useConnection` (disconnect action). - - `onTabChange` → local UI state in the wiring layer (not an MCP call); wiring-layer `useState` (no dedicated hook yet) or screen-level state. - - `onToggleTheme` → app-level theme store, not a core hook. - - `connectionStatus`, `latencyMs`, `serverInfo`, `capabilities` → `useConnection` / `useInspectorClient` / `useServerCapabilities`. -- **Internal refactors**: - - Replace flat `serverName` with `serverInfo.name` lookup; forward full `serverInfo` to `ViewHeader` once that group is updated. - - Compute/validate `availableTabs` against `capabilities` rather than accepting an arbitrary string list; narrow `activeTab`/`availableTabs` to an `InspectorTab` union. - - No transport or fetching logic; remains a pure shell routing children. - -### UnconnectedView - -- **Location**: `views/UnconnectedView/UnconnectedView.tsx` -- **Purpose**: Outer `AppShell` wrapper shown when no MCP session is active; renders `ViewHeader` in disconnected mode and slots the server-list / connect screen into the main region via `children`. + - `logLevel: LoggingLevel` + - `autoScroll: boolean` - **Current props**: - - `children: ReactNode` + - `servers: ServerEntry[]` + - `tools: Tool[]` + - `prompts: Prompt[]` + - `resources: Resource[]` + - `resourceTemplates: ResourceTemplate[]` + - `subscriptions: InspectorResourceSubscription[]` + - `logs: LogEntryData[]` + - `tasks: Task[]` + - `progressByTaskId?: Record` + - `history: MessageEntry[]` - `onToggleTheme: () => void` - **MCP schema touch points**: - - None directly — no handshake has occurred. The view is intentionally MCP-schema-free and only hosts the connect flow. -- **Target props**: - - `children: ReactNode` - - `onToggleTheme: () => void` - - (Optional future) `connectionStatus: "disconnected" | "connecting" | "failed"` if the header needs to surface retry state in this shell. + - `Implementation` / `InitializeResult` — produced internally on connect and forwarded to `ViewHeader`. + - `ServerCapabilities` — will eventually drive `availableTabs` (currently the view exposes all tabs once connected; capability-gating is a follow-up). + - All screen-data types (`Tool`, `Prompt`, `Resource`, `ResourceTemplate`, `Task`, `LoggingLevel`) flow straight through to the relevant screen. - **Callbacks → core hook**: + - Connection toggle (per-card) → `useConnection` (connect / disconnect actions). - `onToggleTheme` → app-level theme store, not a core hook. - - Any connect action rendered inside `children` (e.g. from `ServerListScreen`) → `useConnection` (connect action), not owned by this view. -- **Internal refactors**: - - None required today; keep the shell minimal and let the embedded `ServerListScreen` own all connect/form state. - - Rename internal `HomeLayoutProps` to `UnconnectedViewProps` for consistency with the component name when the refactor lands. + - Per-screen handlers (`onCallTool`, `onGetPrompt`, `onReadResource`, `onSetLevel`, `onCancel` …) currently stubbed as `noop`; the wiring layer will replace these with hook calls (`useManagedTools`, `useManagedPrompts`, `useManagedResources`, `useLogging`, `useTasks`, `useHistory`). +- **Internal refactors / follow-ups**: + - Compute `availableTabs` from `initializeResult.capabilities` instead of always exposing all tabs. + - Narrow `activeTab` / `availableTabs` to an `InspectorTab` union. + - Once core hooks land, the view should accept the hook outputs directly rather than receive bare arrays via props (stories will continue to pass fixtures). ---