From c4587bafb82b44443be245639369e39f96baae8b Mon Sep 17 00:00:00 2001 From: Nandha Reddy Date: Thu, 26 Jun 2025 20:22:28 +1000 Subject: [PATCH] feat: Add resource_link content type support to ToolResults component Add ResourceLinkView component for displaying resource links --- client/src/components/ResourceLinkView.tsx | 104 ++++++++++++++++++ client/src/components/ToolResults.tsx | 9 ++ .../components/__tests__/ToolsTab.test.tsx | 25 +++++ 3 files changed, 138 insertions(+) create mode 100644 client/src/components/ResourceLinkView.tsx diff --git a/client/src/components/ResourceLinkView.tsx b/client/src/components/ResourceLinkView.tsx new file mode 100644 index 00000000..94fce429 --- /dev/null +++ b/client/src/components/ResourceLinkView.tsx @@ -0,0 +1,104 @@ +import { useState, useCallback, useEffect } from "react"; +import { Copy, CheckCheck } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { useToast } from "@/lib/hooks/useToast"; + +interface ResourceLinkViewProps { + uri: string; + name?: string; + description?: string; + mimeType?: string; +} + +const ResourceLinkView = ({ + uri, + name, + description, + mimeType, +}: ResourceLinkViewProps) => { + const { toast } = useToast(); + const [copied, setCopied] = useState(false); + + useEffect(() => { + let timeoutId: NodeJS.Timeout; + if (copied) { + timeoutId = setTimeout(() => { + setCopied(false); + }, 500); + } + return () => { + if (timeoutId) { + clearTimeout(timeoutId); + } + }; + }, [copied]); + + const handleCopyUri = useCallback(() => { + try { + navigator.clipboard.writeText(uri); + setCopied(true); + } catch (error) { + toast({ + title: "Error", + description: `There was an error copying URI to clipboard: ${error instanceof Error ? error.message : String(error)}`, + variant: "destructive", + }); + } + }, [uri, toast]); + + const displayName = name || new URL(uri).pathname.split("/").pop() || uri; + + return ( +
+ + +
+
+ + {uri} + + {mimeType && ( + + {mimeType} + + )} +
+ + {name && ( +
+ {name} +
+ )} + + {description && ( +

+ {description} +

+ )} +
+
+ ); +}; + +export default ResourceLinkView; diff --git a/client/src/components/ToolResults.tsx b/client/src/components/ToolResults.tsx index c6d90700..cf6bb90d 100644 --- a/client/src/components/ToolResults.tsx +++ b/client/src/components/ToolResults.tsx @@ -1,4 +1,5 @@ import JsonView from "./JsonView"; +import ResourceLinkView from "./ResourceLinkView"; import { CallToolResultSchema, CompatibilityCallToolResult, @@ -200,6 +201,14 @@ const ToolResults = ({ toolResult, selectedTool }: ToolResultsProps) => { ) : ( ))} + {item.type === "resource_link" && ( + + )} ))} diff --git a/client/src/components/__tests__/ToolsTab.test.tsx b/client/src/components/__tests__/ToolsTab.test.tsx index c9f9b315..f954b6bd 100644 --- a/client/src/components/__tests__/ToolsTab.test.tsx +++ b/client/src/components/__tests__/ToolsTab.test.tsx @@ -360,4 +360,29 @@ describe("ToolsTab", () => { ).not.toBeInTheDocument(); }); }); + + describe("Resource Link Content Type", () => { + it("should render resource_link content type", () => { + const result = { + content: [ + { + type: "resource_link", + uri: "https://example.com/resource", + name: "Test Resource", + description: "A test resource", + mimeType: "application/json", + }, + ], + }; + + renderToolsTab({ selectedTool: mockTools[0], toolResult: result }); + + expect( + screen.getByText("https://example.com/resource"), + ).toBeInTheDocument(); + expect(screen.getByText("Test Resource")).toBeInTheDocument(); + expect(screen.getByText("A test resource")).toBeInTheDocument(); + expect(screen.getByText("application/json")).toBeInTheDocument(); + }); + }); });