From 57d57a0b34ac1d3e368ea7e9e9b2fd349f131f51 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Tue, 14 Jul 2026 13:10:35 -0400 Subject: [PATCH 01/10] web: Group tool-result resource_links in a scrollable "Resource Links" box with Protocol colors Closes #1685 - ToolResultPanel: coalesce consecutive `resource_link` blocks into a single bordered, internally-scrollable "Resource Links" box (mirroring the "Messages" box in the Protocol monitoring sidebar). Non-link blocks and non-adjacent runs are preserved in order via `segmentContent`. - ResourceLink: render each link card on the recessed `inset` surface (`Card variant="inset"`), matching ProtocolEntry's colors in light and dark modes; nested read-result Code blocks read as raised on the grey card via the inset cascade variable. - Drop redundant metadata: the resource `description` line (ResourceLinkInfo, and thus ContentViewer/ResourceLink) and the green "Resource:" label above the expanded read result. - Update tests + stories for the removed props/label and the new grouping box; add multi-link and split-run coverage. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01RwZGbocZ64yN9bgb2mj1qE --- .../ContentViewer/ContentViewer.test.tsx | 2 - .../elements/ContentViewer/ContentViewer.tsx | 1 - .../ResourceLinkInfo.stories.tsx | 1 - .../ResourceLinkInfo.test.tsx | 10 +- .../ResourceLinkInfo/ResourceLinkInfo.tsx | 15 +-- .../ResourceLink/ResourceLink.stories.tsx | 9 +- .../groups/ResourceLink/ResourceLink.test.tsx | 17 +-- .../groups/ResourceLink/ResourceLink.tsx | 42 +++----- .../ToolResultPanel.stories.tsx | 39 +++++++ .../ToolResultPanel/ToolResultPanel.test.tsx | 41 ++++++- .../ToolResultPanel/ToolResultPanel.tsx | 100 +++++++++++++++--- 11 files changed, 197 insertions(+), 80 deletions(-) diff --git a/clients/web/src/components/elements/ContentViewer/ContentViewer.test.tsx b/clients/web/src/components/elements/ContentViewer/ContentViewer.test.tsx index 1b04403fb..9ac9e0fbc 100644 --- a/clients/web/src/components/elements/ContentViewer/ContentViewer.test.tsx +++ b/clients/web/src/components/elements/ContentViewer/ContentViewer.test.tsx @@ -122,12 +122,10 @@ describe("ContentViewer", () => { type: "resource_link", uri: "ui://app", name: "Cool App", - description: "An app", mimeType: "text/html", }; renderWithMantine(); expect(screen.getByText("Cool App")).toBeInTheDocument(); - expect(screen.getByText("An app")).toBeInTheDocument(); expect(screen.getByText("text/html")).toBeInTheDocument(); expect(screen.queryByRole("button")).not.toBeInTheDocument(); }); diff --git a/clients/web/src/components/elements/ContentViewer/ContentViewer.tsx b/clients/web/src/components/elements/ContentViewer/ContentViewer.tsx index 64087d9a1..fe46a4894 100644 --- a/clients/web/src/components/elements/ContentViewer/ContentViewer.tsx +++ b/clients/web/src/components/elements/ContentViewer/ContentViewer.tsx @@ -367,7 +367,6 @@ function BlockContent({ ); diff --git a/clients/web/src/components/elements/ResourceLinkInfo/ResourceLinkInfo.stories.tsx b/clients/web/src/components/elements/ResourceLinkInfo/ResourceLinkInfo.stories.tsx index cdbecd362..2b255c26f 100644 --- a/clients/web/src/components/elements/ResourceLinkInfo/ResourceLinkInfo.stories.tsx +++ b/clients/web/src/components/elements/ResourceLinkInfo/ResourceLinkInfo.stories.tsx @@ -13,7 +13,6 @@ export const Full: Story = { args: { uri: "file:///docs/readme.md", name: "Readme", - description: "Project documentation", mimeType: "text/markdown", }, }; diff --git a/clients/web/src/components/elements/ResourceLinkInfo/ResourceLinkInfo.test.tsx b/clients/web/src/components/elements/ResourceLinkInfo/ResourceLinkInfo.test.tsx index 1a3dedc6b..526b3727c 100644 --- a/clients/web/src/components/elements/ResourceLinkInfo/ResourceLinkInfo.test.tsx +++ b/clients/web/src/components/elements/ResourceLinkInfo/ResourceLinkInfo.test.tsx @@ -5,18 +5,12 @@ import { ResourceLinkInfo } from "./ResourceLinkInfo"; const URI = "file:///docs/readme.md"; describe("ResourceLinkInfo", () => { - it("renders uri, name, description, and mimeType", () => { + it("renders uri, name, and mimeType", () => { renderWithMantine( - , + , ); expect(screen.getByText(URI)).toBeInTheDocument(); expect(screen.getByText("Readme")).toBeInTheDocument(); - expect(screen.getByText("The project readme")).toBeInTheDocument(); expect(screen.getByText("text/markdown")).toBeInTheDocument(); }); diff --git a/clients/web/src/components/elements/ResourceLinkInfo/ResourceLinkInfo.tsx b/clients/web/src/components/elements/ResourceLinkInfo/ResourceLinkInfo.tsx index 71e2b5e17..77f22f289 100644 --- a/clients/web/src/components/elements/ResourceLinkInfo/ResourceLinkInfo.tsx +++ b/clients/web/src/components/elements/ResourceLinkInfo/ResourceLinkInfo.tsx @@ -6,8 +6,6 @@ export interface ResourceLinkInfoProps { uri: string; /** Optional human-friendly name shown beneath the URI. */ name?: string; - /** Optional description shown beneath the name. */ - description?: string; /** Optional MIME type shown as a badge. */ mimeType?: string; /** @@ -56,21 +54,15 @@ const NameText = Text.withProps({ fw: 600, }); -const DescriptionText = Text.withProps({ - size: "sm", - c: "dimmed", -}); - /** * Pure-display metadata for a `resource_link`: the URI (monospace, link-styled), - * optional name / description, and a MIME-type badge. The optional `action` - * slot lets an interactive wrapper (e.g. {@link ResourceLink}) place an - * expand/collapse indicator in the URI row. + * an optional name, and a MIME-type badge. The optional `action` slot lets an + * interactive wrapper (e.g. {@link ResourceLink}) place an expand/collapse + * indicator in the URI row. */ export function ResourceLinkInfo({ uri, name, - description, mimeType, action, }: ResourceLinkInfoProps) { @@ -84,7 +76,6 @@ export function ResourceLinkInfo({ {name && {name}} - {description && {description}} ); } diff --git a/clients/web/src/components/groups/ResourceLink/ResourceLink.stories.tsx b/clients/web/src/components/groups/ResourceLink/ResourceLink.stories.tsx index 5c4dc89b5..cf79466de 100644 --- a/clients/web/src/components/groups/ResourceLink/ResourceLink.stories.tsx +++ b/clients/web/src/components/groups/ResourceLink/ResourceLink.stories.tsx @@ -41,7 +41,6 @@ export const Static: Story = { args: { uri: URI, name: "Readme", - description: "Project documentation", mimeType: "text/markdown", }, play: async ({ canvasElement }) => { @@ -55,7 +54,6 @@ export const Expandable: Story = { args: { uri: URI, name: "Readme", - description: "Click to read on demand", mimeType: "text/markdown", onReadResource: readMarkdown, }, @@ -66,7 +64,7 @@ export const Expandable: Story = { }); await userEvent.click(button); await waitFor(() => - expect(canvas.getByText("Resource:")).toBeInTheDocument(), + expect(canvas.getByText(/Read on demand/)).toBeInTheDocument(), ); }, }; @@ -77,7 +75,6 @@ export const LargeResult: Story = { args: { uri: BLOB_URI, name: "Blob Resource", - description: "A large gzipped resource", mimeType: "application/gzip", onReadResource: readLargeBlob, }, @@ -86,8 +83,10 @@ export const LargeResult: Story = { await userEvent.click( canvas.getByRole("button", { name: `Expand resource ${BLOB_URI}` }), ); + // The `"blob"` key appears only in the expanded read result's JSON (not in + // the metadata badge), so it confirms the inline result rendered. await waitFor(() => - expect(canvas.getByText("Resource:")).toBeInTheDocument(), + expect(canvas.getByText(/"blob":/)).toBeInTheDocument(), ); }, }; diff --git a/clients/web/src/components/groups/ResourceLink/ResourceLink.test.tsx b/clients/web/src/components/groups/ResourceLink/ResourceLink.test.tsx index f78e84c84..ad820f65f 100644 --- a/clients/web/src/components/groups/ResourceLink/ResourceLink.test.tsx +++ b/clients/web/src/components/groups/ResourceLink/ResourceLink.test.tsx @@ -15,18 +15,12 @@ const readResult = (text: string): ReadResourceResult => ({ }); describe("ResourceLink", () => { - it("renders uri, name, description, and mimeType", () => { + it("renders uri, name, and mimeType", () => { renderWithMantine( - , + , ); expect(screen.getByText(URI)).toBeInTheDocument(); expect(screen.getByText("Readme")).toBeInTheDocument(); - expect(screen.getByText("The project readme")).toBeInTheDocument(); expect(screen.getByText("text/markdown")).toBeInTheDocument(); }); @@ -50,11 +44,10 @@ describe("ResourceLink", () => { await user.click(button); expect(onReadResource).toHaveBeenCalledWith(URI); + // The full read result is rendered inline as formatted JSON. await waitFor(() => - expect(screen.getByText("Resource:")).toBeInTheDocument(), + expect(screen.getByText(/"hello body"/)).toBeInTheDocument(), ); - // The full read result is rendered as formatted JSON. - expect(screen.getByText(/"hello body"/)).toBeInTheDocument(); expect( screen.getByRole("button", { name: `Collapse resource ${URI}` }), ).toHaveAttribute("aria-expanded", "true"); @@ -78,7 +71,7 @@ describe("ResourceLink", () => { await user.click( screen.getByRole("button", { name: `Collapse resource ${URI}` }), ); - expect(screen.queryByText("Resource:")).not.toBeInTheDocument(); + expect(screen.queryByText(/"cached body"/)).not.toBeInTheDocument(); // Re-expand — content returns without a second read. await user.click( diff --git a/clients/web/src/components/groups/ResourceLink/ResourceLink.tsx b/clients/web/src/components/groups/ResourceLink/ResourceLink.tsx index 2e474783a..6e93f823e 100644 --- a/clients/web/src/components/groups/ResourceLink/ResourceLink.tsx +++ b/clients/web/src/components/groups/ResourceLink/ResourceLink.tsx @@ -1,8 +1,8 @@ import { useState } from "react"; import { Alert, + Card, Loader, - Paper, ScrollArea, Stack, Text, @@ -18,8 +18,6 @@ export interface ResourceLinkProps { uri: string; /** Optional human-friendly name shown beneath the URI. */ name?: string; - /** Optional description shown beneath the name. */ - description?: string; /** Optional MIME type shown as a badge. */ mimeType?: string; /** @@ -30,10 +28,15 @@ export interface ResourceLinkProps { onReadResource?: (uri: string) => Promise; } -const LinkCard = Paper.withProps({ +// Recessed "inset" surface so each link card reads the same as a Protocol +// message card (ProtocolEntry), matching its colors in both light and dark +// modes; the inset variant also raises nested Code blocks (the expanded read +// result) onto a lighter surface via its cascade variable. +const LinkCard = Card.withProps({ withBorder: true, - p: "sm", + padding: "sm", radius: "md", + variant: "inset", }); const HeaderButton = UnstyledButton.withProps({ @@ -58,12 +61,6 @@ const ResultScroll = ScrollArea.Autosize.withProps({ offsetScrollbars: true, }); -const ResourceLabel = Text.withProps({ - size: "sm", - fw: 600, - c: "green", -}); - const LoadingText = Text.withProps({ size: "sm", c: "dimmed", @@ -79,7 +76,6 @@ const LoadingText = Text.withProps({ export function ResourceLink({ uri, name, - description, mimeType, onReadResource, }: ResourceLinkProps) { @@ -126,7 +122,6 @@ export function ResourceLink({ @@ -154,18 +149,15 @@ export function ResourceLink({ {error} ) : result !== null ? ( - <> - Resource: - - - - + + + ) : null} )} diff --git a/clients/web/src/components/groups/ToolResultPanel/ToolResultPanel.stories.tsx b/clients/web/src/components/groups/ToolResultPanel/ToolResultPanel.stories.tsx index 059b39949..3d479cf67 100644 --- a/clients/web/src/components/groups/ToolResultPanel/ToolResultPanel.stories.tsx +++ b/clients/web/src/components/groups/ToolResultPanel/ToolResultPanel.stories.tsx @@ -60,6 +60,33 @@ const mixedResult: CallToolResult = { ], }; +const resourceLinksResult: CallToolResult = { + content: [ + { + type: "text", + text: "Here are 3 resource links to resources available in this server:", + }, + { + type: "resource_link", + uri: "demo://resource/dynamic/blob/1", + name: "Blob Resource 1", + mimeType: "text/plain", + }, + { + type: "resource_link", + uri: "demo://resource/dynamic/text/2", + name: "Text Resource 2", + mimeType: "text/plain", + }, + { + type: "resource_link", + uri: "demo://resource/dynamic/blob/3", + name: "Blob Resource 3", + mimeType: "text/plain", + }, + ], +}; + export const Empty: Story = { args: { result: emptyResult, @@ -90,6 +117,18 @@ export const MixedContent: Story = { }, }; +// A run of `resource_link` blocks is grouped into one scrollable "Resource +// Links" box, with each link card in the recessed inset surface that matches +// the Protocol message cards. +export const ResourceLinks: Story = { + args: { + result: resourceLinksResult, + onReadResource: async (uri: string) => ({ + contents: [{ uri, mimeType: "text/plain", text: `Contents of ${uri}` }], + }), + }, +}; + export const ErrorResult: Story = { args: { result: { diff --git a/clients/web/src/components/groups/ToolResultPanel/ToolResultPanel.test.tsx b/clients/web/src/components/groups/ToolResultPanel/ToolResultPanel.test.tsx index 0874cb1e3..84ccd39ab 100644 --- a/clients/web/src/components/groups/ToolResultPanel/ToolResultPanel.test.tsx +++ b/clients/web/src/components/groups/ToolResultPanel/ToolResultPanel.test.tsx @@ -41,7 +41,7 @@ describe("ToolResultPanel", () => { expect(screen.getByText("No results yet")).toBeInTheDocument(); }); - it("renders a resource_link block as an expandable ResourceLink", async () => { + it("groups resource_link blocks in a scrollable Resource Links box", async () => { const user = userEvent.setup(); const onReadResource = vi.fn().mockResolvedValue({ contents: [{ uri: "demo://r/1", text: "linked body" }], @@ -60,6 +60,10 @@ describe("ToolResultPanel", () => { />, ); expect(screen.getByText("ok")).toBeInTheDocument(); + // The link sits inside a grouped, labeled box. + expect( + screen.getByRole("heading", { name: "Resource Links" }), + ).toBeInTheDocument(); await user.click( screen.getByRole("button", { name: "Expand resource demo://r/1" }), ); @@ -69,6 +73,41 @@ describe("ToolResultPanel", () => { ); }); + it("collapses consecutive resource_link blocks into a single box", () => { + const result: CallToolResult = { + content: [ + { type: "text", text: "intro" }, + { type: "resource_link", uri: "demo://r/1", name: "One" }, + { type: "resource_link", uri: "demo://r/2", name: "Two" }, + { type: "resource_link", uri: "demo://r/3", name: "Three" }, + ], + }; + renderWithMantine( {}} />); + // One shared "Resource Links" heading for the whole run of links. + expect( + screen.getAllByRole("heading", { name: "Resource Links" }), + ).toHaveLength(1); + expect(screen.getByText("One")).toBeInTheDocument(); + expect(screen.getByText("Two")).toBeInTheDocument(); + expect(screen.getByText("Three")).toBeInTheDocument(); + }); + + it("renders a separate Resource Links box per non-adjacent run", () => { + const result: CallToolResult = { + content: [ + { type: "resource_link", uri: "demo://r/1", name: "One" }, + { type: "text", text: "divider" }, + { type: "resource_link", uri: "demo://r/2", name: "Two" }, + ], + }; + renderWithMantine( {}} />); + // The text block between the two links splits them into two boxes. + expect( + screen.getAllByRole("heading", { name: "Resource Links" }), + ).toHaveLength(2); + expect(screen.getByText("divider")).toBeInTheDocument(); + }); + it("invokes onClear when the close button is clicked", async () => { const user = userEvent.setup(); const onClear = vi.fn(); diff --git a/clients/web/src/components/groups/ToolResultPanel/ToolResultPanel.tsx b/clients/web/src/components/groups/ToolResultPanel/ToolResultPanel.tsx index 885b39fd9..1f101d460 100644 --- a/clients/web/src/components/groups/ToolResultPanel/ToolResultPanel.tsx +++ b/clients/web/src/components/groups/ToolResultPanel/ToolResultPanel.tsx @@ -2,6 +2,7 @@ import { Alert, CloseButton, Group, + Paper, ScrollArea, Stack, Text, @@ -29,6 +30,36 @@ export interface ToolResultPanelProps { onReadResource?: (uri: string) => Promise; } +type ContentBlock = CallToolResult["content"][number]; +type ResourceLinkBlock = Extract; + +// A result's content is rendered as a run of segments in original order: each +// non-link block on its own, and every maximal run of consecutive +// `resource_link` blocks collapsed into one grouped "Resource Links" box. +type ResultSegment = + | { kind: "links"; links: { block: ResourceLinkBlock; index: number }[] } + | { kind: "block"; block: ContentBlock; index: number }; + +// Walk the content array once, coalescing adjacent `resource_link` blocks into a +// single `links` segment so a run of links renders inside one scrollable box +// while preserving the overall block order. +function segmentContent(content: ContentBlock[]): ResultSegment[] { + const segments: ResultSegment[] = []; + content.forEach((block, index) => { + if (block.type === "resource_link") { + const last = segments[segments.length - 1]; + if (last && last.kind === "links") { + last.links.push({ block, index }); + } else { + segments.push({ kind: "links", links: [{ block, index }] }); + } + } else { + segments.push({ kind: "block", block, index }); + } + }); + return segments; +} + // Outer column: the header pins (`flex: 0 0 auto`) and the scroll region // absorbs overflow when the enclosing card hits its `mah`. `mih: 0` lets the // flex children shrink below their content's intrinsic height. @@ -65,6 +96,39 @@ const ResultStack = Stack.withProps({ gap: "md", }); +// Grouped container for a run of `resource_link` blocks — a bordered box with a +// "Resource Links" heading and its own bounded scroll region, mirroring the +// "Messages" box in the Protocol monitoring sidebar (ProtocolListPanel). +const ResourceLinksBox = Paper.withProps({ + withBorder: true, + radius: "md", + p: "md", +}); + +const ResourceLinksInner = Stack.withProps({ + gap: "sm", +}); + +const ResourceLinksHeader = Title.withProps({ + // The panel "Results" title is h3 (size h4); this sub-box heading is h4 so the + // heading order doesn't skip a level (axe `heading-order`). + order: 4, + size: "h5", +}); + +// Caps the grouped links so a long list scrolls within the box instead of +// pushing the rest of the result down — mirrors the bounded Messages list. +const ResourceLinksScroll = ScrollArea.Autosize.withProps({ + mah: 360, + type: "auto", + scrollbars: "y", + offsetScrollbars: true, +}); + +const ResourceLinksStack = Stack.withProps({ + gap: "sm", +}); + export function ToolResultPanel({ result, onClear, @@ -95,21 +159,31 @@ export function ToolResultPanel({ ) : result.content.length === 0 ? ( No results yet ) : ( - result.content.map((block, index) => - block.type === "resource_link" ? ( - + segmentContent(result.content).map((segment) => + segment.kind === "links" ? ( + + + Resource Links + + + {segment.links.map(({ block, index }) => ( + + ))} + + + + ) : ( ), ) From 151d68085b2ef42a22dda16a8f83ad665637c3d9 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Tue, 14 Jul 2026 13:42:39 -0400 Subject: [PATCH 02/10] web: keep the resource_link description; only drop the "Resource:" label MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reverts the description removal from the previous commit per review — the `description` line is retained on ResourceLinkInfo (and its ContentViewer / ResourceLink callers). Only the redundant green "Resource:" label above the expanded read result stays removed. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01RwZGbocZ64yN9bgb2mj1qE --- .../elements/ContentViewer/ContentViewer.test.tsx | 2 ++ .../elements/ContentViewer/ContentViewer.tsx | 1 + .../ResourceLinkInfo/ResourceLinkInfo.stories.tsx | 1 + .../ResourceLinkInfo/ResourceLinkInfo.test.tsx | 10 ++++++++-- .../ResourceLinkInfo/ResourceLinkInfo.tsx | 15 ++++++++++++--- .../groups/ResourceLink/ResourceLink.stories.tsx | 3 +++ .../groups/ResourceLink/ResourceLink.test.tsx | 10 ++++++++-- .../groups/ResourceLink/ResourceLink.tsx | 4 ++++ .../groups/ToolResultPanel/ToolResultPanel.tsx | 1 + 9 files changed, 40 insertions(+), 7 deletions(-) diff --git a/clients/web/src/components/elements/ContentViewer/ContentViewer.test.tsx b/clients/web/src/components/elements/ContentViewer/ContentViewer.test.tsx index 9ac9e0fbc..1b04403fb 100644 --- a/clients/web/src/components/elements/ContentViewer/ContentViewer.test.tsx +++ b/clients/web/src/components/elements/ContentViewer/ContentViewer.test.tsx @@ -122,10 +122,12 @@ describe("ContentViewer", () => { type: "resource_link", uri: "ui://app", name: "Cool App", + description: "An app", mimeType: "text/html", }; renderWithMantine(); expect(screen.getByText("Cool App")).toBeInTheDocument(); + expect(screen.getByText("An app")).toBeInTheDocument(); expect(screen.getByText("text/html")).toBeInTheDocument(); expect(screen.queryByRole("button")).not.toBeInTheDocument(); }); diff --git a/clients/web/src/components/elements/ContentViewer/ContentViewer.tsx b/clients/web/src/components/elements/ContentViewer/ContentViewer.tsx index fe46a4894..64087d9a1 100644 --- a/clients/web/src/components/elements/ContentViewer/ContentViewer.tsx +++ b/clients/web/src/components/elements/ContentViewer/ContentViewer.tsx @@ -367,6 +367,7 @@ function BlockContent({ ); diff --git a/clients/web/src/components/elements/ResourceLinkInfo/ResourceLinkInfo.stories.tsx b/clients/web/src/components/elements/ResourceLinkInfo/ResourceLinkInfo.stories.tsx index 2b255c26f..cdbecd362 100644 --- a/clients/web/src/components/elements/ResourceLinkInfo/ResourceLinkInfo.stories.tsx +++ b/clients/web/src/components/elements/ResourceLinkInfo/ResourceLinkInfo.stories.tsx @@ -13,6 +13,7 @@ export const Full: Story = { args: { uri: "file:///docs/readme.md", name: "Readme", + description: "Project documentation", mimeType: "text/markdown", }, }; diff --git a/clients/web/src/components/elements/ResourceLinkInfo/ResourceLinkInfo.test.tsx b/clients/web/src/components/elements/ResourceLinkInfo/ResourceLinkInfo.test.tsx index 526b3727c..1a3dedc6b 100644 --- a/clients/web/src/components/elements/ResourceLinkInfo/ResourceLinkInfo.test.tsx +++ b/clients/web/src/components/elements/ResourceLinkInfo/ResourceLinkInfo.test.tsx @@ -5,12 +5,18 @@ import { ResourceLinkInfo } from "./ResourceLinkInfo"; const URI = "file:///docs/readme.md"; describe("ResourceLinkInfo", () => { - it("renders uri, name, and mimeType", () => { + it("renders uri, name, description, and mimeType", () => { renderWithMantine( - , + , ); expect(screen.getByText(URI)).toBeInTheDocument(); expect(screen.getByText("Readme")).toBeInTheDocument(); + expect(screen.getByText("The project readme")).toBeInTheDocument(); expect(screen.getByText("text/markdown")).toBeInTheDocument(); }); diff --git a/clients/web/src/components/elements/ResourceLinkInfo/ResourceLinkInfo.tsx b/clients/web/src/components/elements/ResourceLinkInfo/ResourceLinkInfo.tsx index 77f22f289..71e2b5e17 100644 --- a/clients/web/src/components/elements/ResourceLinkInfo/ResourceLinkInfo.tsx +++ b/clients/web/src/components/elements/ResourceLinkInfo/ResourceLinkInfo.tsx @@ -6,6 +6,8 @@ export interface ResourceLinkInfoProps { uri: string; /** Optional human-friendly name shown beneath the URI. */ name?: string; + /** Optional description shown beneath the name. */ + description?: string; /** Optional MIME type shown as a badge. */ mimeType?: string; /** @@ -54,15 +56,21 @@ const NameText = Text.withProps({ fw: 600, }); +const DescriptionText = Text.withProps({ + size: "sm", + c: "dimmed", +}); + /** * Pure-display metadata for a `resource_link`: the URI (monospace, link-styled), - * an optional name, and a MIME-type badge. The optional `action` slot lets an - * interactive wrapper (e.g. {@link ResourceLink}) place an expand/collapse - * indicator in the URI row. + * optional name / description, and a MIME-type badge. The optional `action` + * slot lets an interactive wrapper (e.g. {@link ResourceLink}) place an + * expand/collapse indicator in the URI row. */ export function ResourceLinkInfo({ uri, name, + description, mimeType, action, }: ResourceLinkInfoProps) { @@ -76,6 +84,7 @@ export function ResourceLinkInfo({ {name && {name}} + {description && {description}} ); } diff --git a/clients/web/src/components/groups/ResourceLink/ResourceLink.stories.tsx b/clients/web/src/components/groups/ResourceLink/ResourceLink.stories.tsx index cf79466de..d308d448f 100644 --- a/clients/web/src/components/groups/ResourceLink/ResourceLink.stories.tsx +++ b/clients/web/src/components/groups/ResourceLink/ResourceLink.stories.tsx @@ -41,6 +41,7 @@ export const Static: Story = { args: { uri: URI, name: "Readme", + description: "Project documentation", mimeType: "text/markdown", }, play: async ({ canvasElement }) => { @@ -54,6 +55,7 @@ export const Expandable: Story = { args: { uri: URI, name: "Readme", + description: "Click to read on demand", mimeType: "text/markdown", onReadResource: readMarkdown, }, @@ -75,6 +77,7 @@ export const LargeResult: Story = { args: { uri: BLOB_URI, name: "Blob Resource", + description: "A large gzipped resource", mimeType: "application/gzip", onReadResource: readLargeBlob, }, diff --git a/clients/web/src/components/groups/ResourceLink/ResourceLink.test.tsx b/clients/web/src/components/groups/ResourceLink/ResourceLink.test.tsx index ad820f65f..dd09fa9bb 100644 --- a/clients/web/src/components/groups/ResourceLink/ResourceLink.test.tsx +++ b/clients/web/src/components/groups/ResourceLink/ResourceLink.test.tsx @@ -15,12 +15,18 @@ const readResult = (text: string): ReadResourceResult => ({ }); describe("ResourceLink", () => { - it("renders uri, name, and mimeType", () => { + it("renders uri, name, description, and mimeType", () => { renderWithMantine( - , + , ); expect(screen.getByText(URI)).toBeInTheDocument(); expect(screen.getByText("Readme")).toBeInTheDocument(); + expect(screen.getByText("The project readme")).toBeInTheDocument(); expect(screen.getByText("text/markdown")).toBeInTheDocument(); }); diff --git a/clients/web/src/components/groups/ResourceLink/ResourceLink.tsx b/clients/web/src/components/groups/ResourceLink/ResourceLink.tsx index 6e93f823e..dc21fbd46 100644 --- a/clients/web/src/components/groups/ResourceLink/ResourceLink.tsx +++ b/clients/web/src/components/groups/ResourceLink/ResourceLink.tsx @@ -18,6 +18,8 @@ export interface ResourceLinkProps { uri: string; /** Optional human-friendly name shown beneath the URI. */ name?: string; + /** Optional description shown beneath the name. */ + description?: string; /** Optional MIME type shown as a badge. */ mimeType?: string; /** @@ -76,6 +78,7 @@ const LoadingText = Text.withProps({ export function ResourceLink({ uri, name, + description, mimeType, onReadResource, }: ResourceLinkProps) { @@ -122,6 +125,7 @@ export function ResourceLink({ diff --git a/clients/web/src/components/groups/ToolResultPanel/ToolResultPanel.tsx b/clients/web/src/components/groups/ToolResultPanel/ToolResultPanel.tsx index 1f101d460..c738853d5 100644 --- a/clients/web/src/components/groups/ToolResultPanel/ToolResultPanel.tsx +++ b/clients/web/src/components/groups/ToolResultPanel/ToolResultPanel.tsx @@ -171,6 +171,7 @@ export function ToolResultPanel({ key={index} uri={block.uri} name={block.name} + description={block.description} mimeType={block.mimeType} onReadResource={onReadResource} /> From d7b43e3885bccb1c20f57823ff7865e2ec77bbb0 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Tue, 14 Jul 2026 14:18:03 -0400 Subject: [PATCH 03/10] web: drop the resource_link description from the link cards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `description` is generated by the server and can be misleading — e.g. a blob resource served with a `text/plain` mimeType gets described as a "plaintext resource" even though the payload is a blob. Rather than surface that nonsense text, drop the description line from ResourceLinkInfo (and its ContentViewer / ResourceLink callers) entirely. Reverts the description-retention from the prior commit; the grouped "Resource Links" box and the "Resource:" label removal are unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01RwZGbocZ64yN9bgb2mj1qE --- .../elements/ContentViewer/ContentViewer.test.tsx | 2 -- .../elements/ContentViewer/ContentViewer.tsx | 1 - .../ResourceLinkInfo/ResourceLinkInfo.stories.tsx | 1 - .../ResourceLinkInfo/ResourceLinkInfo.test.tsx | 10 ++-------- .../ResourceLinkInfo/ResourceLinkInfo.tsx | 15 +++------------ .../groups/ResourceLink/ResourceLink.stories.tsx | 3 --- .../groups/ResourceLink/ResourceLink.test.tsx | 10 ++-------- .../groups/ResourceLink/ResourceLink.tsx | 4 ---- .../groups/ToolResultPanel/ToolResultPanel.tsx | 1 - 9 files changed, 7 insertions(+), 40 deletions(-) diff --git a/clients/web/src/components/elements/ContentViewer/ContentViewer.test.tsx b/clients/web/src/components/elements/ContentViewer/ContentViewer.test.tsx index 1b04403fb..9ac9e0fbc 100644 --- a/clients/web/src/components/elements/ContentViewer/ContentViewer.test.tsx +++ b/clients/web/src/components/elements/ContentViewer/ContentViewer.test.tsx @@ -122,12 +122,10 @@ describe("ContentViewer", () => { type: "resource_link", uri: "ui://app", name: "Cool App", - description: "An app", mimeType: "text/html", }; renderWithMantine(); expect(screen.getByText("Cool App")).toBeInTheDocument(); - expect(screen.getByText("An app")).toBeInTheDocument(); expect(screen.getByText("text/html")).toBeInTheDocument(); expect(screen.queryByRole("button")).not.toBeInTheDocument(); }); diff --git a/clients/web/src/components/elements/ContentViewer/ContentViewer.tsx b/clients/web/src/components/elements/ContentViewer/ContentViewer.tsx index 64087d9a1..fe46a4894 100644 --- a/clients/web/src/components/elements/ContentViewer/ContentViewer.tsx +++ b/clients/web/src/components/elements/ContentViewer/ContentViewer.tsx @@ -367,7 +367,6 @@ function BlockContent({ ); diff --git a/clients/web/src/components/elements/ResourceLinkInfo/ResourceLinkInfo.stories.tsx b/clients/web/src/components/elements/ResourceLinkInfo/ResourceLinkInfo.stories.tsx index cdbecd362..2b255c26f 100644 --- a/clients/web/src/components/elements/ResourceLinkInfo/ResourceLinkInfo.stories.tsx +++ b/clients/web/src/components/elements/ResourceLinkInfo/ResourceLinkInfo.stories.tsx @@ -13,7 +13,6 @@ export const Full: Story = { args: { uri: "file:///docs/readme.md", name: "Readme", - description: "Project documentation", mimeType: "text/markdown", }, }; diff --git a/clients/web/src/components/elements/ResourceLinkInfo/ResourceLinkInfo.test.tsx b/clients/web/src/components/elements/ResourceLinkInfo/ResourceLinkInfo.test.tsx index 1a3dedc6b..526b3727c 100644 --- a/clients/web/src/components/elements/ResourceLinkInfo/ResourceLinkInfo.test.tsx +++ b/clients/web/src/components/elements/ResourceLinkInfo/ResourceLinkInfo.test.tsx @@ -5,18 +5,12 @@ import { ResourceLinkInfo } from "./ResourceLinkInfo"; const URI = "file:///docs/readme.md"; describe("ResourceLinkInfo", () => { - it("renders uri, name, description, and mimeType", () => { + it("renders uri, name, and mimeType", () => { renderWithMantine( - , + , ); expect(screen.getByText(URI)).toBeInTheDocument(); expect(screen.getByText("Readme")).toBeInTheDocument(); - expect(screen.getByText("The project readme")).toBeInTheDocument(); expect(screen.getByText("text/markdown")).toBeInTheDocument(); }); diff --git a/clients/web/src/components/elements/ResourceLinkInfo/ResourceLinkInfo.tsx b/clients/web/src/components/elements/ResourceLinkInfo/ResourceLinkInfo.tsx index 71e2b5e17..77f22f289 100644 --- a/clients/web/src/components/elements/ResourceLinkInfo/ResourceLinkInfo.tsx +++ b/clients/web/src/components/elements/ResourceLinkInfo/ResourceLinkInfo.tsx @@ -6,8 +6,6 @@ export interface ResourceLinkInfoProps { uri: string; /** Optional human-friendly name shown beneath the URI. */ name?: string; - /** Optional description shown beneath the name. */ - description?: string; /** Optional MIME type shown as a badge. */ mimeType?: string; /** @@ -56,21 +54,15 @@ const NameText = Text.withProps({ fw: 600, }); -const DescriptionText = Text.withProps({ - size: "sm", - c: "dimmed", -}); - /** * Pure-display metadata for a `resource_link`: the URI (monospace, link-styled), - * optional name / description, and a MIME-type badge. The optional `action` - * slot lets an interactive wrapper (e.g. {@link ResourceLink}) place an - * expand/collapse indicator in the URI row. + * an optional name, and a MIME-type badge. The optional `action` slot lets an + * interactive wrapper (e.g. {@link ResourceLink}) place an expand/collapse + * indicator in the URI row. */ export function ResourceLinkInfo({ uri, name, - description, mimeType, action, }: ResourceLinkInfoProps) { @@ -84,7 +76,6 @@ export function ResourceLinkInfo({ {name && {name}} - {description && {description}} ); } diff --git a/clients/web/src/components/groups/ResourceLink/ResourceLink.stories.tsx b/clients/web/src/components/groups/ResourceLink/ResourceLink.stories.tsx index d308d448f..cf79466de 100644 --- a/clients/web/src/components/groups/ResourceLink/ResourceLink.stories.tsx +++ b/clients/web/src/components/groups/ResourceLink/ResourceLink.stories.tsx @@ -41,7 +41,6 @@ export const Static: Story = { args: { uri: URI, name: "Readme", - description: "Project documentation", mimeType: "text/markdown", }, play: async ({ canvasElement }) => { @@ -55,7 +54,6 @@ export const Expandable: Story = { args: { uri: URI, name: "Readme", - description: "Click to read on demand", mimeType: "text/markdown", onReadResource: readMarkdown, }, @@ -77,7 +75,6 @@ export const LargeResult: Story = { args: { uri: BLOB_URI, name: "Blob Resource", - description: "A large gzipped resource", mimeType: "application/gzip", onReadResource: readLargeBlob, }, diff --git a/clients/web/src/components/groups/ResourceLink/ResourceLink.test.tsx b/clients/web/src/components/groups/ResourceLink/ResourceLink.test.tsx index dd09fa9bb..ad820f65f 100644 --- a/clients/web/src/components/groups/ResourceLink/ResourceLink.test.tsx +++ b/clients/web/src/components/groups/ResourceLink/ResourceLink.test.tsx @@ -15,18 +15,12 @@ const readResult = (text: string): ReadResourceResult => ({ }); describe("ResourceLink", () => { - it("renders uri, name, description, and mimeType", () => { + it("renders uri, name, and mimeType", () => { renderWithMantine( - , + , ); expect(screen.getByText(URI)).toBeInTheDocument(); expect(screen.getByText("Readme")).toBeInTheDocument(); - expect(screen.getByText("The project readme")).toBeInTheDocument(); expect(screen.getByText("text/markdown")).toBeInTheDocument(); }); diff --git a/clients/web/src/components/groups/ResourceLink/ResourceLink.tsx b/clients/web/src/components/groups/ResourceLink/ResourceLink.tsx index dc21fbd46..6e93f823e 100644 --- a/clients/web/src/components/groups/ResourceLink/ResourceLink.tsx +++ b/clients/web/src/components/groups/ResourceLink/ResourceLink.tsx @@ -18,8 +18,6 @@ export interface ResourceLinkProps { uri: string; /** Optional human-friendly name shown beneath the URI. */ name?: string; - /** Optional description shown beneath the name. */ - description?: string; /** Optional MIME type shown as a badge. */ mimeType?: string; /** @@ -78,7 +76,6 @@ const LoadingText = Text.withProps({ export function ResourceLink({ uri, name, - description, mimeType, onReadResource, }: ResourceLinkProps) { @@ -125,7 +122,6 @@ export function ResourceLink({ diff --git a/clients/web/src/components/groups/ToolResultPanel/ToolResultPanel.tsx b/clients/web/src/components/groups/ToolResultPanel/ToolResultPanel.tsx index c738853d5..1f101d460 100644 --- a/clients/web/src/components/groups/ToolResultPanel/ToolResultPanel.tsx +++ b/clients/web/src/components/groups/ToolResultPanel/ToolResultPanel.tsx @@ -171,7 +171,6 @@ export function ToolResultPanel({ key={index} uri={block.uri} name={block.name} - description={block.description} mimeType={block.mimeType} onReadResource={onReadResource} /> From 0651c8868ea2537108207c70d556c0ed3a18cf9d Mon Sep 17 00:00:00 2001 From: cliffhall Date: Tue, 14 Jul 2026 14:52:33 -0400 Subject: [PATCH 04/10] =?UTF-8?q?web:=20refine=20resource-link=20cards=20?= =?UTF-8?q?=E2=80=94=20URI-below-name=20+=20copy,=20ProtocolEntry=20toggle?= =?UTF-8?q?,=20full-height=20box?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up refinements to the Resource Links display: - ResourceLinkInfo: move the URI below the name and add a CopyButton beside it so the URI can be copied. The name + MIME badge (and the optional expand control) sit on the header row; the URI + copy sit on the row below. - ResourceLink: replace the bespoke chevron with the same tooltip'd ExpandToggle used by ProtocolEntry, and animate the read-result panel with Mantine's Collapse (same as ProtocolEntry). The toggle is a sibling of the URI copy button (no card-wide wrapping button) so the two real buttons never nest (axe `nested-interactive`). - ToolResultPanel: when a result contains a Resource Links box, fill the card's height so the box grows into the available space and scrolls internally (pinned "Resource Links" heading), instead of capping at a fixed content height. Plain text/image results keep the scroll-within-card body. - ToolsScreen: give the result card `flex: 1` so it fills the pane, providing the definite height the Resource Links box grows into. - Tests/stories updated for the new layout, the Expand/Collapse toggle labels, the mounted-through-collapse read result, and the full-height box (with a height-bounded decorator on the ResourceLinks story). Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01RwZGbocZ64yN9bgb2mj1qE --- .../ContentViewer/ContentViewer.test.tsx | 6 +- .../ResourceLinkInfo/ResourceLinkInfo.tsx | 49 ++++-- .../ResourceLink/ResourceLink.stories.tsx | 15 +- .../groups/ResourceLink/ResourceLink.test.tsx | 67 +++----- .../groups/ResourceLink/ResourceLink.tsx | 103 +++++------- .../ToolResultPanel.stories.tsx | 14 +- .../ToolResultPanel/ToolResultPanel.test.tsx | 4 +- .../ToolResultPanel/ToolResultPanel.tsx | 158 ++++++++++++------ .../screens/ToolsScreen/ToolsScreen.tsx | 6 +- 9 files changed, 234 insertions(+), 188 deletions(-) diff --git a/clients/web/src/components/elements/ContentViewer/ContentViewer.test.tsx b/clients/web/src/components/elements/ContentViewer/ContentViewer.test.tsx index 9ac9e0fbc..4cebc3e70 100644 --- a/clients/web/src/components/elements/ContentViewer/ContentViewer.test.tsx +++ b/clients/web/src/components/elements/ContentViewer/ContentViewer.test.tsx @@ -127,7 +127,11 @@ describe("ContentViewer", () => { renderWithMantine(); expect(screen.getByText("Cool App")).toBeInTheDocument(); expect(screen.getByText("text/html")).toBeInTheDocument(); - expect(screen.queryByRole("button")).not.toBeInTheDocument(); + // The URI copy button is present, but there's no expand control. + expect(screen.getByRole("button", { name: "Copy" })).toBeInTheDocument(); + expect( + screen.queryByRole("button", { name: "Expand" }), + ).not.toBeInTheDocument(); }); it("renders nothing for unknown block types", () => { diff --git a/clients/web/src/components/elements/ResourceLinkInfo/ResourceLinkInfo.tsx b/clients/web/src/components/elements/ResourceLinkInfo/ResourceLinkInfo.tsx index 77f22f289..575b0061d 100644 --- a/clients/web/src/components/elements/ResourceLinkInfo/ResourceLinkInfo.tsx +++ b/clients/web/src/components/elements/ResourceLinkInfo/ResourceLinkInfo.tsx @@ -1,16 +1,18 @@ import type { ReactNode } from "react"; import { Badge, Group, Stack, Text } from "@mantine/core"; +import { CopyButton } from "../CopyButton/CopyButton"; export interface ResourceLinkInfoProps { - /** The linked resource's URI (always shown). */ + /** The linked resource's URI (always shown, with a copy button). */ uri: string; - /** Optional human-friendly name shown beneath the URI. */ + /** Optional human-friendly name shown above the URI. */ name?: string; /** Optional MIME type shown as a badge. */ mimeType?: string; /** - * Optional trailing element placed at the end of the URI row — e.g. an - * expand/collapse indicator supplied by an interactive wrapper. + * Optional trailing element placed at the end of the header row (beside the + * MIME badge) — e.g. an expand/collapse control supplied by an interactive + * wrapper. */ action?: ReactNode; } @@ -19,13 +21,21 @@ const HeaderStack = Stack.withProps({ gap: 4, }); -const UriRow = Group.withProps({ - justify: "space-between", +// Name + meta (MIME badge, action). `justify` is set per-instance: spread when +// a name is present, otherwise the meta hugs the right. +const HeaderRow = Group.withProps({ wrap: "nowrap", gap: "xs", align: "flex-start", }); +// Copy control + the URI, on the line below the name. +const UriRow = Group.withProps({ + wrap: "nowrap", + gap: "xs", + align: "center", +}); + const UriText = Text.withProps({ size: "sm", c: "blue", @@ -52,13 +62,16 @@ const MimeBadge = Badge.withProps({ const NameText = Text.withProps({ size: "sm", fw: 600, + flex: 1, + miw: 0, }); /** - * Pure-display metadata for a `resource_link`: the URI (monospace, link-styled), - * an optional name, and a MIME-type badge. The optional `action` slot lets an - * interactive wrapper (e.g. {@link ResourceLink}) place an expand/collapse - * indicator in the URI row. + * Pure-display metadata for a `resource_link`: an optional name and MIME-type + * badge on the header row, then the URI (monospace, link-styled) on the line + * below with a copy button. The optional `action` slot lets an interactive + * wrapper (e.g. {@link ResourceLink}) place an expand/collapse control beside + * the MIME badge. */ export function ResourceLinkInfo({ uri, @@ -66,16 +79,22 @@ export function ResourceLinkInfo({ mimeType, action, }: ResourceLinkInfoProps) { + const hasHeader = Boolean(name || mimeType || action); return ( + {hasHeader && ( + + {name && {name}} + + {mimeType && {mimeType}} + {action} + + + )} + {uri} - - {mimeType && {mimeType}} - {action} - - {name && {name}} ); } diff --git a/clients/web/src/components/groups/ResourceLink/ResourceLink.stories.tsx b/clients/web/src/components/groups/ResourceLink/ResourceLink.stories.tsx index cf79466de..9db1cee74 100644 --- a/clients/web/src/components/groups/ResourceLink/ResourceLink.stories.tsx +++ b/clients/web/src/components/groups/ResourceLink/ResourceLink.stories.tsx @@ -46,7 +46,11 @@ export const Static: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); expect(canvas.getByText(URI)).toBeInTheDocument(); - expect(canvas.queryByRole("button")).not.toBeInTheDocument(); + // Copy button is present; there's no expand control in the static card. + expect(canvas.getByRole("button", { name: "Copy" })).toBeInTheDocument(); + expect( + canvas.queryByRole("button", { name: "Expand" }), + ).not.toBeInTheDocument(); }, }; @@ -59,10 +63,7 @@ export const Expandable: Story = { }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); - const button = canvas.getByRole("button", { - name: `Expand resource ${URI}`, - }); - await userEvent.click(button); + await userEvent.click(canvas.getByRole("button", { name: "Expand" })); await waitFor(() => expect(canvas.getByText(/Read on demand/)).toBeInTheDocument(), ); @@ -80,9 +81,7 @@ export const LargeResult: Story = { }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); - await userEvent.click( - canvas.getByRole("button", { name: `Expand resource ${BLOB_URI}` }), - ); + await userEvent.click(canvas.getByRole("button", { name: "Expand" })); // The `"blob"` key appears only in the expanded read result's JSON (not in // the metadata badge), so it confirms the inline result rendered. await waitFor(() => diff --git a/clients/web/src/components/groups/ResourceLink/ResourceLink.test.tsx b/clients/web/src/components/groups/ResourceLink/ResourceLink.test.tsx index ad820f65f..4eb484288 100644 --- a/clients/web/src/components/groups/ResourceLink/ResourceLink.test.tsx +++ b/clients/web/src/components/groups/ResourceLink/ResourceLink.test.tsx @@ -24,9 +24,13 @@ describe("ResourceLink", () => { expect(screen.getByText("text/markdown")).toBeInTheDocument(); }); - it("is not interactive without onReadResource", () => { + it("renders no expand control without onReadResource", () => { renderWithMantine(); - expect(screen.queryByRole("button")).not.toBeInTheDocument(); + // The URI copy button is always present, but there's no expand affordance. + expect(screen.getByRole("button", { name: "Copy" })).toBeInTheDocument(); + expect( + screen.queryByRole("button", { name: "Expand" }), + ).not.toBeInTheDocument(); }); it("reads the resource on demand and renders the result inline", async () => { @@ -36,21 +40,17 @@ describe("ResourceLink", () => { , ); - const button = screen.getByRole("button", { - name: `Expand resource ${URI}`, - }); - expect(button).toHaveAttribute("aria-expanded", "false"); - - await user.click(button); + await user.click(screen.getByRole("button", { name: "Expand" })); expect(onReadResource).toHaveBeenCalledWith(URI); // The full read result is rendered inline as formatted JSON. await waitFor(() => expect(screen.getByText(/"hello body"/)).toBeInTheDocument(), ); + // The toggle flips to the collapse control once expanded. expect( - screen.getByRole("button", { name: `Collapse resource ${URI}` }), - ).toHaveAttribute("aria-expanded", "true"); + screen.getByRole("button", { name: "Collapse" }), + ).toBeInTheDocument(); }); it("collapses and re-expands without re-reading (result cached)", async () => { @@ -60,23 +60,18 @@ describe("ResourceLink", () => { , ); - await user.click( - screen.getByRole("button", { name: `Expand resource ${URI}` }), - ); + await user.click(screen.getByRole("button", { name: "Expand" })); await waitFor(() => expect(screen.getByText(/"cached body"/)).toBeInTheDocument(), ); - // Collapse — content is hidden. - await user.click( - screen.getByRole("button", { name: `Collapse resource ${URI}` }), - ); - expect(screen.queryByText(/"cached body"/)).not.toBeInTheDocument(); + // Collapse — the toggle flips back to the expand control. (The read result + // stays mounted inside the animated Collapse, so it isn't re-read.) + await user.click(screen.getByRole("button", { name: "Collapse" })); + expect(screen.getByRole("button", { name: "Expand" })).toBeInTheDocument(); - // Re-expand — content returns without a second read. - await user.click( - screen.getByRole("button", { name: `Expand resource ${URI}` }), - ); + // Re-expand — content is still present without a second read. + await user.click(screen.getByRole("button", { name: "Expand" })); expect(screen.getByText(/"cached body"/)).toBeInTheDocument(); expect(onReadResource).toHaveBeenCalledTimes(1); }); @@ -88,7 +83,7 @@ describe("ResourceLink", () => { , ); - await user.click(screen.getByRole("button")); + await user.click(screen.getByRole("button", { name: "Expand" })); await waitFor(() => expect(screen.getByText("Failed to read resource")).toBeInTheDocument(), ); @@ -104,7 +99,7 @@ describe("ResourceLink", () => { , ); - await user.click(screen.getByRole("button")); + await user.click(screen.getByRole("button", { name: "Expand" })); await waitFor(() => expect(screen.getByText("Failed to read resource")).toBeInTheDocument(), ); @@ -122,20 +117,14 @@ describe("ResourceLink", () => { ); // First expand fails. - await user.click( - screen.getByRole("button", { name: `Expand resource ${URI}` }), - ); + await user.click(screen.getByRole("button", { name: "Expand" })); await waitFor(() => expect(screen.getByText("Failed to read resource")).toBeInTheDocument(), ); // Collapse, then re-expand — the read is retried (error is not cached). - await user.click( - screen.getByRole("button", { name: `Collapse resource ${URI}` }), - ); - await user.click( - screen.getByRole("button", { name: `Expand resource ${URI}` }), - ); + await user.click(screen.getByRole("button", { name: "Collapse" })); + await user.click(screen.getByRole("button", { name: "Expand" })); await waitFor(() => expect(screen.getByText(/"recovered body"/)).toBeInTheDocument(), @@ -160,15 +149,9 @@ describe("ResourceLink", () => { ); // Expand — read is in flight (loading), then collapse and re-expand. - await user.click( - screen.getByRole("button", { name: `Expand resource ${URI}` }), - ); - await user.click( - screen.getByRole("button", { name: `Collapse resource ${URI}` }), - ); - await user.click( - screen.getByRole("button", { name: `Expand resource ${URI}` }), - ); + await user.click(screen.getByRole("button", { name: "Expand" })); + await user.click(screen.getByRole("button", { name: "Collapse" })); + await user.click(screen.getByRole("button", { name: "Expand" })); // Still only the original in-flight read — no redundant fetch. expect(onReadResource).toHaveBeenCalledTimes(1); diff --git a/clients/web/src/components/groups/ResourceLink/ResourceLink.tsx b/clients/web/src/components/groups/ResourceLink/ResourceLink.tsx index 6e93f823e..cff3a7ea0 100644 --- a/clients/web/src/components/groups/ResourceLink/ResourceLink.tsx +++ b/clients/web/src/components/groups/ResourceLink/ResourceLink.tsx @@ -1,22 +1,14 @@ import { useState } from "react"; -import { - Alert, - Card, - Loader, - ScrollArea, - Stack, - Text, - UnstyledButton, -} from "@mantine/core"; +import { Alert, Card, Collapse, ScrollArea, Stack, Text } from "@mantine/core"; import type { ReadResourceResult } from "@modelcontextprotocol/sdk/types.js"; -import { RiArrowDownSLine, RiArrowRightSLine } from "react-icons/ri"; import { ContentViewer } from "../../elements/ContentViewer/ContentViewer"; +import { ExpandToggle } from "../../elements/ExpandToggle/ExpandToggle"; import { ResourceLinkInfo } from "../../elements/ResourceLinkInfo/ResourceLinkInfo"; export interface ResourceLinkProps { /** The linked resource's URI (always shown). */ uri: string; - /** Optional human-friendly name shown beneath the URI. */ + /** Optional human-friendly name shown above the URI. */ name?: string; /** Optional MIME type shown as a badge. */ mimeType?: string; @@ -39,11 +31,6 @@ const LinkCard = Card.withProps({ variant: "inset", }); -const HeaderButton = UnstyledButton.withProps({ - w: "100%", - ta: "left", -}); - const ExpandedSection = Stack.withProps({ gap: "xs", mt: "xs", @@ -109,57 +96,49 @@ export function ResourceLink({ } } - const Chevron = expanded ? RiArrowDownSLine : RiArrowRightSLine; + // Same tooltip'd expand/collapse control as ProtocolEntry (ExpandToggle), + // placed in the header row's meta slot as a sibling of the URI's copy button. const action = expandable ? ( - loading ? ( - - ) : ( - - ) + void toggle()} /> ) : undefined; - const info = ( - - ); - return ( - {expandable ? ( - void toggle()} - aria-expanded={expanded} - aria-label={`${expanded ? "Collapse" : "Expand"} resource ${uri}`} - > - {info} - - ) : ( - info - )} - {expanded && ( - - {loading ? ( - Loading resource… - ) : error !== null ? ( - - {error} - - ) : result !== null ? ( - - - - ) : null} - + + {/* Same expand/collapse animation as ProtocolEntry: content stays mounted + (so the cached read result survives a collapse) and animates via + Mantine's Collapse. */} + {expandable && ( + + + {loading ? ( + Loading resource… + ) : error !== null ? ( + + {error} + + ) : result !== null ? ( + + + + ) : null} + + )} ); diff --git a/clients/web/src/components/groups/ToolResultPanel/ToolResultPanel.stories.tsx b/clients/web/src/components/groups/ToolResultPanel/ToolResultPanel.stories.tsx index 3d479cf67..98a3ae08e 100644 --- a/clients/web/src/components/groups/ToolResultPanel/ToolResultPanel.stories.tsx +++ b/clients/web/src/components/groups/ToolResultPanel/ToolResultPanel.stories.tsx @@ -1,5 +1,6 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import { Card, Flex } from "@mantine/core"; import { fn } from "storybook/test"; import { ToolResultPanel } from "./ToolResultPanel"; @@ -119,7 +120,9 @@ export const MixedContent: Story = { // A run of `resource_link` blocks is grouped into one scrollable "Resource // Links" box, with each link card in the recessed inset surface that matches -// the Protocol message cards. +// the Protocol message cards. The decorator mirrors the Tools screen's +// full-height result card (ContentPane height → ContentCard `flex: 1`) so the +// box fills the available space and scrolls within, as it does in the app. export const ResourceLinks: Story = { args: { result: resourceLinksResult, @@ -127,6 +130,15 @@ export const ResourceLinks: Story = { contents: [{ uri, mimeType: "text/plain", text: `Contents of ${uri}` }], }), }, + decorators: [ + (Story) => ( + + + + + + ), + ], }; export const ErrorResult: Story = { diff --git a/clients/web/src/components/groups/ToolResultPanel/ToolResultPanel.test.tsx b/clients/web/src/components/groups/ToolResultPanel/ToolResultPanel.test.tsx index 84ccd39ab..5c1e67b12 100644 --- a/clients/web/src/components/groups/ToolResultPanel/ToolResultPanel.test.tsx +++ b/clients/web/src/components/groups/ToolResultPanel/ToolResultPanel.test.tsx @@ -64,9 +64,7 @@ describe("ToolResultPanel", () => { expect( screen.getByRole("heading", { name: "Resource Links" }), ).toBeInTheDocument(); - await user.click( - screen.getByRole("button", { name: "Expand resource demo://r/1" }), - ); + await user.click(screen.getByRole("button", { name: "Expand" })); expect(onReadResource).toHaveBeenCalledWith("demo://r/1"); await waitFor(() => expect(screen.getByText(/"linked body"/)).toBeInTheDocument(), diff --git a/clients/web/src/components/groups/ToolResultPanel/ToolResultPanel.tsx b/clients/web/src/components/groups/ToolResultPanel/ToolResultPanel.tsx index 1f101d460..2d3c88f8b 100644 --- a/clients/web/src/components/groups/ToolResultPanel/ToolResultPanel.tsx +++ b/clients/web/src/components/groups/ToolResultPanel/ToolResultPanel.tsx @@ -60,13 +60,14 @@ function segmentContent(content: ContentBlock[]): ResultSegment[] { return segments; } -// Outer column: the header pins (`flex: 0 0 auto`) and the scroll region -// absorbs overflow when the enclosing card hits its `mah`. `mih: 0` lets the -// flex children shrink below their content's intrinsic height. +// Outer column fills the (full-height) result card: the header pins +// (`flex: 0 0 auto`) and the body below fills the rest. `mih: 0` lets the flex +// children shrink below their content's intrinsic height. const PanelStack = Stack.withProps({ gap: "md", miw: 0, mih: 0, + flex: 1, }); const HeaderRow = Group.withProps({ @@ -81,10 +82,10 @@ const HeaderLeft = Group.withProps({ wrap: "nowrap", }); -// `0 1 auto` + `mih: 0` lets the scroll region shrink (never grow) so a short -// result doesn't reserve height while a long one scrolls within the card cap. +// Body scroll region for non-resource-link results: fills the card and scrolls +// within it when the content is taller than the available space. const ResultScroll = ScrollArea.withProps({ - flex: "0 1 auto", + flex: 1, miw: 0, mih: 0, type: "auto", @@ -96,17 +97,33 @@ const ResultStack = Stack.withProps({ gap: "md", }); +// Body column for results that contain a "Resource Links" box: it fills the +// card so the box (which is `flex: 1` within it) can grow to the available +// height and scroll internally, rather than capping at its content height. +const FillStack = Stack.withProps({ + gap: "md", + flex: 1, + mih: 0, +}); + // Grouped container for a run of `resource_link` blocks — a bordered box with a -// "Resource Links" heading and its own bounded scroll region, mirroring the -// "Messages" box in the Protocol monitoring sidebar (ProtocolListPanel). +// pinned "Resource Links" heading and its own scroll region, mirroring the +// "Messages" box in the Protocol monitoring sidebar (ProtocolListPanel). The +// `panel` variant makes it a flex column (overflow hidden, min-height 0) and +// `flex: 1` lets it grow to fill the result card's available height. const ResourceLinksBox = Paper.withProps({ withBorder: true, radius: "md", p: "md", + variant: "panel", + flex: 1, + mih: 0, }); const ResourceLinksInner = Stack.withProps({ gap: "sm", + flex: 1, + mih: 0, }); const ResourceLinksHeader = Title.withProps({ @@ -116,10 +133,10 @@ const ResourceLinksHeader = Title.withProps({ size: "h5", }); -// Caps the grouped links so a long list scrolls within the box instead of -// pushing the rest of the result down — mirrors the bounded Messages list. -const ResourceLinksScroll = ScrollArea.Autosize.withProps({ - mah: 360, +// Fills the box below the pinned heading and scrolls the link list within it. +const ResourceLinksScroll = ScrollArea.withProps({ + flex: 1, + mih: 0, type: "auto", scrollbars: "y", offsetScrollbars: true, @@ -129,11 +146,65 @@ const ResourceLinksStack = Stack.withProps({ gap: "sm", }); +function ResourceLinksGroup({ + links, + onReadResource, +}: { + links: { block: ResourceLinkBlock; index: number }[]; + onReadResource?: (uri: string) => Promise; +}) { + return ( + + + Resource Links + + + {links.map(({ block, index }) => ( + + ))} + + + + + ); +} + export function ToolResultPanel({ result, onClear, onReadResource, }: ToolResultPanelProps) { + const segments = + result.isError || result.content.length === 0 + ? [] + : segmentContent(result.content); + // Results with a Resource Links box fill the card so the box grows to the + // available height (and scrolls inside). Plain text/image results keep the + // scroll-within-card body so a short result doesn't reserve empty height. + const hasLinks = segments.some((s) => s.kind === "links"); + + const segmentNodes = segments.map((segment) => + segment.kind === "links" ? ( + + ) : ( + + ), + ); + return ( @@ -147,49 +218,26 @@ export function ToolResultPanel({ - - - {result.isError ? ( - - {result.content - .filter((b) => b.type === "text") - .map((b) => b.text) - .join("\n")} - - ) : result.content.length === 0 ? ( - No results yet - ) : ( - segmentContent(result.content).map((segment) => - segment.kind === "links" ? ( - - - Resource Links - - - {segment.links.map(({ block, index }) => ( - - ))} - - - - - ) : ( - - ), - ) - )} - - + {result.isError ? ( + + + {result.content + .filter((b) => b.type === "text") + .map((b) => b.text) + .join("\n")} + + + ) : result.content.length === 0 ? ( + + No results yet + + ) : hasLinks ? ( + {segmentNodes} + ) : ( + + {segmentNodes} + + )} ); } diff --git a/clients/web/src/components/screens/ToolsScreen/ToolsScreen.tsx b/clients/web/src/components/screens/ToolsScreen/ToolsScreen.tsx index aa312e18a..86584b3d0 100644 --- a/clients/web/src/components/screens/ToolsScreen/ToolsScreen.tsx +++ b/clients/web/src/components/screens/ToolsScreen/ToolsScreen.tsx @@ -181,7 +181,11 @@ export function ToolsScreen({ // `result` (App.tsx), so the executing form (progress + cancel) shows // until the result lands. - + {/* `flex={1}` makes the result card fill the pane's full height (not + just size to content) so ToolResultPanel's inner scroll regions — + in particular a "Resource Links" box — expand into the available + space and scroll within, instead of leaving the card short. */} + onClearResult?.()} From 47d96020145fb50e882877cb5fca96373fd6ad7a Mon Sep 17 00:00:00 2001 From: cliffhall Date: Tue, 14 Jul 2026 15:07:58 -0400 Subject: [PATCH 05/10] =?UTF-8?q?web:=20align=20resource-link=20card=20wit?= =?UTF-8?q?h=20ProtocolEntry=20=E2=80=94=20toggle=20row,=20badge,=20URI=20?= =?UTF-8?q?style?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bring ResourceLinkInfo closer to the ProtocolEntry message card: - Move the expand/collapse toggle to the second (URI) row, opposite the copy button — mirroring ProtocolEntry, whose toggle sits on the row below the badges. The MIME badge stays on the header row's right, beside the name. - MIME badge: bump to `size="md"` so the lowercase MIME text matches the point size of ProtocolEntry's uppercase method/status badges, and make it a solid dark-blue fill with white text in dark mode (keeping the blue-light tint in light mode) instead of a washed-out translucent chip. - Render the URI like ProtocolEntry/NetworkEntry render URLs — `sm` / `fw: 500` in the default sans-serif face and text color — rather than a blue monospace "link". `monoBreak` is kept only for long-URI wrapping. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01RwZGbocZ64yN9bgb2mj1qE --- .../ResourceLinkInfo/ResourceLinkInfo.tsx | 69 ++++++++++++------- 1 file changed, 43 insertions(+), 26 deletions(-) diff --git a/clients/web/src/components/elements/ResourceLinkInfo/ResourceLinkInfo.tsx b/clients/web/src/components/elements/ResourceLinkInfo/ResourceLinkInfo.tsx index 575b0061d..5bbb771ac 100644 --- a/clients/web/src/components/elements/ResourceLinkInfo/ResourceLinkInfo.tsx +++ b/clients/web/src/components/elements/ResourceLinkInfo/ResourceLinkInfo.tsx @@ -10,9 +10,10 @@ export interface ResourceLinkInfoProps { /** Optional MIME type shown as a badge. */ mimeType?: string; /** - * Optional trailing element placed at the end of the header row (beside the - * MIME badge) — e.g. an expand/collapse control supplied by an interactive - * wrapper. + * Optional trailing element placed at the end of the URI row (opposite the + * copy button) — e.g. an expand/collapse control supplied by an interactive + * wrapper. Mirrors ProtocolEntry, whose toggle sits on the row below the + * badges. */ action?: ReactNode; } @@ -21,42 +22,58 @@ const HeaderStack = Stack.withProps({ gap: 4, }); -// Name + meta (MIME badge, action). `justify` is set per-instance: spread when -// a name is present, otherwise the meta hugs the right. +// Name (left) + MIME badge (right). `justify` is set per-instance: spread when +// a name is present, otherwise the badge hugs the right. const HeaderRow = Group.withProps({ wrap: "nowrap", gap: "xs", - align: "flex-start", + align: "center", }); -// Copy control + the URI, on the line below the name. +// Copy control + URI (left) and the optional expand/collapse control (right), +// on the line below the header — mirroring ProtocolEntry's controls row. const UriRow = Group.withProps({ + justify: "space-between", wrap: "nowrap", gap: "xs", align: "center", }); +// Copy button + URI cluster; flexes so the URI fills and the action stays right. +const UriCluster = Group.withProps({ + wrap: "nowrap", + gap: "xs", + align: "center", + flex: 1, + miw: 0, +}); + +// Match how ProtocolEntry/NetworkEntry render a URL: `sm` / `fw: 500`, in the +// default sans-serif face and text color (not a blue monospace "link"). The +// `monoBreak` variant only adds `word-break: break-all` so a long URI wraps +// within the card instead of overflowing. const UriText = Text.withProps({ size: "sm", - c: "blue", - ff: "monospace", + fw: 500, variant: "monoBreak", flex: 1, miw: 0, }); -const MetaGroup = Group.withProps({ - gap: "xs", - wrap: "nowrap", -}); - const MimeBadge = Badge.withProps({ - size: "sm", - variant: "light", - color: "blue", + // Match the point size of the ProtocolEntry method/status badges; the + // lowercase MIME text reads smaller than their uppercase labels at `sm`. + size: "md", + radius: "sm", // MIME types are conventionally lowercase; keep them as-is rather than // letting Badge's default uppercase transform mangle them. tt: "none", + autoContrast: false, + // Light mode: the tinted blue-light chip (unchanged). Dark mode: a solid + // dark-blue fill with white text — matching the solid ProtocolEntry badges + // rather than a washed-out translucent tint. + bg: "light-dark(var(--mantine-color-blue-light), var(--mantine-color-blue-9))", + c: "light-dark(var(--mantine-color-blue-light-color), var(--mantine-color-white))", }); const NameText = Text.withProps({ @@ -70,8 +87,8 @@ const NameText = Text.withProps({ * Pure-display metadata for a `resource_link`: an optional name and MIME-type * badge on the header row, then the URI (monospace, link-styled) on the line * below with a copy button. The optional `action` slot lets an interactive - * wrapper (e.g. {@link ResourceLink}) place an expand/collapse control beside - * the MIME badge. + * wrapper (e.g. {@link ResourceLink}) place an expand/collapse control at the + * end of the URI row. */ export function ResourceLinkInfo({ uri, @@ -79,21 +96,21 @@ export function ResourceLinkInfo({ mimeType, action, }: ResourceLinkInfoProps) { - const hasHeader = Boolean(name || mimeType || action); + const hasHeader = Boolean(name || mimeType); return ( {hasHeader && ( {name && {name}} - - {mimeType && {mimeType}} - {action} - + {mimeType && {mimeType}} )} - - {uri} + + + {uri} + + {action} ); From cbbbb4fbb8ee446c31ce4933967330c8c26e871e Mon Sep 17 00:00:00 2001 From: cliffhall Date: Tue, 14 Jul 2026 15:27:55 -0400 Subject: [PATCH 06/10] web: scope result-card fill to resource-link results (review follow-up) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Code-review finding: ToolsScreen gave the result card `flex={1}` unconditionally, so plain text/image/error results also filled the full pane height — leaving a tall empty card and a height jump versus the input-form state. Fill is only needed so a "Resource Links" box can grow and scroll. Gate the fill on a new `resultHasResourceLinks(result)` helper (extracted to `toolResultUtils.ts` so the component module keeps exporting only components, per react-refresh/only-export-components). Non-link results keep their content-sized card; link results still fill and scroll internally. Tests: unit tests for the helper (link / text-only / empty / error) and a ToolsScreen test that a resource-link result renders the Resource Links box (covering the `flex={1}` branch). Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01RwZGbocZ64yN9bgb2mj1qE --- .../ToolResultPanel/ToolResultPanel.test.tsx | 31 +++++++++++++++++++ .../ToolResultPanel/ToolResultPanel.tsx | 3 +- .../groups/ToolResultPanel/toolResultUtils.ts | 14 +++++++++ .../screens/ToolsScreen/ToolsScreen.test.tsx | 21 +++++++++++++ .../screens/ToolsScreen/ToolsScreen.tsx | 14 ++++++--- 5 files changed, 77 insertions(+), 6 deletions(-) create mode 100644 clients/web/src/components/groups/ToolResultPanel/toolResultUtils.ts diff --git a/clients/web/src/components/groups/ToolResultPanel/ToolResultPanel.test.tsx b/clients/web/src/components/groups/ToolResultPanel/ToolResultPanel.test.tsx index 5c1e67b12..a55d07bb7 100644 --- a/clients/web/src/components/groups/ToolResultPanel/ToolResultPanel.test.tsx +++ b/clients/web/src/components/groups/ToolResultPanel/ToolResultPanel.test.tsx @@ -7,6 +7,7 @@ import { waitFor, } from "../../../test/renderWithMantine"; import { ToolResultPanel } from "./ToolResultPanel"; +import { resultHasResourceLinks } from "./toolResultUtils"; const okResult: CallToolResult = { content: [{ type: "text", text: "ok" }], @@ -113,4 +114,34 @@ describe("ToolResultPanel", () => { await user.click(screen.getByRole("button", { name: "Close results" })); expect(onClear).toHaveBeenCalledTimes(1); }); + + describe("resultHasResourceLinks", () => { + it("is true only for a non-error result containing a resource_link", () => { + expect( + resultHasResourceLinks({ + content: [ + { type: "text", text: "ok" }, + { type: "resource_link", uri: "demo://r/1", name: "Linked" }, + ], + }), + ).toBe(true); + }); + + it("is false for a text-only result", () => { + expect(resultHasResourceLinks(okResult)).toBe(false); + }); + + it("is false for an empty result", () => { + expect(resultHasResourceLinks(emptyResult)).toBe(false); + }); + + it("is false when the result is an error, even with a resource_link", () => { + expect( + resultHasResourceLinks({ + isError: true, + content: [{ type: "resource_link", uri: "demo://r/1", name: "Linked" }], + }), + ).toBe(false); + }); + }); }); diff --git a/clients/web/src/components/groups/ToolResultPanel/ToolResultPanel.tsx b/clients/web/src/components/groups/ToolResultPanel/ToolResultPanel.tsx index 2d3c88f8b..128fbf428 100644 --- a/clients/web/src/components/groups/ToolResultPanel/ToolResultPanel.tsx +++ b/clients/web/src/components/groups/ToolResultPanel/ToolResultPanel.tsx @@ -14,6 +14,7 @@ import type { } from "@modelcontextprotocol/sdk/types.js"; import { ContentViewer } from "../../elements/ContentViewer/ContentViewer"; import { ResourceLink } from "../ResourceLink/ResourceLink"; +import { resultHasResourceLinks } from "./toolResultUtils"; export interface ToolResultPanelProps { result: CallToolResult; @@ -187,7 +188,7 @@ export function ToolResultPanel({ // Results with a Resource Links box fill the card so the box grows to the // available height (and scrolls inside). Plain text/image results keep the // scroll-within-card body so a short result doesn't reserve empty height. - const hasLinks = segments.some((s) => s.kind === "links"); + const hasLinks = resultHasResourceLinks(result); const segmentNodes = segments.map((segment) => segment.kind === "links" ? ( diff --git a/clients/web/src/components/groups/ToolResultPanel/toolResultUtils.ts b/clients/web/src/components/groups/ToolResultPanel/toolResultUtils.ts new file mode 100644 index 000000000..3098c4da1 --- /dev/null +++ b/clients/web/src/components/groups/ToolResultPanel/toolResultUtils.ts @@ -0,0 +1,14 @@ +import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; + +/** + * Whether a result renders a "Resource Links" box — i.e. a non-error result + * with at least one `resource_link` block. Hosts use this to decide whether the + * result surface should fill the available height (so the box can grow and + * scroll internally); plain text/image results keep their content-sized card. + */ +export function resultHasResourceLinks(result: CallToolResult): boolean { + return ( + !result.isError && + result.content.some((block) => block.type === "resource_link") + ); +} diff --git a/clients/web/src/components/screens/ToolsScreen/ToolsScreen.test.tsx b/clients/web/src/components/screens/ToolsScreen/ToolsScreen.test.tsx index ecd8a622a..1e0af6a6c 100644 --- a/clients/web/src/components/screens/ToolsScreen/ToolsScreen.test.tsx +++ b/clients/web/src/components/screens/ToolsScreen/ToolsScreen.test.tsx @@ -176,6 +176,27 @@ describe("ToolsScreen", () => { expect(screen.getByText("Results")).toBeInTheDocument(); }); + it("fills the result card with a Resource Links box when the result has links", () => { + // A resource_link result takes the full-height (`flex={1}`) card branch so + // the Resource Links box can grow and scroll within. + renderWithMantine( + , + ); + expect( + screen.getByRole("heading", { name: "Resource Links" }), + ).toBeInTheDocument(); + }); + it("invokes onCallTool with form values on Execute", async () => { const user = userEvent.setup(); const onCallTool = vi.fn(); diff --git a/clients/web/src/components/screens/ToolsScreen/ToolsScreen.tsx b/clients/web/src/components/screens/ToolsScreen/ToolsScreen.tsx index 86584b3d0..d538f556c 100644 --- a/clients/web/src/components/screens/ToolsScreen/ToolsScreen.tsx +++ b/clients/web/src/components/screens/ToolsScreen/ToolsScreen.tsx @@ -10,6 +10,7 @@ import { type ToolProgress, } from "../../groups/ToolDetailPanel/ToolDetailPanel"; import { ToolResultPanel } from "../../groups/ToolResultPanel/ToolResultPanel"; +import { resultHasResourceLinks } from "../../groups/ToolResultPanel/toolResultUtils"; import { collectSchemaDefaults } from "../../../utils/jsonUtils"; export interface ToolCallState { @@ -181,11 +182,14 @@ export function ToolsScreen({ // `result` (App.tsx), so the executing form (progress + cancel) shows // until the result lands. - {/* `flex={1}` makes the result card fill the pane's full height (not - just size to content) so ToolResultPanel's inner scroll regions — - in particular a "Resource Links" box — expand into the available - space and scroll within, instead of leaving the card short. */} - + {/* Fill the pane's full height only when the result renders a + "Resource Links" box, so that box can expand into the available + space and scroll within. Plain text/image/error results keep the + content-sized card (matching the input-form state) instead of + reserving a tall empty card. */} + onClearResult?.()} From 48f8b5a108224ed007d77dc582e22a9add3e6368 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Tue, 14 Jul 2026 15:40:12 -0400 Subject: [PATCH 07/10] web: prettier-format the resultHasResourceLinks error-case test fixture The error-case fixture in ToolResultPanel.test.tsx wasn't re-formatted after adding the required `name` field, failing CI's `format:check`. No logic change. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01RwZGbocZ64yN9bgb2mj1qE --- .../groups/ToolResultPanel/ToolResultPanel.test.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/clients/web/src/components/groups/ToolResultPanel/ToolResultPanel.test.tsx b/clients/web/src/components/groups/ToolResultPanel/ToolResultPanel.test.tsx index a55d07bb7..2108f268d 100644 --- a/clients/web/src/components/groups/ToolResultPanel/ToolResultPanel.test.tsx +++ b/clients/web/src/components/groups/ToolResultPanel/ToolResultPanel.test.tsx @@ -139,7 +139,9 @@ describe("ToolResultPanel", () => { expect( resultHasResourceLinks({ isError: true, - content: [{ type: "resource_link", uri: "demo://r/1", name: "Linked" }], + content: [ + { type: "resource_link", uri: "demo://r/1", name: "Linked" }, + ], }), ).toBe(false); }); From 029361479b3728cd1cc4c5d2ca172488a7fbaae5 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Tue, 14 Jul 2026 16:04:04 -0400 Subject: [PATCH 08/10] web: cap a non-link block above the Resource Links box at 50% height Addresses the mixed-content case from the review: a long text block preceding the links would take its full natural height and push the Resource Links box almost entirely off-screen (only the first card peeking at the bottom). In the links-containing result, wrap each non-link block in a ScrollArea.Autosize capped at `mah: 50%` (flex "0 1 auto"), so it scrolls within at most half the card and the links box keeps the rest. A short block still takes only its content height; a result with no links renders non-link blocks uncapped in the outer scroll body as before. Adds a `ResourceLinksWithLongText` story (full-height decorator) exercising the cap. Verified visually: the text scrolls within ~50% and the links box fills the remainder. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01RwZGbocZ64yN9bgb2mj1qE --- .../ToolResultPanel.stories.tsx | 52 +++++++++++++++---- .../ToolResultPanel/ToolResultPanel.tsx | 44 ++++++++++++---- 2 files changed, 76 insertions(+), 20 deletions(-) diff --git a/clients/web/src/components/groups/ToolResultPanel/ToolResultPanel.stories.tsx b/clients/web/src/components/groups/ToolResultPanel/ToolResultPanel.stories.tsx index 98a3ae08e..49eb35b4d 100644 --- a/clients/web/src/components/groups/ToolResultPanel/ToolResultPanel.stories.tsx +++ b/clients/web/src/components/groups/ToolResultPanel/ToolResultPanel.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from "@storybook/react-vite"; +import type { Decorator, Meta, StoryObj } from "@storybook/react-vite"; import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import { Card, Flex } from "@mantine/core"; import { fn } from "storybook/test"; @@ -88,6 +88,34 @@ const resourceLinksResult: CallToolResult = { ], }; +// A long text block preceding the links, so the 50%-height cap on the text is +// exercised: the text scrolls within (at most) half the card and the Resource +// Links box keeps the rest. +const resourceLinksWithLongTextResult: CallToolResult = { + content: [ + { + type: "text", + text: "Here are 3 resource links to resources available in this server. " + .repeat(60) + .trim(), + }, + ...resourceLinksResult.content.filter((b) => b.type === "resource_link"), + ], +}; + +// Mirrors the Tools screen's full-height result card (ContentPane height → +// ContentCard `flex: 1`) so the box fills the available space and scrolls +// within, as it does in the app. +const fillHeightDecorators: Decorator[] = [ + (Story) => ( + + + + + + ), +]; + export const Empty: Story = { args: { result: emptyResult, @@ -130,15 +158,19 @@ export const ResourceLinks: Story = { contents: [{ uri, mimeType: "text/plain", text: `Contents of ${uri}` }], }), }, - decorators: [ - (Story) => ( - - - - - - ), - ], + decorators: fillHeightDecorators, +}; + +// A long text block above the links is capped at half the card height and +// scrolls within, so it can't push the Resource Links box out of view. +export const ResourceLinksWithLongText: Story = { + args: { + result: resourceLinksWithLongTextResult, + onReadResource: async (uri: string) => ({ + contents: [{ uri, mimeType: "text/plain", text: `Contents of ${uri}` }], + }), + }, + decorators: fillHeightDecorators, }; export const ErrorResult: Story = { diff --git a/clients/web/src/components/groups/ToolResultPanel/ToolResultPanel.tsx b/clients/web/src/components/groups/ToolResultPanel/ToolResultPanel.tsx index 128fbf428..75e8552a1 100644 --- a/clients/web/src/components/groups/ToolResultPanel/ToolResultPanel.tsx +++ b/clients/web/src/components/groups/ToolResultPanel/ToolResultPanel.tsx @@ -98,6 +98,20 @@ const ResultStack = Stack.withProps({ gap: "md", }); +// A non-link block that shares the card with a "Resource Links" box: capped at +// half the available height and scrollable within, so a long text block can't +// crowd the links box out of view (it keeps the remaining space). `Autosize` +// sizes to content up to the cap, so a short block still takes only what it +// needs. Without links, non-link blocks flow in the main scroll body instead. +const NonLinkCap = ScrollArea.Autosize.withProps({ + mah: "50%", + flex: "0 1 auto", + mih: 0, + type: "auto", + scrollbars: "y", + offsetScrollbars: true, +}); + // Body column for results that contain a "Resource Links" box: it fills the // card so the box (which is `flex: 1` within it) can grow to the available // height and scroll internally, rather than capping at its content height. @@ -190,21 +204,31 @@ export function ToolResultPanel({ // scroll-within-card body so a short result doesn't reserve empty height. const hasLinks = resultHasResourceLinks(result); - const segmentNodes = segments.map((segment) => - segment.kind === "links" ? ( - - ) : ( + const segmentNodes = segments.map((segment) => { + if (segment.kind === "links") { + return ( + + ); + } + const viewer = ( - ), - ); + ); + // Alongside a Resource Links box, cap the block at half the height (and let + // it scroll); on its own it flows in the outer scroll body uncapped. + return hasLinks ? ( + {viewer} + ) : ( + viewer + ); + }); return ( From af129e845a8aa16fd11e6041a942b68a78718795 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Wed, 15 Jul 2026 10:58:26 -0400 Subject: [PATCH 09/10] =?UTF-8?q?web:=20address=20review=20=E2=80=94=20exp?= =?UTF-8?q?and-toggle=20a11y,=20stale=20JSDoc,=20stronger=20fill=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review follow-ups on PR #1686: - ExpandToggle: add an optional `ariaLabel` (defaults to the tooltip verb, so ProtocolEntry/NetworkEntry/Task are unchanged) and expose `aria-expanded`. ResourceLink passes a per-resource label ("Expand/Collapse resource ") so multiple link toggles are distinguishable to assistive tech and announce their state — restoring the a11y the card-wide button previously provided, while keeping the visible tooltip a plain "Expand"/"Collapse". - ResourceLinkInfo: fix the stale JSDoc that still described the URI as "monospace, link-styled" — it now renders sans-serif sm/fw:500 default color. - ToolsScreen test: assert the result card actually gets `flex: 1` for a links result and NOT for a plain text result, so the test verifies the fill branch it names rather than only that the heading renders. Tests: ExpandToggle gains ariaLabel + aria-expanded cases; ResourceLink / ToolResultPanel / stories updated for the per-resource toggle name. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01RwZGbocZ64yN9bgb2mj1qE --- clients/cli/package-lock.json | 1 - clients/launcher/package-lock.json | 553 +++++++++++++++++- clients/tui/package-lock.json | 1 - .../ContentViewer/ContentViewer.test.tsx | 2 +- .../ExpandToggle/ExpandToggle.test.tsx | 26 + .../elements/ExpandToggle/ExpandToggle.tsx | 21 +- .../ResourceLinkInfo/ResourceLinkInfo.tsx | 9 +- .../ResourceLink/ResourceLink.stories.tsx | 10 +- .../groups/ResourceLink/ResourceLink.test.tsx | 56 +- .../groups/ResourceLink/ResourceLink.tsx | 8 +- .../ToolResultPanel/ToolResultPanel.test.tsx | 4 +- .../screens/ToolsScreen/ToolsScreen.test.tsx | 9 +- 12 files changed, 667 insertions(+), 33 deletions(-) diff --git a/clients/cli/package-lock.json b/clients/cli/package-lock.json index eedd0680a..5613e610e 100644 --- a/clients/cli/package-lock.json +++ b/clients/cli/package-lock.json @@ -6,7 +6,6 @@ "packages": { "": { "name": "@modelcontextprotocol/inspector-cli", - "version": "2.0.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.29.0", diff --git a/clients/launcher/package-lock.json b/clients/launcher/package-lock.json index d23208e5b..55f0543cf 100644 --- a/clients/launcher/package-lock.json +++ b/clients/launcher/package-lock.json @@ -6,7 +6,6 @@ "packages": { "": { "name": "@modelcontextprotocol/inspector-launcher", - "version": "2.0.0", "license": "MIT", "dependencies": { "commander": "^13.1.0" @@ -76,6 +75,40 @@ "node": ">=18" } }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.1", "dev": true, @@ -289,6 +322,25 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, "node_modules/@oxc-project/types": { "version": "0.133.0", "dev": true, @@ -297,6 +349,23 @@ "url": "https://github.com/sponsors/Boshen" } }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", + "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, "node_modules/@rolldown/binding-darwin-arm64": { "version": "1.0.3", "cpu": [ @@ -312,6 +381,247 @@ "node": "^20.19.0 || >=22.12.0" } }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", + "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", + "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", + "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", + "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", + "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", + "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", + "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", + "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", + "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", + "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", + "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", + "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", + "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.1", "dev": true, @@ -322,6 +632,17 @@ "dev": true, "license": "MIT" }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@types/chai": { "version": "5.2.3", "dev": true, @@ -1393,6 +1714,27 @@ "lightningcss-win32-x64-msvc": "1.32.0" } }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/lightningcss-darwin-arm64": { "version": "1.32.0", "cpu": [ @@ -1412,6 +1754,207 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/locate-path": { "version": "6.0.0", "dev": true, @@ -1824,6 +2367,14 @@ "typescript": ">=4.8.4" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, "node_modules/type-check": { "version": "0.4.0", "dev": true, diff --git a/clients/tui/package-lock.json b/clients/tui/package-lock.json index 91c3e9a14..03de6972f 100644 --- a/clients/tui/package-lock.json +++ b/clients/tui/package-lock.json @@ -6,7 +6,6 @@ "packages": { "": { "name": "@modelcontextprotocol/inspector-tui", - "version": "2.0.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.29.0", diff --git a/clients/web/src/components/elements/ContentViewer/ContentViewer.test.tsx b/clients/web/src/components/elements/ContentViewer/ContentViewer.test.tsx index 4cebc3e70..8c9ee3331 100644 --- a/clients/web/src/components/elements/ContentViewer/ContentViewer.test.tsx +++ b/clients/web/src/components/elements/ContentViewer/ContentViewer.test.tsx @@ -130,7 +130,7 @@ describe("ContentViewer", () => { // The URI copy button is present, but there's no expand control. expect(screen.getByRole("button", { name: "Copy" })).toBeInTheDocument(); expect( - screen.queryByRole("button", { name: "Expand" }), + screen.queryByRole("button", { name: /^Expand resource/ }), ).not.toBeInTheDocument(); }); diff --git a/clients/web/src/components/elements/ExpandToggle/ExpandToggle.test.tsx b/clients/web/src/components/elements/ExpandToggle/ExpandToggle.test.tsx index 01e6c2bc4..c52614d62 100644 --- a/clients/web/src/components/elements/ExpandToggle/ExpandToggle.test.tsx +++ b/clients/web/src/components/elements/ExpandToggle/ExpandToggle.test.tsx @@ -23,4 +23,30 @@ describe("ExpandToggle", () => { await user.click(screen.getByRole("button", { name: "Expand" })); expect(onToggle).toHaveBeenCalledTimes(1); }); + + it("uses ariaLabel as the accessible name when provided", () => { + renderWithMantine( + , + ); + expect( + screen.getByRole("button", { name: "Expand resource demo://r/1" }), + ).toBeInTheDocument(); + }); + + it("exposes aria-expanded=false when collapsed", () => { + renderWithMantine(); + expect(screen.getByRole("button")).toHaveAttribute( + "aria-expanded", + "false", + ); + }); + + it("exposes aria-expanded=true when expanded", () => { + renderWithMantine(); + expect(screen.getByRole("button")).toHaveAttribute("aria-expanded", "true"); + }); }); diff --git a/clients/web/src/components/elements/ExpandToggle/ExpandToggle.tsx b/clients/web/src/components/elements/ExpandToggle/ExpandToggle.tsx index d8965012e..0d578281d 100644 --- a/clients/web/src/components/elements/ExpandToggle/ExpandToggle.tsx +++ b/clients/web/src/components/elements/ExpandToggle/ExpandToggle.tsx @@ -5,16 +5,28 @@ export interface ExpandToggleProps { /** Whether the owning entry is currently expanded. */ expanded: boolean; onToggle: () => void; + /** + * Overrides the accessible name (aria-label). Defaults to the tooltip text + * ("Expand"/"Collapse"). Pass a per-entry name (e.g. including the resource + * URI) when several toggles sit in one list, so assistive tech can tell them + * apart; the visible tooltip stays the plain verb. + */ + ariaLabel?: string; } /** * Icon toggle for a per-entry expand/collapse control (Protocol, Network, and * Task cards). Uses the same expand/collapse-vertical icons as the list-level * ListToggle: collapsed shows the expand icon, expanded shows the collapse - * icon. The aria-label stays "Expand"/"Collapse" so it reads the same as the - * text button it replaced. + * icon. The tooltip stays "Expand"/"Collapse" (the same verb as the text button + * it replaced); `aria-expanded` exposes the disclosure state and `ariaLabel` + * can distinguish sibling toggles. */ -export function ExpandToggle({ expanded, onToggle }: ExpandToggleProps) { +export function ExpandToggle({ + expanded, + onToggle, + ariaLabel, +}: ExpandToggleProps) { const Icon = expanded ? RiCollapseVerticalLine : RiExpandVerticalLine; const label = expanded ? "Collapse" : "Expand"; return ( @@ -23,7 +35,8 @@ export function ExpandToggle({ expanded, onToggle }: ExpandToggleProps) { variant="subtle" color="gray" size="md" - aria-label={label} + aria-label={ariaLabel ?? label} + aria-expanded={expanded} onClick={onToggle} > diff --git a/clients/web/src/components/elements/ResourceLinkInfo/ResourceLinkInfo.tsx b/clients/web/src/components/elements/ResourceLinkInfo/ResourceLinkInfo.tsx index 5bbb771ac..a9cc1e4b6 100644 --- a/clients/web/src/components/elements/ResourceLinkInfo/ResourceLinkInfo.tsx +++ b/clients/web/src/components/elements/ResourceLinkInfo/ResourceLinkInfo.tsx @@ -85,10 +85,11 @@ const NameText = Text.withProps({ /** * Pure-display metadata for a `resource_link`: an optional name and MIME-type - * badge on the header row, then the URI (monospace, link-styled) on the line - * below with a copy button. The optional `action` slot lets an interactive - * wrapper (e.g. {@link ResourceLink}) place an expand/collapse control at the - * end of the URI row. + * badge on the header row, then the URI on the line below with a copy button. + * The URI is styled like ProtocolEntry/NetworkEntry URLs (`sm` / `fw: 500`, + * default sans-serif face and color), not a blue monospace link. The optional + * `action` slot lets an interactive wrapper (e.g. {@link ResourceLink}) place + * an expand/collapse control at the end of the URI row. */ export function ResourceLinkInfo({ uri, diff --git a/clients/web/src/components/groups/ResourceLink/ResourceLink.stories.tsx b/clients/web/src/components/groups/ResourceLink/ResourceLink.stories.tsx index 9db1cee74..a1dbac366 100644 --- a/clients/web/src/components/groups/ResourceLink/ResourceLink.stories.tsx +++ b/clients/web/src/components/groups/ResourceLink/ResourceLink.stories.tsx @@ -49,7 +49,7 @@ export const Static: Story = { // Copy button is present; there's no expand control in the static card. expect(canvas.getByRole("button", { name: "Copy" })).toBeInTheDocument(); expect( - canvas.queryByRole("button", { name: "Expand" }), + canvas.queryByRole("button", { name: /^Expand resource/ }), ).not.toBeInTheDocument(); }, }; @@ -63,7 +63,9 @@ export const Expandable: Story = { }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); - await userEvent.click(canvas.getByRole("button", { name: "Expand" })); + await userEvent.click( + canvas.getByRole("button", { name: `Expand resource ${URI}` }), + ); await waitFor(() => expect(canvas.getByText(/Read on demand/)).toBeInTheDocument(), ); @@ -81,7 +83,9 @@ export const LargeResult: Story = { }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); - await userEvent.click(canvas.getByRole("button", { name: "Expand" })); + await userEvent.click( + canvas.getByRole("button", { name: `Expand resource ${BLOB_URI}` }), + ); // The `"blob"` key appears only in the expanded read result's JSON (not in // the metadata badge), so it confirms the inline result rendered. await waitFor(() => diff --git a/clients/web/src/components/groups/ResourceLink/ResourceLink.test.tsx b/clients/web/src/components/groups/ResourceLink/ResourceLink.test.tsx index 4eb484288..50a4f13b8 100644 --- a/clients/web/src/components/groups/ResourceLink/ResourceLink.test.tsx +++ b/clients/web/src/components/groups/ResourceLink/ResourceLink.test.tsx @@ -29,7 +29,7 @@ describe("ResourceLink", () => { // The URI copy button is always present, but there's no expand affordance. expect(screen.getByRole("button", { name: "Copy" })).toBeInTheDocument(); expect( - screen.queryByRole("button", { name: "Expand" }), + screen.queryByRole("button", { name: `Expand resource ${URI}` }), ).not.toBeInTheDocument(); }); @@ -40,7 +40,9 @@ describe("ResourceLink", () => { , ); - await user.click(screen.getByRole("button", { name: "Expand" })); + await user.click( + screen.getByRole("button", { name: `Expand resource ${URI}` }), + ); expect(onReadResource).toHaveBeenCalledWith(URI); // The full read result is rendered inline as formatted JSON. @@ -49,7 +51,7 @@ describe("ResourceLink", () => { ); // The toggle flips to the collapse control once expanded. expect( - screen.getByRole("button", { name: "Collapse" }), + screen.getByRole("button", { name: `Collapse resource ${URI}` }), ).toBeInTheDocument(); }); @@ -60,18 +62,26 @@ describe("ResourceLink", () => { , ); - await user.click(screen.getByRole("button", { name: "Expand" })); + await user.click( + screen.getByRole("button", { name: `Expand resource ${URI}` }), + ); await waitFor(() => expect(screen.getByText(/"cached body"/)).toBeInTheDocument(), ); // Collapse — the toggle flips back to the expand control. (The read result // stays mounted inside the animated Collapse, so it isn't re-read.) - await user.click(screen.getByRole("button", { name: "Collapse" })); - expect(screen.getByRole("button", { name: "Expand" })).toBeInTheDocument(); + await user.click( + screen.getByRole("button", { name: `Collapse resource ${URI}` }), + ); + expect( + screen.getByRole("button", { name: `Expand resource ${URI}` }), + ).toBeInTheDocument(); // Re-expand — content is still present without a second read. - await user.click(screen.getByRole("button", { name: "Expand" })); + await user.click( + screen.getByRole("button", { name: `Expand resource ${URI}` }), + ); expect(screen.getByText(/"cached body"/)).toBeInTheDocument(); expect(onReadResource).toHaveBeenCalledTimes(1); }); @@ -83,7 +93,9 @@ describe("ResourceLink", () => { , ); - await user.click(screen.getByRole("button", { name: "Expand" })); + await user.click( + screen.getByRole("button", { name: `Expand resource ${URI}` }), + ); await waitFor(() => expect(screen.getByText("Failed to read resource")).toBeInTheDocument(), ); @@ -99,7 +111,9 @@ describe("ResourceLink", () => { , ); - await user.click(screen.getByRole("button", { name: "Expand" })); + await user.click( + screen.getByRole("button", { name: `Expand resource ${URI}` }), + ); await waitFor(() => expect(screen.getByText("Failed to read resource")).toBeInTheDocument(), ); @@ -117,14 +131,20 @@ describe("ResourceLink", () => { ); // First expand fails. - await user.click(screen.getByRole("button", { name: "Expand" })); + await user.click( + screen.getByRole("button", { name: `Expand resource ${URI}` }), + ); await waitFor(() => expect(screen.getByText("Failed to read resource")).toBeInTheDocument(), ); // Collapse, then re-expand — the read is retried (error is not cached). - await user.click(screen.getByRole("button", { name: "Collapse" })); - await user.click(screen.getByRole("button", { name: "Expand" })); + await user.click( + screen.getByRole("button", { name: `Collapse resource ${URI}` }), + ); + await user.click( + screen.getByRole("button", { name: `Expand resource ${URI}` }), + ); await waitFor(() => expect(screen.getByText(/"recovered body"/)).toBeInTheDocument(), @@ -149,9 +169,15 @@ describe("ResourceLink", () => { ); // Expand — read is in flight (loading), then collapse and re-expand. - await user.click(screen.getByRole("button", { name: "Expand" })); - await user.click(screen.getByRole("button", { name: "Collapse" })); - await user.click(screen.getByRole("button", { name: "Expand" })); + await user.click( + screen.getByRole("button", { name: `Expand resource ${URI}` }), + ); + await user.click( + screen.getByRole("button", { name: `Collapse resource ${URI}` }), + ); + await user.click( + screen.getByRole("button", { name: `Expand resource ${URI}` }), + ); // Still only the original in-flight read — no redundant fetch. expect(onReadResource).toHaveBeenCalledTimes(1); diff --git a/clients/web/src/components/groups/ResourceLink/ResourceLink.tsx b/clients/web/src/components/groups/ResourceLink/ResourceLink.tsx index cff3a7ea0..1430c46e9 100644 --- a/clients/web/src/components/groups/ResourceLink/ResourceLink.tsx +++ b/clients/web/src/components/groups/ResourceLink/ResourceLink.tsx @@ -98,8 +98,14 @@ export function ResourceLink({ // Same tooltip'd expand/collapse control as ProtocolEntry (ExpandToggle), // placed in the header row's meta slot as a sibling of the URI's copy button. + // A per-resource `ariaLabel` keeps the toggles distinguishable to assistive + // tech when several links are listed (the visible tooltip stays "Expand"). const action = expandable ? ( - void toggle()} /> + void toggle()} + ariaLabel={`${expanded ? "Collapse" : "Expand"} resource ${uri}`} + /> ) : undefined; return ( diff --git a/clients/web/src/components/groups/ToolResultPanel/ToolResultPanel.test.tsx b/clients/web/src/components/groups/ToolResultPanel/ToolResultPanel.test.tsx index 2108f268d..9f608af19 100644 --- a/clients/web/src/components/groups/ToolResultPanel/ToolResultPanel.test.tsx +++ b/clients/web/src/components/groups/ToolResultPanel/ToolResultPanel.test.tsx @@ -65,7 +65,9 @@ describe("ToolResultPanel", () => { expect( screen.getByRole("heading", { name: "Resource Links" }), ).toBeInTheDocument(); - await user.click(screen.getByRole("button", { name: "Expand" })); + await user.click( + screen.getByRole("button", { name: "Expand resource demo://r/1" }), + ); expect(onReadResource).toHaveBeenCalledWith("demo://r/1"); await waitFor(() => expect(screen.getByText(/"linked body"/)).toBeInTheDocument(), diff --git a/clients/web/src/components/screens/ToolsScreen/ToolsScreen.test.tsx b/clients/web/src/components/screens/ToolsScreen/ToolsScreen.test.tsx index 1e0af6a6c..15692b6aa 100644 --- a/clients/web/src/components/screens/ToolsScreen/ToolsScreen.test.tsx +++ b/clients/web/src/components/screens/ToolsScreen/ToolsScreen.test.tsx @@ -163,7 +163,7 @@ describe("ToolsScreen", () => { expect(screen.queryByText("Results")).toBeNull(); }); - it("renders the result panel when callState has a result", () => { + it("renders a content-sized result card for a plain text result", () => { renderWithMantine( { />, ); expect(screen.getByText("Results")).toBeInTheDocument(); + // No links → the result card is NOT flex-filled (sizes to content). + const card = screen.getByText("Results").closest(".mantine-Card-root"); + expect(card).not.toHaveStyle({ flex: "1" }); }); it("fills the result card with a Resource Links box when the result has links", () => { @@ -195,6 +198,10 @@ describe("ToolsScreen", () => { expect( screen.getByRole("heading", { name: "Resource Links" }), ).toBeInTheDocument(); + // Links → the result card fills the pane (`flex: 1`), distinguishing this + // branch from the content-sized text-result card above. + const card = screen.getByText("Results").closest(".mantine-Card-root"); + expect(card).toHaveStyle({ flex: "1" }); }); it("invokes onCallTool with form values on Execute", async () => { From 9c4d67962a2f2ce5ac06d34a546009bcdecdaccd Mon Sep 17 00:00:00 2001 From: cliffhall Date: Wed, 15 Jul 2026 10:59:29 -0400 Subject: [PATCH 10/10] web: restore cli/tui/launcher package-locks accidentally committed These lock files were touched by a local install in the worktree (dropping the `version` field and adding optional @emnapi platform deps); they are unrelated to this PR. Restore them to their v2/main state. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01RwZGbocZ64yN9bgb2mj1qE --- clients/cli/package-lock.json | 1 + clients/launcher/package-lock.json | 553 +---------------------------- clients/tui/package-lock.json | 1 + 3 files changed, 3 insertions(+), 552 deletions(-) diff --git a/clients/cli/package-lock.json b/clients/cli/package-lock.json index 5613e610e..eedd0680a 100644 --- a/clients/cli/package-lock.json +++ b/clients/cli/package-lock.json @@ -6,6 +6,7 @@ "packages": { "": { "name": "@modelcontextprotocol/inspector-cli", + "version": "2.0.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.29.0", diff --git a/clients/launcher/package-lock.json b/clients/launcher/package-lock.json index 55f0543cf..d23208e5b 100644 --- a/clients/launcher/package-lock.json +++ b/clients/launcher/package-lock.json @@ -6,6 +6,7 @@ "packages": { "": { "name": "@modelcontextprotocol/inspector-launcher", + "version": "2.0.0", "license": "MIT", "dependencies": { "commander": "^13.1.0" @@ -75,40 +76,6 @@ "node": ">=18" } }, - "node_modules/@emnapi/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.1", "dev": true, @@ -322,25 +289,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", - "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@tybys/wasm-util": "^0.10.3" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "peerDependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1" - } - }, "node_modules/@oxc-project/types": { "version": "0.133.0", "dev": true, @@ -349,23 +297,6 @@ "url": "https://github.com/sponsors/Boshen" } }, - "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", - "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, "node_modules/@rolldown/binding-darwin-arm64": { "version": "1.0.3", "cpu": [ @@ -381,247 +312,6 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", - "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", - "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", - "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", - "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", - "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", - "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", - "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", - "cpu": [ - "s390x" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", - "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", - "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", - "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", - "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "1.10.0", - "@emnapi/runtime": "1.10.0", - "@napi-rs/wasm-runtime": "^1.1.4" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", - "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", - "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, "node_modules/@rolldown/pluginutils": { "version": "1.0.1", "dev": true, @@ -632,17 +322,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", - "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@types/chai": { "version": "5.2.3", "dev": true, @@ -1714,27 +1393,6 @@ "lightningcss-win32-x64-msvc": "1.32.0" } }, - "node_modules/lightningcss-android-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", - "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, "node_modules/lightningcss-darwin-arm64": { "version": "1.32.0", "cpu": [ @@ -1754,207 +1412,6 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", - "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", - "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", - "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", - "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", - "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", - "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", - "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", - "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", - "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, "node_modules/locate-path": { "version": "6.0.0", "dev": true, @@ -2367,14 +1824,6 @@ "typescript": ">=4.8.4" } }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD", - "optional": true - }, "node_modules/type-check": { "version": "0.4.0", "dev": true, diff --git a/clients/tui/package-lock.json b/clients/tui/package-lock.json index 03de6972f..91c3e9a14 100644 --- a/clients/tui/package-lock.json +++ b/clients/tui/package-lock.json @@ -6,6 +6,7 @@ "packages": { "": { "name": "@modelcontextprotocol/inspector-tui", + "version": "2.0.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.29.0",