import { Alert, Card, CloseButton, Flex, Group, Loader, Stack, Text, } from "@mantine/core"; import type { MalformedListItem } from "@inspector/core/mcp"; import type { ProtocolEra, ReadResourceResult, Resource, ResourceTemplateType as ResourceTemplate, } from "@modelcontextprotocol/client"; import type { InspectorResourceSubscription, ResourceSubscriptionStreamState, } from "../../../../../../core/mcp/types.js"; import { ResourceControls } from "../../groups/ResourceControls/ResourceControls"; import type { ListPaginationControlsProps } from "../../elements/ListPaginationControls/ListPaginationControls"; import { ResourcePreviewPanel } from "../../groups/ResourcePreviewPanel/ResourcePreviewPanel"; import { ResourceTemplatePanel } from "../../groups/ResourceTemplatePanel/ResourceTemplatePanel"; export interface ReadResourceState { status: "idle" | "pending" | "ok" | "error"; uri?: string; result?: ReadResourceResult; error?: string; lastUpdated?: Date; isSubscribed?: boolean; } export interface ResourcesScreenProps { resources: Resource[]; templates: ResourceTemplate[]; subscriptions: InspectorResourceSubscription[]; /** * Modern-era `subscriptions/listen` stream state (#1630). On the modern era * the Subscriptions section shows a stream-status badge and a header dot; * `active: false` (the legacy default) renders neither. */ subscriptionStreamState?: ResourceSubscriptionStreamState; /** Negotiated protocol era; gates the modern subscription stream chrome. */ protocolEra?: ProtocolEra; readState?: ReadResourceState; ui: ResourcesUiState; listChanged: boolean; completionsSupported?: boolean; /** * Whether the connected server advertises the `resources.subscribe` * capability. When false, the Subscribe/Unsubscribe button and the * Subscriptions accordion section are hidden. Defaults to true so the * controls render unless a caller explicitly marks them unsupported. */ subscriptionsSupported?: boolean; onUiChange: (next: ResourcesUiState) => void; onRefreshList: () => void; /** A failed list load, rendered above the sidebar list (#1953). */ /** * Entries dropped from this screen's list result(s) as malformed; the list * panel warns about them above the list (#1909). */ malformedListItems?: MalformedListItem[]; loadError?: Error | null; /** Pagination controls rendered in the sidebar (#1721). */ pagination: ListPaginationControlsProps; onReadResource: (uri: string) => void; onSubscribeResource: (uri: string) => void; onUnsubscribeResource: (uri: string) => void; onCompleteArgument?: ( ref: | { type: "ref/resource"; uri: string } | { type: "ref/prompt"; name: string }, argumentName: string, argumentValue: string, context: Record, ) => Promise; compact: boolean; onCompactChange: (next: boolean) => void; } // Selection (resource URI, template URI, the originating-template marker), the // sidebar search, and accordion open-sections — controlled by the parent (App) // as one object so they persist across tab navigation within a live session // (#1417). `openSections` undefined → ResourceControls falls back to the // compact-derived default. export interface ResourcesUiState { selectedResourceUri?: string; selectedTemplateUri?: string; originatingTemplateUri?: string; search: string; openSections?: string[]; } const ScreenLayout = Flex.withProps({ variant: "screen", h: "calc(100dvh - var(--app-shell-header-height, 0px) - var(--app-shell-footer-height, 0px))", gap: "md", p: "xl", }); const Sidebar = Stack.withProps({ // Widened from 340 to comfortably fit the pagination controls // (Load-next-page button + status) without cramping list entries (#1721). w: 360, flex: "0 0 auto", }); // Card that grows with its content but is capped at the screen height by the // `sidebar` variant (`max-height: 100%`), like the Tools panel. The column // layout lets ResourceControls' accordion take over per-section scrolling once // the content would overflow that cap. const SidebarCard = Card.withProps({ withBorder: true, padding: "lg", variant: "sidebar", }); const DetailCard = Card.withProps({ withBorder: true, padding: "lg", }); // Card that sizes to its content but caps at the screen's available // height. When content fits, the card stays compact (footer sits right // under the body); when content would overflow, the inner ScrollArea // inside ResourcePreviewPanel shrinks and scrolls. const PreviewCard = Card.withProps({ withBorder: true, padding: "lg", variant: "preview", }); // Column that pins the preview card to the top of the available space // and bounds its growth via the consumer-set `mah`. The card inside // keeps its natural height up to that cap. const PreviewPane = Flex.withProps({ flex: 1, miw: 0, direction: "column", align: "stretch", }); const EmptyState = Text.withProps({ c: "dimmed", ta: "center", py: "xl", }); // Centered loader/status column shown while a resource is being read. const CenteredStatus = Stack.withProps({ align: "center", py: "xl", }); const ReadErrorAlert = Alert.withProps({ color: "red", variant: "light", title: "Read Error", }); const SCROLL_MAX_HEIGHT = "calc(100dvh - var(--app-shell-header-height, 0px) - var(--app-shell-footer-height, 0px) - var(--mantine-spacing-xl) * 2)"; export function ResourcesScreen({ resources, templates, subscriptions, subscriptionStreamState, protocolEra, readState, ui, listChanged, completionsSupported, subscriptionsSupported = true, onUiChange, onRefreshList, malformedListItems, loadError, pagination, onReadResource, onSubscribeResource, onUnsubscribeResource, onCompleteArgument, compact, onCompactChange, }: ResourcesScreenProps) { const { selectedResourceUri, selectedTemplateUri, originatingTemplateUri, search, openSections, } = ui; const selectedResource = selectedResourceUri ? resources.find((r) => r.uri === selectedResourceUri) : undefined; const selectedTemplate = selectedTemplateUri ? templates.find((t) => t.uriTemplate === selectedTemplateUri) : undefined; // For template-expanded URIs that don't appear in the resources list, // construct a synthetic Resource so the preview panel can render. const readResource: Resource | undefined = selectedResource ?? (readState?.uri && readState.uri === selectedResourceUri ? { name: readState.uri, uri: readState.uri } : undefined); function handleSelectResource(uri: string) { onUiChange({ ...ui, selectedTemplateUri: undefined, selectedResourceUri: uri, originatingTemplateUri: undefined, }); onReadResource(uri); } function handleSelectTemplate(uriTemplate: string) { onUiChange({ ...ui, selectedResourceUri: undefined, selectedTemplateUri: uriTemplate, originatingTemplateUri: undefined, }); } function handleReadResource(uri: string) { // Once the user reads (either from the template form or a refresh // inside the preview panel), hand the screen over to the preview: // clearing the template selection hides the template form so only // the rendered resource is shown. We remember the template URI so // closing the preview can restore the form. onUiChange({ ...ui, originatingTemplateUri: selectedTemplateUri ?? originatingTemplateUri, selectedTemplateUri: undefined, selectedResourceUri: uri, }); onReadResource(uri); } function handleClosePreview() { if (originatingTemplateUri) { onUiChange({ ...ui, selectedResourceUri: undefined, selectedTemplateUri: originatingTemplateUri, originatingTemplateUri: undefined, }); } else { onUiChange({ ...ui, selectedResourceUri: undefined }); } } function renderReadState() { if (!readState) return null; if (readState.status === "pending") { return ( Reading resource... ); } if (readState.status === "error") { return ( {readState.error ?? "Failed to read resource"} ); } if (readState.result && readResource) { return ( handleReadResource(readResource.uri)} onSubscribe={() => onSubscribeResource(readResource.uri)} onUnsubscribe={() => onUnsubscribeResource(readResource.uri)} onClose={handleClosePreview} /> ); } return null; } return ( // `data-*` readiness contract for the headless tab smoke (#2148); see // clients/web/README.md#core-tab-automation-contract. onUiChange({ ...ui, search: value })} onOpenSectionsChange={(value) => onUiChange({ ...ui, openSections: value }) } onSelectUri={handleSelectResource} onSelectTemplate={handleSelectTemplate} onUnsubscribeResource={onUnsubscribeResource} compact={compact} onCompactChange={onCompactChange} /> {selectedTemplate ? ( // Template form only — once the user clicks Read Resource, // handleReadResource clears the template selection so the // resource branch takes over and the preview is shown alone. // Fills the main area width (like the preview pane) rather than // being capped, so the URI preview and variable inputs get the // full width on wide displays. onCompleteArgument( { type: "ref/resource", uri: selectedTemplate.uriTemplate, }, argName, value, context, ) : undefined } /> ) : readResource ? ( // Sized-to-content preview pane, capped at the screen's available // height. When the resource body fits, the card hugs its content // and the subscribe/refresh row sits right under it. When the body // would overflow, the inner ScrollArea inside ResourcePreviewPanel // shrinks and scrolls, keeping the footer pinned at the cap. // miw=0 prevents wide content (long unbroken lines, tables) from // pushing the pane past the viewport's right edge. {renderReadState()} ) : ( Select a resource to preview )} ); }