diff --git a/src/components/GitHubAgentBrowser.tsx b/src/components/GitHubAgentBrowser.tsx index 422076e..5f02ba6 100644 --- a/src/components/GitHubAgentBrowser.tsx +++ b/src/components/GitHubAgentBrowser.tsx @@ -18,6 +18,7 @@ import { Badge } from "@/components/ui/badge"; import { api, type GitHubAgentFile, type AgentExport, type Agent } from "@/lib/api"; import { type AgentIconName } from "./CCAgents"; import { ICON_MAP } from "./IconPicker"; +import { open } from "@tauri-apps/plugin-shell"; interface GitHubAgentBrowserProps { isOpen: boolean; @@ -148,6 +149,15 @@ export const GitHubAgentBrowser: React.FC = ({ return ; }; + const handleGitHubLinkClick = async (e: React.MouseEvent) => { + e.preventDefault(); + try { + await open("https://github.com/getAsterisk/claudia/tree/main/cc_agents"); + } catch (error) { + console.error('Failed to open GitHub link:', error); + } + }; + return ( @@ -163,15 +173,13 @@ export const GitHubAgentBrowser: React.FC = ({

Agents are fetched from{" "} - github.com/getAsterisk/claudia/cc_agents - +

You can contribute your custom agents to the repository! diff --git a/src/components/StreamMessage.tsx b/src/components/StreamMessage.tsx index 28f1118..26decbc 100644 --- a/src/components/StreamMessage.tsx +++ b/src/components/StreamMessage.tsx @@ -34,7 +34,8 @@ import { SystemInitializedWidget, TaskWidget, LSResultWidget, - ThinkingWidget + ThinkingWidget, + WebSearchWidget } from "./ToolWidgets"; interface StreamMessageProps { @@ -239,6 +240,12 @@ const StreamMessageComponent: React.FC = ({ message, classNa return ; } + // WebSearch tool + if (toolName === "websearch" && input?.query) { + renderedSomething = true; + return ; + } + // Default - return null return null; }; @@ -354,7 +361,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']; + const toolsWithWidgets = ['task','edit','multiedit','todowrite','ls','read','glob','bash','write','grep','websearch']; if (toolsWithWidgets.includes(toolName) || toolUse.name?.startsWith('mcp__')) { hasCorrespondingWidget = true; } diff --git a/src/components/ToolWidgets.tsx b/src/components/ToolWidgets.tsx index d8a2dc1..58e784a 100644 --- a/src/components/ToolWidgets.tsx +++ b/src/components/ToolWidgets.tsx @@ -51,6 +51,8 @@ import { createPortal } from "react-dom"; import * as Diff from 'diff'; import { Card, CardContent } from "@/components/ui/card"; import { detectLinks, makeLinksClickable } from "@/lib/linkDetector"; +import ReactMarkdown from "react-markdown"; +import { open } from "@tauri-apps/plugin-shell"; /** * Widget for TodoWrite tool - displays a beautiful TODO list @@ -857,45 +859,205 @@ export const GrepWidget: React.FC<{ path?: string; exclude?: string; result?: any; -}> = ({ pattern, include, path, exclude, result: _result }) => { +}> = ({ pattern, include, path, exclude, result }) => { + const [isExpanded, setIsExpanded] = useState(true); + + // Extract result content if available + let resultContent = ''; + let isError = false; + + if (result) { + isError = result.is_error || false; + if (typeof result.content === 'string') { + resultContent = result.content; + } else if (result.content && typeof result.content === 'object') { + if (result.content.text) { + resultContent = result.content.text; + } else if (Array.isArray(result.content)) { + resultContent = result.content + .map((c: any) => (typeof c === 'string' ? c : c.text || JSON.stringify(c))) + .join('\n'); + } else { + resultContent = JSON.stringify(result.content, null, 2); + } + } + } + + // Parse grep results to extract file paths and matches + const parseGrepResults = (content: string) => { + const lines = content.split('\n').filter(line => line.trim()); + const results: Array<{ + file: string; + lineNumber: number; + content: string; + }> = []; + + lines.forEach(line => { + // Common grep output format: filename:lineNumber:content + const match = line.match(/^(.+?):(\d+):(.*)$/); + if (match) { + results.push({ + file: match[1], + lineNumber: parseInt(match[2], 10), + content: match[3] + }); + } + }); + + return results; + }; + + const grepResults = result && !isError ? parseGrepResults(resultContent) : []; + return (

-
- +
+ Searching with grep + {!result && ( +
+
+ Searching... +
+ )}
-
-
- Pattern: - - {pattern} - + + {/* Search Parameters */} +
+
+ {/* Pattern with regex highlighting */} +
+
+ + Pattern +
+ + {pattern} + +
+ + {/* Path */} + {path && ( +
+
+ + Path +
+ + {path} + +
+ )} + + {/* Include/Exclude patterns in a row */} + {(include || exclude) && ( +
+ {include && ( +
+
+ + Include +
+ + {include} + +
+ )} + + {exclude && ( +
+
+ + Exclude +
+ + {exclude} + +
+ )} +
+ )}
- {path && ( -
- Path: - - {path} - -
- )} - {include && ( -
- Include: - - {include} - -
- )} - {exclude && ( -
- Exclude: - - {exclude} - -
- )}
+ + {/* Results */} + {result && ( +
+ {isError ? ( +
+ +
+ {resultContent || "Search failed"} +
+
+ ) : grepResults.length > 0 ? ( + <> + + + {isExpanded && ( +
+
+ {grepResults.map((match, idx) => { + const fileName = match.file.split('/').pop() || match.file; + const dirPath = match.file.substring(0, match.file.lastIndexOf('/')); + + return ( +
+
+ + + {match.lineNumber} + +
+ +
+
+ + {fileName} + + {dirPath && ( + + {dirPath} + + )} +
+ + {match.content.trim()} + +
+
+ ); + })} +
+
+ )} + + ) : ( +
+ +
+ No matches found for the given pattern. +
+
+ )} +
+ )}
); }; @@ -1870,6 +2032,218 @@ export const TaskWidget: React.FC<{ ); }; +/** + * Widget for WebSearch tool - displays web search query and results + */ +export const WebSearchWidget: React.FC<{ + query: string; + result?: any; +}> = ({ query, result }) => { + const [expandedSections, setExpandedSections] = useState>(new Set()); + + // Parse the result to extract all links sections and build a structured representation + const parseSearchResult = (resultContent: string) => { + const sections: Array<{ + type: 'text' | 'links'; + content: string | Array<{ title: string; url: string }>; + }> = []; + + // Split by "Links: [" to find all link sections + const parts = resultContent.split(/Links:\s*\[/); + + // First part is always text (or empty) + if (parts[0]) { + sections.push({ type: 'text', content: parts[0].trim() }); + } + + // Process each links section + parts.slice(1).forEach(part => { + try { + // Find the closing bracket + const closingIndex = part.indexOf(']'); + if (closingIndex === -1) return; + + const linksJson = '[' + part.substring(0, closingIndex + 1); + const remainingText = part.substring(closingIndex + 1).trim(); + + // Parse the JSON array + const links = JSON.parse(linksJson); + sections.push({ type: 'links', content: links }); + + // Add any remaining text + if (remainingText) { + sections.push({ type: 'text', content: remainingText }); + } + } catch (e) { + // If parsing fails, treat it as text + sections.push({ type: 'text', content: 'Links: [' + part }); + } + }); + + return sections; + }; + + const toggleSection = (index: number) => { + const newExpanded = new Set(expandedSections); + if (newExpanded.has(index)) { + newExpanded.delete(index); + } else { + newExpanded.add(index); + } + setExpandedSections(newExpanded); + }; + + // Extract result content if available + let searchResults: { + sections: Array<{ + type: 'text' | 'links'; + content: string | Array<{ title: string; url: string }>; + }>; + noResults: boolean; + } = { sections: [], noResults: false }; + + if (result) { + let resultContent = ''; + if (typeof result.content === 'string') { + resultContent = result.content; + } else if (result.content && typeof result.content === 'object') { + if (result.content.text) { + resultContent = result.content.text; + } else if (Array.isArray(result.content)) { + resultContent = result.content + .map((c: any) => (typeof c === 'string' ? c : c.text || JSON.stringify(c))) + .join('\n'); + } else { + resultContent = JSON.stringify(result.content, null, 2); + } + } + + searchResults.noResults = resultContent.toLowerCase().includes('no links found') || + resultContent.toLowerCase().includes('no results'); + searchResults.sections = parseSearchResult(resultContent); + } + + const handleLinkClick = async (url: string) => { + try { + await open(url); + } catch (error) { + console.error('Failed to open URL:', error); + } + }; + + return ( +
+ {/* Subtle Search Query Header */} +
+ + Web Search + {query} +
+ + {/* Results */} + {result && ( +
+ {!searchResults.sections.length ? ( +
+
+
+
+
+
+ Searching... +
+ ) : searchResults.noResults ? ( +
+
+ + No results found +
+
+ ) : ( +
+ {searchResults.sections.map((section, idx) => { + if (section.type === 'text') { + return ( +
+ {section.content as string} +
+ ); + } else if (section.type === 'links' && Array.isArray(section.content)) { + const links = section.content; + const isExpanded = expandedSections.has(idx); + + return ( +
+ {/* Toggle Button */} + + + {/* Links Display */} + {isExpanded ? ( + /* Expanded Card View */ +
+ {links.map((link, linkIdx) => ( + + ))} +
+ ) : ( + /* Collapsed Pills View */ +
+ {links.map((link, linkIdx) => ( + + ))} +
+ )} +
+ ); + } + return null; + })} +
+ )} +
+ )} +
+ ); +}; + /** * Widget for displaying AI thinking/reasoning content * Collapsible and closed by default