diff --git a/src/components/StreamMessage.tsx b/src/components/StreamMessage.tsx index 26decbc..2976750 100644 --- a/src/components/StreamMessage.tsx +++ b/src/components/StreamMessage.tsx @@ -35,7 +35,8 @@ import { TaskWidget, LSResultWidget, ThinkingWidget, - WebSearchWidget + WebSearchWidget, + WebFetchWidget } from "./ToolWidgets"; interface StreamMessageProps { @@ -246,6 +247,12 @@ const StreamMessageComponent: React.FC = ({ message, classNa return ; } + // WebFetch tool + if (toolName === "webfetch" && input?.url) { + renderedSomething = true; + return ; + } + // Default - return null return null; }; @@ -361,7 +368,7 @@ const StreamMessageComponent: React.FC = ({ message, classNa const toolUse = prevMsg.message.content.find((c: any) => c.type === 'tool_use' && c.id === content.tool_use_id); if (toolUse) { const toolName = toolUse.name?.toLowerCase(); - const toolsWithWidgets = ['task','edit','multiedit','todowrite','ls','read','glob','bash','write','grep','websearch']; + const toolsWithWidgets = ['task','edit','multiedit','todowrite','ls','read','glob','bash','write','grep','websearch','webfetch']; if (toolsWithWidgets.includes(toolName) || toolUse.name?.startsWith('mcp__')) { hasCorrespondingWidget = true; } diff --git a/src/components/ToolWidgets.tsx b/src/components/ToolWidgets.tsx index 58e784a..589b222 100644 --- a/src/components/ToolWidgets.tsx +++ b/src/components/ToolWidgets.tsx @@ -2288,3 +2288,187 @@ export const ThinkingWidget: React.FC<{ ); }; + +/** + * Widget for WebFetch tool - displays URL fetching with optional prompts + */ +export const WebFetchWidget: React.FC<{ + url: string; + prompt?: string; + result?: any; +}> = ({ url, prompt, result }) => { + const [isExpanded, setIsExpanded] = useState(false); + const [showFullContent, setShowFullContent] = useState(false); + + // Extract result content if available + let fetchedContent = ''; + let isLoading = !result; + let hasError = false; + + if (result) { + if (typeof result.content === 'string') { + fetchedContent = result.content; + } else if (result.content && typeof result.content === 'object') { + if (result.content.text) { + fetchedContent = result.content.text; + } else if (Array.isArray(result.content)) { + fetchedContent = result.content + .map((c: any) => (typeof c === 'string' ? c : c.text || JSON.stringify(c))) + .join('\n'); + } else { + fetchedContent = JSON.stringify(result.content, null, 2); + } + } + + // Check if there's an error + hasError = result.is_error || + fetchedContent.toLowerCase().includes('error') || + fetchedContent.toLowerCase().includes('failed'); + } + + // Truncate content for preview + const maxPreviewLength = 500; + const isTruncated = fetchedContent.length > maxPreviewLength; + const previewContent = isTruncated && !showFullContent + ? fetchedContent.substring(0, maxPreviewLength) + '...' + : fetchedContent; + + // Extract domain from URL for display + const getDomain = (urlString: string) => { + try { + const urlObj = new URL(urlString); + return urlObj.hostname; + } catch { + return urlString; + } + }; + + const handleUrlClick = async () => { + try { + await open(url); + } catch (error) { + console.error('Failed to open URL:', error); + } + }; + + return ( +
+ {/* Header with URL and optional prompt */} +
+ {/* URL Display */} +
+ + Fetching + +
+ + {/* Prompt Display */} + {prompt && ( +
+ + + {isExpanded && ( +
+

+ {prompt} +

+
+ )} +
+ )} +
+ + {/* Results */} + {isLoading ? ( +
+
+
+
+
+
+
+ Fetching content from {getDomain(url)}... +
+
+ ) : fetchedContent ? ( +
+ {hasError ? ( +
+
+ + Failed to fetch content +
+
+                {fetchedContent}
+              
+
+ ) : ( +
+ {/* Content Header */} +
+
+ + Content from {getDomain(url)} +
+ {isTruncated && ( + + )} +
+ + {/* Fetched Content */} +
+
+
+                    {previewContent}
+                  
+ {!showFullContent && isTruncated && ( +
+ )} +
+
+
+ )} +
+ ) : ( +
+
+
+ + No content returned +
+
+
+ )} +
+ ); +};