* feat(web): expandable resource_link with read-on-demand (#1449) Replace the placeholder `resource_link` rendering in ContentViewer with a dedicated, Mantine-native `ResourceLink` element that mirrors V1's behavior: - Shows uri (monospace, link-styled), optional name/description, and a mimeType badge. - Expand/collapse affordance (chevron + loading spinner) that is keyboard accessible (native button: role, aria-expanded, aria-label, Enter/Space). - On first expand, reads the linked resource on demand via an injected callback and renders the full read result inline as formatted JSON under a green "Resource:" label, capped to a max height with scroll for large results. The fetched result is cached across collapse/expand. The read handler is threaded as a "dumb component + callback as prop" through ToolResultPanel → ToolsScreen → InspectorView, with App supplying a handler that returns the `ReadResourceResult` from `inspectorClient.readResource`. Also extract the shared `resourceContentsToBlock` helper into `utils/resourceContents.ts` (previously private to ResourcePreviewPanel). Tests: unit coverage for ResourceLink (collapsed/expanded/error/cache) and the ContentViewer wiring, plus Storybook stories with play functions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(web): scroll large resource_link read results within the card A plain ScrollArea with only `mah` does not scroll when its parent (the content-sized card) has no definite height, so a large read result overflowed the card instead of scrolling. Switch to ScrollArea.Autosize, which grows with content up to the max height and then scrolls. Adds a LargeResult story covering the bounded-scroll behavior. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(web): make ResourceLink a group; extract ResourceLinkInfo element ResourceLink is stateful (expand/loading/cached result) and fetches data via a callback — group-like behavior, and the only `elements/` component using useState. Move it to `groups/` and split out the pure-display metadata as a new `elements/ResourceLinkInfo` element (uri/name/description/mime + an optional action slot). This keeps the layer dependencies clean (no element→group import, which moving ResourceLink alone would have introduced): - ContentViewer (element) renders ResourceLinkInfo (element) statically for `resource_link` blocks and no longer takes an onReadResource prop. - ToolResultPanel (group) renders the interactive ResourceLink (group) for `resource_link` blocks, ContentViewer for the rest. - ResourceLink (group) composes ResourceLinkInfo + ContentViewer. Also reverts the now-single-consumer resourceContents.ts extraction, restoring ResourcePreviewPanel's local toContentBlock. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(web): make ResourceLink read retryable and guard double-fetch Address PR review: a failed read is no longer cached as terminal — only a successful result is cached, so collapsing and re-expanding retries the read after a transient failure. Add a loading guard so toggling while a read is in flight does not fire a redundant second read. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(web): prettier formatting for ResourceLink test Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: remove accidentally-committed build artifacts; gitignore clients/*/build A stray 'git add -A' swept 8 compiled build files into d25c9382. Remove them from tracking and add clients/*/build to .gitignore so client build output is never committed again. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a10ced988c
commit
8d4363d0ea
1 changed files with 728 additions and 17 deletions
|
|
@ -7,12 +7,21 @@ import {
|
|||
Text,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
||||
import type {
|
||||
CallToolResult,
|
||||
ReadResourceResult,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
import { ContentViewer } from "../../elements/ContentViewer/ContentViewer";
|
||||
import { ResourceLink } from "../ResourceLink/ResourceLink";
|
||||
|
||||
export interface ToolResultPanelProps {
|
||||
result: CallToolResult;
|
||||
onClear: () => void;
|
||||
/**
|
||||
* Read-on-demand handler so `resource_link` blocks in the result can fetch
|
||||
* and inline their contents.
|
||||
*/
|
||||
onReadResource?: (uri: string) => Promise<ReadResourceResult>;
|
||||
}
|
||||
|
||||
const ClearButton = Button.withProps({
|
||||
|
|
@ -49,7 +58,11 @@ const ResultStack = Stack.withProps({
|
|||
gap: "md",
|
||||
});
|
||||
|
||||
export function ToolResultPanel({ result, onClear }: ToolResultPanelProps) {
|
||||
export function ToolResultPanel({
|
||||
result,
|
||||
onClear,
|
||||
onReadResource,
|
||||
}: ToolResultPanelProps) {
|
||||
return (
|
||||
<PanelStack>
|
||||
<HeaderRow>
|
||||
|
|
@ -68,13 +81,24 @@ export function ToolResultPanel({ result, onClear }: ToolResultPanelProps) {
|
|||
) : result.content.length === 0 ? (
|
||||
<Text c="dimmed">No results yet</Text>
|
||||
) : (
|
||||
result.content.map((block, index) => (
|
||||
<ContentViewer
|
||||
key={index}
|
||||
block={block}
|
||||
copyable={block.type === "text"}
|
||||
/>
|
||||
))
|
||||
result.content.map((block, index) =>
|
||||
block.type === "resource_link" ? (
|
||||
<ResourceLink
|
||||
key={index}
|
||||
uri={block.uri}
|
||||
name={block.name}
|
||||
description={block.description}
|
||||
mimeType={block.mimeType}
|
||||
onReadResource={onReadResource}
|
||||
/>
|
||||
) : (
|
||||
<ContentViewer
|
||||
key={index}
|
||||
block={block}
|
||||
copyable={block.type === "text"}
|
||||
/>
|
||||
),
|
||||
)
|
||||
)}
|
||||
</ResultStack>
|
||||
</ResultScroll>
|
||||
|
|
|
|||
Loading…
Reference in a new issue