/** * SearchRenderer Component * * Renders search results (grep, glob) with file:line format. * Shows pattern, match count, and individual results. */ import { useState } from 'react'; import { Search, ChevronDown, ChevronRight, FileText } from 'lucide-react'; import { cn } from '@/lib/utils'; import type { SearchDisplayData, SearchMatch } from '@dexto/core'; interface SearchRendererProps { /** Search display data from tool result */ data: SearchDisplayData; /** Maximum matches to show before truncation (default: 5) */ maxMatches?: number; /** Whether to start expanded (default: false) */ defaultExpanded?: boolean; } /** * Extract relative path from full path. */ function getRelativePath(path: string): string { const parts = path.split('/').filter(Boolean); if (parts.length <= 2) return path; return `.../${parts.slice(-2).join('/')}`; } /** * Renders a single search match result. */ function MatchResult({ match }: { match: SearchMatch }) { const { file, line, content } = match; return (
{getRelativePath(file)} {line > 0 && :{line}}
{content && (
{content.trim()}
)}
); } /** * Renders search results with collapsible match list. */ export function SearchRenderer({ data, maxMatches = 5, defaultExpanded = false, }: SearchRendererProps) { const { pattern, matches, totalMatches, truncated } = data; const [expanded, setExpanded] = useState(defaultExpanded); const [showAll, setShowAll] = useState(false); const displayMatches = showAll ? matches : matches.slice(0, maxMatches); const hasMoreMatches = matches.length > maxMatches && !showAll; const wasServerTruncated = truncated; return (
{/* Header with search info */}
{totalMatches}{' '} {totalMatches === 1 ? 'match' : 'matches'} for{' '} {pattern} {wasServerTruncated && ( (truncated) )}
{/* Results section */} {matches.length > 0 && (
{!expanded ? ( ) : (
{displayMatches.map((match, index) => ( ))} {hasMoreMatches && ( )}
)}
)} {/* No results */} {matches.length === 0 && (
No matches found
)}
); }