/** * Agentic file search executor * Executes search plans to find exact code locations before editing */ export interface SearchResult { filePath: string; lineNumber: number; lineContent: string; matchedTerm?: string; matchedPattern?: string; contextBefore: string[]; contextAfter: string[]; confidence: 'high' | 'medium' | 'low'; } export interface SearchPlan { editType: string; reasoning: string; searchTerms: string[]; regexPatterns?: string[]; fileTypesToSearch?: string[]; expectedMatches?: number; fallbackSearch?: { terms: string[]; patterns?: string[]; }; } export interface SearchExecutionResult { success: boolean; results: SearchResult[]; filesSearched: number; executionTime: number; usedFallback: boolean; error?: string; } /** * Execute a search plan against the codebase */ export function executeSearchPlan( searchPlan: SearchPlan, files: Record ): SearchExecutionResult { const startTime = Date.now(); const results: SearchResult[] = []; let filesSearched = 0; let usedFallback = false; const { searchTerms = [], regexPatterns = [], fileTypesToSearch = ['.jsx', '.tsx', '.js', '.ts'], fallbackSearch } = searchPlan; // Helper function to perform search const performSearch = (terms: string[], patterns?: string[]): SearchResult[] => { const searchResults: SearchResult[] = []; for (const [filePath, content] of Object.entries(files)) { // Skip files that don't match the desired extensions const shouldSearch = fileTypesToSearch.some(ext => filePath.endsWith(ext)); if (!shouldSearch) continue; filesSearched++; const lines = content.split('\n'); for (let i = 0; i < lines.length; i++) { const line = lines[i]; let matched = false; let matchedTerm: string | undefined; let matchedPattern: string | undefined; // Check simple search terms (case-insensitive) for (const term of terms) { if (line.toLowerCase().includes(term.toLowerCase())) { matched = true; matchedTerm = term; break; } } // Check regex patterns if no term match if (!matched && patterns) { for (const pattern of patterns) { try { const regex = new RegExp(pattern, 'i'); if (regex.test(line)) { matched = true; matchedPattern = pattern; break; } } catch (e) { console.warn(`[file-search] Invalid regex pattern: ${pattern}`); } } } if (matched) { // Get context lines (3 before, 3 after) const contextBefore = lines.slice(Math.max(0, i - 3), i); const contextAfter = lines.slice(i + 1, Math.min(lines.length, i + 4)); // Determine confidence based on match type and context let confidence: 'high' | 'medium' | 'low' = 'medium'; // High confidence if it's an exact match or in a component definition if (matchedTerm && line.includes(matchedTerm)) { confidence = 'high'; } else if (line.includes('function') || line.includes('export') || line.includes('return')) { confidence = 'high'; } else if (matchedPattern) { confidence = 'medium'; } searchResults.push({ filePath, lineNumber: i + 1, lineContent: line.trim(), matchedTerm, matchedPattern, contextBefore, contextAfter, confidence }); } } } return searchResults; }; // Execute primary search results.push(...performSearch(searchTerms, regexPatterns)); // If no results and we have a fallback, try it if (results.length === 0 && fallbackSearch) { console.log('[file-search] No results from primary search, trying fallback...'); usedFallback = true; results.push(...performSearch( fallbackSearch.terms, fallbackSearch.patterns )); } const executionTime = Date.now() - startTime; // Sort results by confidence results.sort((a, b) => { const confidenceOrder = { high: 3, medium: 2, low: 1 }; return confidenceOrder[b.confidence] - confidenceOrder[a.confidence]; }); return { success: results.length > 0, results, filesSearched, executionTime, usedFallback, error: results.length === 0 ? 'No matches found for search terms' : undefined }; } /** * Format search results for AI consumption */ export function formatSearchResultsForAI(results: SearchResult[]): string { if (results.length === 0) { return 'No search results found.'; } const sections: string[] = []; sections.push('šŸ” SEARCH RESULTS - EXACT LOCATIONS FOUND:\n'); // Group by file for better readability const resultsByFile = new Map(); for (const result of results) { if (!resultsByFile.has(result.filePath)) { resultsByFile.set(result.filePath, []); } resultsByFile.get(result.filePath)!.push(result); } for (const [filePath, fileResults] of resultsByFile) { sections.push(`\nšŸ“„ FILE: ${filePath}`); for (const result of fileResults) { sections.push(`\n šŸ“ Line ${result.lineNumber} (${result.confidence} confidence)`); if (result.matchedTerm) { sections.push(` Matched: "${result.matchedTerm}"`); } else if (result.matchedPattern) { sections.push(` Pattern: ${result.matchedPattern}`); } sections.push(` Code: ${result.lineContent}`); if (result.contextBefore.length > 0 || result.contextAfter.length > 0) { sections.push(` Context:`); for (const line of result.contextBefore) { sections.push(` ${line}`); } sections.push(` → ${result.lineContent}`); for (const line of result.contextAfter) { sections.push(` ${line}`); } } } } sections.push('\n\nšŸŽÆ RECOMMENDED ACTION:'); // Recommend the highest confidence result const bestResult = results[0]; sections.push(`Edit ${bestResult.filePath} at line ${bestResult.lineNumber}`); return sections.join('\n'); } /** * Select the best file to edit based on search results */ export function selectTargetFile( results: SearchResult[], editType: string ): { filePath: string; lineNumber: number; reason: string } | null { if (results.length === 0) return null; // For style updates, prefer components over CSS files if (editType === 'UPDATE_STYLE') { const componentResult = results.find(r => r.filePath.endsWith('.jsx') || r.filePath.endsWith('.tsx') ); if (componentResult) { return { filePath: componentResult.filePath, lineNumber: componentResult.lineNumber, reason: 'Found component with style to update' }; } } // For remove operations, find the component that renders the element if (editType === 'REMOVE_ELEMENT') { const renderResult = results.find(r => r.lineContent.includes('return') || r.lineContent.includes('<') ); if (renderResult) { return { filePath: renderResult.filePath, lineNumber: renderResult.lineNumber, reason: 'Found element to remove in render output' }; } } // Default: use highest confidence result const best = results[0]; return { filePath: best.filePath, lineNumber: best.lineNumber, reason: `Highest confidence match (${best.confidence})` }; }