import { memo, useMemo } from "react"; import { Search } from "lucide-react"; import type { ToolPart } from "../types/parts"; import { PreviewCard, PreviewEmpty, PreviewError, PreviewLoading } from "./preview-primitives"; interface GrepMatch { path: string; line?: number; text: string; } function coerceString(value: unknown): string | undefined { return typeof value === "string" && value.trim() ? value : undefined; } function parseMatch(value: unknown): GrepMatch | null { if (typeof value === "string") { const [path, maybeLine, ...rest] = value.split(":"); const line = Number(maybeLine); if (rest.length > 0 && Number.isFinite(line)) { return { path, line, text: rest.join(":").trim(), }; } return { path: "match", text: value, }; } if (!value || typeof value !== "object") { return null; } const record = value as Record; const path = coerceString(record.path) ?? coerceString(record.file) ?? coerceString(record.filePath) ?? "match"; const lineValue = record.line ?? record.lineNumber ?? record.line_number; const line = typeof lineValue === "number" ? lineValue : Number(lineValue); const text = coerceString(record.text) ?? coerceString(record.content) ?? coerceString(record.lineText) ?? coerceString(record.preview); if (!text) { return null; } return { path, line: Number.isFinite(line) ? line : undefined, text, }; } function extractPattern(input: unknown): string | undefined { if (!input || typeof input !== "object") { return undefined; } const record = input as Record; return coerceString(record.pattern) ?? coerceString(record.query); } function extractMatches(output: unknown): GrepMatch[] { if (Array.isArray(output)) { return output.map(parseMatch).filter((value): value is GrepMatch => value !== null); } if (output && typeof output === "object") { const record = output as Record; const candidates = [record.matches, record.results, record.output]; for (const candidate of candidates) { if (Array.isArray(candidate)) { return candidate.map(parseMatch).filter((value): value is GrepMatch => value !== null); } } } if (typeof output === "string") { return output .split("\n") .map((line) => line.trim()) .filter(Boolean) .map(parseMatch) .filter((value): value is GrepMatch => value !== null); } return []; } export interface GrepResultsPreviewProps { part: ToolPart; } export const GrepResultsPreview = memo(({ part }: GrepResultsPreviewProps) => { const pattern = extractPattern(part.state.input); const matches = useMemo(() => extractMatches(part.state.output), [part.state.output]); const groupedMatches = useMemo(() => { const groups = new Map(); for (const match of matches) { const list = groups.get(match.path) ?? []; list.push(match); groups.set(match.path, list); } return [...groups.entries()]; }, [matches]); return ( } title={pattern ? `Search results for "${pattern}"` : "Search results"} description={matches.length > 0 ? `${matches.length} match${matches.length === 1 ? "" : "es"}` : undefined} > {part.state.status === "running" ? : null} {part.state.error ? : null} {part.state.status === "completed" && matches.length === 0 ? ( ) : null} {groupedMatches.map(([path, pathMatches]) => (
{path}
{pathMatches.map((match, index) => (
{match.line ?? "·"}
                  {match.text}
                
))}
))}
); }); GrepResultsPreview.displayName = "GrepResultsPreview";