Skip to content

Commit c4587ba

Browse files
committed
feat: Add resource_link content type support to ToolResults component
Add ResourceLinkView component for displaying resource links
1 parent 5e92e88 commit c4587ba

File tree

3 files changed

+138
-0
lines changed

3 files changed

+138
-0
lines changed
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { useState, useCallback, useEffect } from "react";
2+
import { Copy, CheckCheck } from "lucide-react";
3+
import { Button } from "@/components/ui/button";
4+
import { useToast } from "@/lib/hooks/useToast";
5+
6+
interface ResourceLinkViewProps {
7+
uri: string;
8+
name?: string;
9+
description?: string;
10+
mimeType?: string;
11+
}
12+
13+
const ResourceLinkView = ({
14+
uri,
15+
name,
16+
description,
17+
mimeType,
18+
}: ResourceLinkViewProps) => {
19+
const { toast } = useToast();
20+
const [copied, setCopied] = useState(false);
21+
22+
useEffect(() => {
23+
let timeoutId: NodeJS.Timeout;
24+
if (copied) {
25+
timeoutId = setTimeout(() => {
26+
setCopied(false);
27+
}, 500);
28+
}
29+
return () => {
30+
if (timeoutId) {
31+
clearTimeout(timeoutId);
32+
}
33+
};
34+
}, [copied]);
35+
36+
const handleCopyUri = useCallback(() => {
37+
try {
38+
navigator.clipboard.writeText(uri);
39+
setCopied(true);
40+
} catch (error) {
41+
toast({
42+
title: "Error",
43+
description: `There was an error copying URI to clipboard: ${error instanceof Error ? error.message : String(error)}`,
44+
variant: "destructive",
45+
});
46+
}
47+
}, [uri, toast]);
48+
49+
const displayName = name || new URL(uri).pathname.split("/").pop() || uri;
50+
51+
return (
52+
<div
53+
className="p-4 border rounded relative bg-gray-50 dark:bg-gray-800"
54+
role="article"
55+
aria-label={`Resource link: ${displayName}`}
56+
>
57+
<Button
58+
size="icon"
59+
variant="ghost"
60+
className="absolute top-2 right-2"
61+
onClick={handleCopyUri}
62+
>
63+
{copied ? (
64+
<CheckCheck className="size-4 dark:text-green-700 text-green-600" />
65+
) : (
66+
<Copy className="size-4 text-foreground" />
67+
)}
68+
</Button>
69+
70+
<div className="pr-10">
71+
<div className="flex items-start justify-between gap-2 mb-2">
72+
<a
73+
href={uri}
74+
target="_blank"
75+
rel="noopener noreferrer"
76+
className="text-sm text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 hover:underline focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-1 rounded px-1 py-0.5 break-all font-mono flex-1 min-w-0"
77+
aria-label={`Open resource: ${uri}`}
78+
>
79+
{uri}
80+
</a>
81+
{mimeType && (
82+
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200 flex-shrink-0">
83+
{mimeType}
84+
</span>
85+
)}
86+
</div>
87+
88+
{name && (
89+
<div className="font-semibold text-sm text-gray-900 dark:text-gray-100 mb-1">
90+
{name}
91+
</div>
92+
)}
93+
94+
{description && (
95+
<p className="text-sm text-gray-600 dark:text-gray-300 leading-relaxed">
96+
{description}
97+
</p>
98+
)}
99+
</div>
100+
</div>
101+
);
102+
};
103+
104+
export default ResourceLinkView;

client/src/components/ToolResults.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import JsonView from "./JsonView";
2+
import ResourceLinkView from "./ResourceLinkView";
23
import {
34
CallToolResultSchema,
45
CompatibilityCallToolResult,
@@ -200,6 +201,14 @@ const ToolResults = ({ toolResult, selectedTool }: ToolResultsProps) => {
200201
) : (
201202
<JsonView data={item.resource} />
202203
))}
204+
{item.type === "resource_link" && (
205+
<ResourceLinkView
206+
uri={item.uri}
207+
name={item.name}
208+
description={item.description}
209+
mimeType={item.mimeType}
210+
/>
211+
)}
203212
</div>
204213
))}
205214
</div>

client/src/components/__tests__/ToolsTab.test.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,4 +360,29 @@ describe("ToolsTab", () => {
360360
).not.toBeInTheDocument();
361361
});
362362
});
363+
364+
describe("Resource Link Content Type", () => {
365+
it("should render resource_link content type", () => {
366+
const result = {
367+
content: [
368+
{
369+
type: "resource_link",
370+
uri: "https://example.com/resource",
371+
name: "Test Resource",
372+
description: "A test resource",
373+
mimeType: "application/json",
374+
},
375+
],
376+
};
377+
378+
renderToolsTab({ selectedTool: mockTools[0], toolResult: result });
379+
380+
expect(
381+
screen.getByText("https://example.com/resource"),
382+
).toBeInTheDocument();
383+
expect(screen.getByText("Test Resource")).toBeInTheDocument();
384+
expect(screen.getByText("A test resource")).toBeInTheDocument();
385+
expect(screen.getByText("application/json")).toBeInTheDocument();
386+
});
387+
});
363388
});

0 commit comments

Comments
 (0)