Skip to content

Commit 202e3a6

Browse files
Garothhugelung
authored andcommitted
Hugelung/rich mcp response (RooCodeInc#1941)
* showing images after mcp responses * images now open in a webview tab * Open Graph link metadata display for MCP responses * almost totally working rich mcp response display with images and embeds * closer * header for response display * updated styling of mcp responses * default to plain text if rich response is loading * formatting fix * added changeset output * remove some old code * add the dashed border back * avoid XSS attacks by sanitizing the preview image urls and embeds * remove incorrect vendor prefix css * delete old version of open image implementation * undo some comment removals and cleanups to make PR easier to read --------- Co-authored-by: Andrei Edell <[email protected]>
1 parent a229b8c commit 202e3a6

File tree

12 files changed

+985
-170
lines changed

12 files changed

+985
-170
lines changed

.changeset/old-dancers-smell.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"claude-dev": minor
3+
---
4+
5+
Add rich MCP responses with images and link previews

package-lock.json

Lines changed: 22 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,7 @@
264264
"isbinaryfile": "^5.0.2",
265265
"mammoth": "^1.8.0",
266266
"monaco-vscode-textmate-theme-converter": "^0.1.7",
267+
"open-graph-scraper": "^6.9.0",
267268
"openai": "^4.83.0",
268269
"os-name": "^6.0.0",
269270
"p-wait-for": "^5.0.2",

src/core/webview/ClineProvider.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import * as vscode from "vscode"
1010
import { buildApiHandler } from "../../api"
1111
import { downloadTask } from "../../integrations/misc/export-markdown"
1212
import { openFile, openImage } from "../../integrations/misc/open-file"
13+
import { fetchOpenGraphData, isImageUrl } from "../../integrations/misc/link-preview"
1314
import { selectImages } from "../../integrations/misc/process-images"
1415
import { getTheme } from "../../integrations/theme/getTheme"
1516
import WorkspaceTracker from "../../integrations/workspace/WorkspaceTracker"
@@ -663,6 +664,17 @@ export class ClineProvider implements vscode.WebviewViewProvider {
663664
case "openImage":
664665
openImage(message.text!)
665666
break
667+
case "openInBrowser":
668+
if (message.url) {
669+
vscode.env.openExternal(vscode.Uri.parse(message.url))
670+
}
671+
break
672+
case "fetchOpenGraphData":
673+
this.fetchOpenGraphData(message.text!)
674+
break
675+
case "checkIsImageUrl":
676+
this.checkIsImageUrl(message.text!)
677+
break
666678
case "openFile":
667679
openFile(message.text!)
668680
break
@@ -1955,6 +1967,53 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
19551967
return await this.context.secrets.get(key)
19561968
}
19571969

1970+
// Open Graph Data
1971+
1972+
async fetchOpenGraphData(url: string) {
1973+
try {
1974+
// Use the fetchOpenGraphData function from link-preview.ts
1975+
const ogData = await fetchOpenGraphData(url)
1976+
1977+
// Send the data back to the webview
1978+
await this.postMessageToWebview({
1979+
type: "openGraphData",
1980+
openGraphData: ogData,
1981+
url: url,
1982+
})
1983+
} catch (error) {
1984+
console.error(`Error fetching Open Graph data for ${url}:`, error)
1985+
// Send an error response
1986+
await this.postMessageToWebview({
1987+
type: "openGraphData",
1988+
error: `Failed to fetch Open Graph data: ${error}`,
1989+
url: url,
1990+
})
1991+
}
1992+
}
1993+
1994+
// Check if a URL is an image
1995+
async checkIsImageUrl(url: string) {
1996+
try {
1997+
// Check if the URL is an image
1998+
const isImage = await isImageUrl(url)
1999+
2000+
// Send the result back to the webview
2001+
await this.postMessageToWebview({
2002+
type: "isImageUrlResult",
2003+
isImage,
2004+
url,
2005+
})
2006+
} catch (error) {
2007+
console.error(`Error checking if URL is an image: ${url}`, error)
2008+
// Send an error response
2009+
await this.postMessageToWebview({
2010+
type: "isImageUrlResult",
2011+
isImage: false,
2012+
url,
2013+
})
2014+
}
2015+
}
2016+
19582017
// dev
19592018

19602019
async resetState() {
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import axios from "axios"
2+
import ogs from "open-graph-scraper"
3+
4+
export interface OpenGraphData {
5+
title?: string
6+
description?: string
7+
image?: string
8+
url?: string
9+
siteName?: string
10+
type?: string
11+
}
12+
13+
/**
14+
* Fetches Open Graph metadata from a URL
15+
* @param url The URL to fetch metadata from
16+
* @returns Promise resolving to OpenGraphData
17+
*/
18+
export async function fetchOpenGraphData(url: string): Promise<OpenGraphData> {
19+
try {
20+
const options = {
21+
url: url,
22+
timeout: 5000,
23+
headers: {
24+
"user-agent": "Mozilla/5.0 (compatible; VSCodeExtension/1.0; +https://cline.bot)",
25+
},
26+
onlyGetOpenGraphInfo: false, // Get all metadata, not just Open Graph
27+
fetchOptions: {
28+
redirect: "follow", // Follow redirects
29+
} as any,
30+
}
31+
32+
const { result } = await ogs(options)
33+
34+
// Use type assertion to avoid TypeScript errors
35+
const data = result as any
36+
37+
// Handle image URLs
38+
let imageUrl = data.ogImage?.[0]?.url || data.twitterImage?.[0]?.url
39+
40+
// If the image URL is relative, make it absolute
41+
if (imageUrl && (imageUrl.startsWith("/") || imageUrl.startsWith("./"))) {
42+
try {
43+
// Extract the base URL and make the relative URL absolute
44+
const urlObj = new URL(url)
45+
const baseUrl = `${urlObj.protocol}//${urlObj.hostname}`
46+
imageUrl = new URL(imageUrl, baseUrl).href
47+
} catch (error) {
48+
console.error(`Error converting relative URL to absolute: ${imageUrl}`, error)
49+
}
50+
}
51+
52+
return {
53+
title: data.ogTitle || data.twitterTitle || data.dcTitle || data.title || new URL(url).hostname,
54+
description:
55+
data.ogDescription ||
56+
data.twitterDescription ||
57+
data.dcDescription ||
58+
data.description ||
59+
"No description available",
60+
image: imageUrl,
61+
url: data.ogUrl || url,
62+
siteName: data.ogSiteName || new URL(url).hostname,
63+
type: data.ogType,
64+
}
65+
} catch (error) {
66+
console.error(`Error fetching Open Graph data for ${url}:`, error)
67+
// Return basic information based on the URL
68+
try {
69+
const urlObj = new URL(url)
70+
return {
71+
title: urlObj.hostname,
72+
description: url,
73+
url: url,
74+
siteName: urlObj.hostname,
75+
}
76+
} catch {
77+
return {
78+
title: url,
79+
description: url,
80+
url: url,
81+
}
82+
}
83+
}
84+
}
85+
86+
/**
87+
* Checks if a URL is an image by making a HEAD request and checking the content type
88+
* @param url The URL to check
89+
* @returns Promise resolving to boolean indicating if the URL is an image
90+
*/
91+
export async function isImageUrl(url: string): Promise<boolean> {
92+
try {
93+
const response = await axios.head(url, {
94+
headers: {
95+
"User-Agent": "Mozilla/5.0 (compatible; VSCodeExtension/1.0; +https://cline.bot)",
96+
},
97+
timeout: 3000,
98+
})
99+
100+
const contentType = response.headers["content-type"]
101+
return contentType && contentType.startsWith("image/")
102+
} catch (error) {
103+
console.error(`Error checking if URL is an image: ${url}`, error)
104+
// If we can't determine, fall back to checking the file extension
105+
return /\.(jpg|jpeg|png|gif|webp|svg)$/i.test(url)
106+
}
107+
}

src/shared/ExtensionMessage.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ export interface ExtensionMessage {
3131
| "mcpMarketplaceCatalog"
3232
| "mcpDownloadDetails"
3333
| "commitSearchResults"
34+
| "openGraphData"
35+
| "isImageUrlResult"
3436
text?: string
3537
action?:
3638
| "chatButtonClicked"
@@ -55,6 +57,16 @@ export interface ExtensionMessage {
5557
error?: string
5658
mcpDownloadDetails?: McpDownloadResponse
5759
commits?: GitCommit[]
60+
openGraphData?: {
61+
title?: string
62+
description?: string
63+
image?: string
64+
url?: string
65+
siteName?: string
66+
type?: string
67+
}
68+
url?: string
69+
isImage?: boolean
5870
}
5971

6072
export type Platform = "aix" | "darwin" | "freebsd" | "linux" | "openbsd" | "sunos" | "win32" | "unknown"

src/shared/WebviewMessage.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export interface WebviewMessage {
2222
| "requestOllamaModels"
2323
| "requestLmStudioModels"
2424
| "openImage"
25+
| "openInBrowser"
2526
| "openFile"
2627
| "openMention"
2728
| "cancelTask"
@@ -52,6 +53,9 @@ export interface WebviewMessage {
5253
| "fetchLatestMcpServersFromHub"
5354
| "telemetrySetting"
5455
| "openSettings"
56+
| "updateMcpTimeout"
57+
| "fetchOpenGraphData"
58+
| "checkIsImageUrl"
5559
// | "relaunchChromeDebugMode"
5660
text?: string
5761
disabled?: boolean
@@ -70,6 +74,9 @@ export interface WebviewMessage {
7074
serverName?: string
7175
toolName?: string
7276
autoApprove?: boolean
77+
78+
// For openInBrowser
79+
url?: string
7380
}
7481

7582
export type ClineAskResponse = "yesButtonClicked" | "noButtonClicked" | "messageResponse"

webview-ui/package-lock.json

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

webview-ui/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44
"private": true,
55
"dependencies": {
66
"@floating-ui/react": "^0.27.4",
7+
"@types/dompurify": "^3.0.5",
78
"@vscode/webview-ui-toolkit": "^1.4.0",
89
"debounce": "^2.1.1",
10+
"dompurify": "^3.2.4",
911
"fast-deep-equal": "^3.1.3",
1012
"fuse.js": "^7.0.0",
1113
"fzf": "^0.5.2",

0 commit comments

Comments
 (0)