From 0dc7a5021800bc88ffe3d6eb060d63276fa9f5fe Mon Sep 17 00:00:00 2001 From: Nandha Reddy Date: Wed, 9 Jul 2025 05:15:46 +1000 Subject: [PATCH] feat: Enhance ResourceLinkView with expandable resource content view Add click-to-expand functionality for inline resource content display --- client/src/App.tsx | 15 +- client/src/components/ResourceLinkView.tsx | 188 +++++++++++---------- client/src/components/ToolResults.tsx | 11 +- client/src/components/ToolsTab.tsx | 6 + 4 files changed, 128 insertions(+), 92 deletions(-) diff --git a/client/src/App.tsx b/client/src/App.tsx index 3ceafcae..cff51b4f 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -80,6 +80,9 @@ const App = () => { ResourceTemplate[] >([]); const [resourceContent, setResourceContent] = useState(""); + const [resourceContentMap, setResourceContentMap] = useState< + Record + >({}); const [prompts, setPrompts] = useState([]); const [promptContent, setPromptContent] = useState(""); const [tools, setTools] = useState([]); @@ -461,7 +464,12 @@ const App = () => { ReadResourceResultSchema, "resources", ); - setResourceContent(JSON.stringify(response, null, 2)); + const content = JSON.stringify(response, null, 2); + setResourceContent(content); + setResourceContentMap((prev) => ({ + ...prev, + [uri]: content, + })); }; const subscribeToResource = async (uri: string) => { @@ -863,6 +871,11 @@ const App = () => { toolResult={toolResult} nextCursor={nextToolCursor} error={errors.tools} + resourceContent={resourceContentMap} + onReadResource={(uri: string) => { + clearError("resources"); + readResource(uri); + }} /> void; } -const ResourceLinkView = ({ - uri, - name, - description, - mimeType, -}: ResourceLinkViewProps) => { - const { toast } = useToast(); - const [copied, setCopied] = useState(false); +const ResourceLinkView = memo( + ({ + uri, + name, + description, + mimeType, + resourceContent, + onReadResource, + }: ResourceLinkViewProps) => { + const [{ expanded, loading }, setState] = useState({ + expanded: false, + loading: 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} + const expandedContent = useMemo( + () => + expanded && resourceContent ? ( +
+
+ Resource: +
+
- )} + ) : null, + [expanded, resourceContent], + ); - {description && ( -

- {description} -

- )} + const handleClick = useCallback(() => { + if (!onReadResource) return; + if (!expanded) { + setState((prev) => ({ ...prev, expanded: true, loading: true })); + onReadResource(uri); + setState((prev) => ({ ...prev, loading: false })); + } else { + setState((prev) => ({ ...prev, expanded: false })); + } + }, [expanded, onReadResource, uri]); + + const handleKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if ((e.key === "Enter" || e.key === " ") && onReadResource) { + e.preventDefault(); + handleClick(); + } + }, + [handleClick, onReadResource], + ); + + return ( +
+
+
+
+ + {uri} + +
+ {mimeType && ( + + {mimeType} + + )} + {onReadResource && ( +
+ {name && ( +
+ {name} +
+ )} + {description && ( +

+ {description} +

+ )} +
+
+ {expandedContent}
-
- ); -}; + ); + }, +); export default ResourceLinkView; diff --git a/client/src/components/ToolResults.tsx b/client/src/components/ToolResults.tsx index cf6bb90d..f2450066 100644 --- a/client/src/components/ToolResults.tsx +++ b/client/src/components/ToolResults.tsx @@ -10,6 +10,8 @@ import { validateToolOutput, hasOutputSchema } from "@/utils/schemaUtils"; interface ToolResultsProps { toolResult: CompatibilityCallToolResult | null; selectedTool: Tool | null; + resourceContent: Record; + onReadResource?: (uri: string) => void; } const checkContentCompatibility = ( @@ -62,7 +64,12 @@ const checkContentCompatibility = ( } }; -const ToolResults = ({ toolResult, selectedTool }: ToolResultsProps) => { +const ToolResults = ({ + toolResult, + selectedTool, + resourceContent, + onReadResource, +}: ToolResultsProps) => { if (!toolResult) return null; if ("content" in toolResult) { @@ -207,6 +214,8 @@ const ToolResults = ({ toolResult, selectedTool }: ToolResultsProps) => { name={item.name} description={item.description} mimeType={item.mimeType} + resourceContent={resourceContent[item.uri] || ""} + onReadResource={onReadResource} /> )}
diff --git a/client/src/components/ToolsTab.tsx b/client/src/components/ToolsTab.tsx index 9e4bc69a..3c7db84e 100644 --- a/client/src/components/ToolsTab.tsx +++ b/client/src/components/ToolsTab.tsx @@ -28,6 +28,8 @@ const ToolsTab = ({ setSelectedTool, toolResult, nextCursor, + resourceContent, + onReadResource, }: { tools: Tool[]; listTools: () => void; @@ -38,6 +40,8 @@ const ToolsTab = ({ toolResult: CompatibilityCallToolResult | null; nextCursor: ListToolsResult["nextCursor"]; error: string | null; + resourceContent: Record; + onReadResource?: (uri: string) => void; }) => { const [params, setParams] = useState>({}); const [isToolRunning, setIsToolRunning] = useState(false); @@ -267,6 +271,8 @@ const ToolsTab = ({
) : (