/**
* 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
)}
);
}