diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index c8553a8fc67..759c3ac5536 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -88,6 +88,7 @@ import { ApiMessage } from "../task-persistence/apiMessages" import { getMessagesSinceLastSummary, summarizeConversation } from "../condense" import { maybeRemoveImageBlocks } from "../../api/transform/image-cleaning" import { restoreTodoListForTask } from "../tools/updateTodoListTool" +import { AutoIndexingService } from "../../services/code-index/auto-indexing/AutoIndexingService" // Constants const MAX_EXPONENTIAL_BACKOFF_SECONDS = 600 // 10 minutes @@ -1232,6 +1233,23 @@ export class Task extends EventEmitter { showRooIgnoredFiles, }) + // Check if automatic indexing should be triggered based on user prompt + const taskProvider = this.providerRef.deref() + if (taskProvider?.codeIndexManager) { + const autoIndexingService = AutoIndexingService.getInstance(taskProvider.codeIndexManager) + try { + const shouldTriggerIndexing = await autoIndexingService.analyzeUserPromptForIndexing(userContent) + if (shouldTriggerIndexing) { + // Trigger indexing asynchronously without blocking the task + autoIndexingService.triggerAutomaticIndexing("User prompt analysis").catch((error) => { + console.warn("[AutoIndexingService] Failed to trigger automatic indexing:", error) + }) + } + } catch (error) { + console.warn("[AutoIndexingService] Error analyzing user prompt for indexing:", error) + } + } + const environmentDetails = await getEnvironmentDetails(this, includeFileDetails) // Add environment details as its own text block, separate from tool diff --git a/src/services/code-index/auto-indexing/AutoIndexingService.ts b/src/services/code-index/auto-indexing/AutoIndexingService.ts new file mode 100644 index 00000000000..b2a0634240f --- /dev/null +++ b/src/services/code-index/auto-indexing/AutoIndexingService.ts @@ -0,0 +1,276 @@ +import * as vscode from "vscode" +import { Anthropic } from "@anthropic-ai/sdk" +import { CodeIndexManager } from "../manager" + +/** + * Service that automatically triggers codebase indexing based on user prompts + * and business logic changes without requiring manual user intervention. + */ +export class AutoIndexingService { + private static instance: AutoIndexingService | undefined + private codeIndexManager: CodeIndexManager | undefined + private lastIndexingTime: number = 0 + private readonly indexingCooldownMs = 30000 // 30 seconds cooldown between auto-indexing + + // Keywords that suggest the user might benefit from codebase indexing + private readonly indexingTriggerKeywords = [ + "find", + "search", + "locate", + "where", + "how does", + "how is", + "what does", + "understand", + "explain", + "analyze", + "explore", + "investigate", + "review", + "refactor", + "modify", + "change", + "update", + "implement", + "add feature", + "bug", + "error", + "issue", + "problem", + "fix", + "debug", + "trace", + "architecture", + "structure", + "design", + "pattern", + "flow", + "workflow", + "integration", + "dependency", + "relationship", + "connection", + "usage", + "api", + "endpoint", + "route", + "handler", + "controller", + "service", + "component", + "module", + "class", + "function", + "method", + "interface", + "state", + "reducer", + "store", + "context", + "provider", + "hook", + "database", + "model", + "schema", + "query", + "migration", + "entity", + ] + + // File patterns that indicate business logic changes + private readonly businessLogicPatterns = [ + /\/(reducers?|store|state)\//i, + /\/(api|handlers?|controllers?|services?)\//i, + /\/(components?|ui|views?|pages?)\//i, + /\/(models?|entities?|schemas?)\//i, + /\/(routes?|routing)\//i, + /\/(middleware|interceptors?)\//i, + /\/(utils?|helpers?|lib)\//i, + /\.(reducer|store|state|api|service|controller|handler)\./i, + /\/(src|lib|app)\//i, + ] + + private constructor(codeIndexManager?: CodeIndexManager) { + this.codeIndexManager = codeIndexManager + } + + public static getInstance(codeIndexManager?: CodeIndexManager): AutoIndexingService { + if (!AutoIndexingService.instance) { + if (codeIndexManager === undefined || codeIndexManager === null) { + throw new Error("CodeIndexManager is required to create AutoIndexingService instance") + } + AutoIndexingService.instance = new AutoIndexingService(codeIndexManager) + } + if (codeIndexManager && !AutoIndexingService.instance.codeIndexManager) { + AutoIndexingService.instance.codeIndexManager = codeIndexManager + } + return AutoIndexingService.instance + } + + /** + * Analyzes user content to determine if automatic indexing should be triggered + */ + public async analyzeUserPromptForIndexing(userContent: Anthropic.Messages.ContentBlockParam[]): Promise { + if (!this.shouldAttemptIndexing()) { + return false + } + + // Extract text content from user message + const textContent = this.extractTextFromUserContent(userContent) + if (!textContent) { + return false + } + + // Check if the prompt contains keywords that suggest codebase exploration + const containsTriggerKeywords = this.containsIndexingTriggerKeywords(textContent) + + // Check if the prompt mentions specific files or code patterns + const mentionsCodePatterns = this.mentionsCodePatterns(textContent) + + // Check if this is a new task (likely to benefit from fresh indexing) + const isNewTask = this.isNewTaskPrompt(textContent) + + return containsTriggerKeywords || mentionsCodePatterns || isNewTask + } + + /** + * Analyzes file changes to determine if they represent business logic changes + * that should trigger automatic indexing + */ + public shouldTriggerIndexingForFileChange(filePath: string): boolean { + if (!this.shouldAttemptIndexing()) { + return false + } + + // Check if the file matches business logic patterns + return this.businessLogicPatterns.some((pattern) => pattern.test(filePath)) + } + + /** + * Triggers automatic indexing if conditions are met + */ + public async triggerAutomaticIndexing(reason: string): Promise { + if (!this.shouldAttemptIndexing()) { + console.log(`[AutoIndexingService] Skipping indexing - conditions not met: ${reason}`) + return false + } + + try { + console.log(`[AutoIndexingService] Triggering automatic indexing: ${reason}`) + + // Update last indexing time to prevent rapid re-indexing + this.lastIndexingTime = Date.now() + + // Start indexing asynchronously + await this.codeIndexManager!.startIndexing() + + console.log(`[AutoIndexingService] Successfully triggered indexing: ${reason}`) + return true + } catch (error) { + console.error(`[AutoIndexingService] Failed to trigger automatic indexing:`, error) + return false + } + } + + /** + * Checks if indexing should be attempted based on current conditions + */ + private shouldAttemptIndexing(): boolean { + // Check if code index manager is available and properly configured + if ( + !this.codeIndexManager || + !this.codeIndexManager.isFeatureEnabled || + !this.codeIndexManager.isFeatureConfigured + ) { + return false + } + + // Check cooldown period to prevent over-indexing + const timeSinceLastIndexing = Date.now() - this.lastIndexingTime + if (timeSinceLastIndexing < this.indexingCooldownMs) { + return false + } + + return true + } + + /** + * Extracts text content from user content blocks + */ + private extractTextFromUserContent(userContent: Anthropic.Messages.ContentBlockParam[]): string { + const textBlocks: string[] = [] + + for (const block of userContent) { + if (block.type === "text") { + textBlocks.push(block.text) + } else if (block.type === "tool_result") { + if (typeof block.content === "string") { + textBlocks.push(block.content) + } else if (Array.isArray(block.content)) { + for (const contentBlock of block.content) { + if (contentBlock.type === "text") { + textBlocks.push(contentBlock.text) + } + } + } + } + } + + return textBlocks.join(" ").toLowerCase() + } + + /** + * Checks if text contains keywords that suggest indexing would be beneficial + */ + private containsIndexingTriggerKeywords(text: string): boolean { + return this.indexingTriggerKeywords.some((keyword) => text.includes(keyword.toLowerCase())) + } + + /** + * Checks if text mentions code patterns or file structures + */ + private mentionsCodePatterns(text: string): boolean { + // Check for mentions of file extensions + const codeFileExtensions = [ + ".ts", + ".js", + ".tsx", + ".jsx", + ".py", + ".java", + ".cs", + ".cpp", + ".c", + ".go", + ".rs", + ".php", + ] + const mentionsFileExtensions = codeFileExtensions.some((ext) => text.includes(ext)) + + // Check for mentions of common code terms + const codeTerms = ["function", "class", "component", "module", "import", "export", "interface", "type"] + const mentionsCodeTerms = codeTerms.some((term) => text.includes(term)) + + // Check for file path patterns + const mentionsFilePaths = + text.includes("/") && (text.includes("src") || text.includes("lib") || text.includes("app")) + + return mentionsFileExtensions || mentionsCodeTerms || mentionsFilePaths + } + + /** + * Checks if this appears to be a new task prompt + */ + private isNewTaskPrompt(text: string): boolean { + // Look for task-like language + const taskIndicators = ["", "please", "can you", "i need", "help me", "create", "build", "develop"] + return taskIndicators.some((indicator) => text.includes(indicator)) + } + + /** + * Disposes of the service instance + */ + public static dispose(): void { + AutoIndexingService.instance = undefined + } +} diff --git a/src/services/code-index/auto-indexing/__tests__/AutoIndexingService.spec.ts b/src/services/code-index/auto-indexing/__tests__/AutoIndexingService.spec.ts new file mode 100644 index 00000000000..d6d0a117f5a --- /dev/null +++ b/src/services/code-index/auto-indexing/__tests__/AutoIndexingService.spec.ts @@ -0,0 +1,166 @@ +import { AutoIndexingService } from "../AutoIndexingService" + +// Mock TelemetryService +vi.mock("@roo-code/telemetry", () => ({ + TelemetryService: { + instance: { + captureEvent: vi.fn(), + }, + }, +})) + +describe("AutoIndexingService", () => { + let mockCodeIndexManager: any + + beforeEach(() => { + // Clear singleton instance before each test + ;(AutoIndexingService as any)._instance = undefined + + // Mock CodeIndexManager with all required properties + mockCodeIndexManager = { + startIndexing: vi.fn().mockResolvedValue(undefined), + state: "Standby", + isFeatureEnabled: true, + isFeatureConfigured: true, + } + }) + + afterEach(() => { + vi.clearAllMocks() + // Clear singleton instance after each test + ;(AutoIndexingService as any)._instance = undefined + }) + + describe("getInstance", () => { + it("should return the same instance when called multiple times", () => { + const instance1 = AutoIndexingService.getInstance(mockCodeIndexManager) + const instance2 = AutoIndexingService.getInstance(mockCodeIndexManager) + expect(instance1).toBe(instance2) + }) + + it("should create instance with valid CodeIndexManager", () => { + const instance = AutoIndexingService.getInstance(mockCodeIndexManager) + expect(instance).toBeInstanceOf(AutoIndexingService) + }) + }) + + describe("analyzeUserPromptForIndexing", () => { + it("should return true for prompts containing indexing trigger keywords", async () => { + const autoIndexingService = AutoIndexingService.getInstance(mockCodeIndexManager) + // Reset the lastIndexingTime to allow indexing + ;(autoIndexingService as any).lastIndexingTime = 0 + + const testCases = [ + "explore the codebase", + "find all functions", + "search for components", + "understand the architecture", + "analyze the code structure", + ] + + for (const text of testCases) { + const userContent = [{ type: "text" as const, text }] + const result = await autoIndexingService.analyzeUserPromptForIndexing(userContent) + expect(result).toBe(true) + } + }) + + it("should return false for prompts without trigger keywords", async () => { + const autoIndexingService = AutoIndexingService.getInstance(mockCodeIndexManager) + // Reset the lastIndexingTime to allow indexing + ;(autoIndexingService as any).lastIndexingTime = 0 + + const userContent = [{ type: "text" as const, text: "hello world" }] + const result = await autoIndexingService.analyzeUserPromptForIndexing(userContent) + expect(result).toBe(false) + }) + + it("should handle empty content", async () => { + const autoIndexingService = AutoIndexingService.getInstance(mockCodeIndexManager) + // Reset the lastIndexingTime to allow indexing + ;(autoIndexingService as any).lastIndexingTime = 0 + + const userContent = [{ type: "text" as const, text: "" }] + const result = await autoIndexingService.analyzeUserPromptForIndexing(userContent) + expect(result).toBe(false) + }) + }) + + describe("shouldTriggerIndexingForFileChange", () => { + it("should return true for business logic files", () => { + const autoIndexingService = AutoIndexingService.getInstance(mockCodeIndexManager) + // Reset the lastIndexingTime to allow indexing + ;(autoIndexingService as any).lastIndexingTime = 0 + + const businessLogicFiles = [ + "/src/reducers/userReducer.ts", + "/app/api/handlers/user.ts", + "/components/UserProfile.tsx", + "/src/services/authService.ts", + "/lib/utils/helper.ts", + ] + + for (const filePath of businessLogicFiles) { + const result = autoIndexingService.shouldTriggerIndexingForFileChange(filePath) + expect(result).toBe(true) + } + }) + + it("should return false for non-business logic files", () => { + const autoIndexingService = AutoIndexingService.getInstance(mockCodeIndexManager) + // Reset the lastIndexingTime to allow indexing + ;(autoIndexingService as any).lastIndexingTime = 0 + + const nonBusinessLogicFiles = [ + "/README.md", + "/package.json", + "/tsconfig.json", + "/.gitignore", + "/docs/guide.md", + ] + + for (const filePath of nonBusinessLogicFiles) { + const result = autoIndexingService.shouldTriggerIndexingForFileChange(filePath) + expect(result).toBe(false) + } + }) + }) + + describe("core functionality", () => { + it("should have proper singleton behavior", () => { + const instance1 = AutoIndexingService.getInstance(mockCodeIndexManager) + const instance2 = AutoIndexingService.getInstance(mockCodeIndexManager) + expect(instance1).toBe(instance2) + }) + + it("should analyze user prompts correctly", async () => { + const autoIndexingService = AutoIndexingService.getInstance(mockCodeIndexManager) + ;(autoIndexingService as any).lastIndexingTime = 0 + + // Test positive cases + const positiveContent = [{ type: "text" as const, text: "explore the codebase structure" }] + const positiveResult = await autoIndexingService.analyzeUserPromptForIndexing(positiveContent) + expect(positiveResult).toBe(true) + + // Test negative cases + const negativeContent = [{ type: "text" as const, text: "hello world" }] + const negativeResult = await autoIndexingService.analyzeUserPromptForIndexing(negativeContent) + expect(negativeResult).toBe(false) + }) + + it("should detect business logic file changes correctly", () => { + const autoIndexingService = AutoIndexingService.getInstance(mockCodeIndexManager) + ;(autoIndexingService as any).lastIndexingTime = 0 + + // Test positive cases + expect(autoIndexingService.shouldTriggerIndexingForFileChange("/src/components/App.tsx")).toBe(true) + expect(autoIndexingService.shouldTriggerIndexingForFileChange("/api/handlers/user.ts")).toBe(true) + expect(autoIndexingService.shouldTriggerIndexingForFileChange("/reducers/auth.ts")).toBe(true) + + // Test negative cases + expect(autoIndexingService.shouldTriggerIndexingForFileChange("/README.md")).toBe(false) + expect(autoIndexingService.shouldTriggerIndexingForFileChange("/package.json")).toBe(false) + expect(autoIndexingService.shouldTriggerIndexingForFileChange("/.gitignore")).toBe(false) + }) + }) +}) diff --git a/src/services/code-index/manager.ts b/src/services/code-index/manager.ts index 18e0752c34d..a1e52d16cc7 100644 --- a/src/services/code-index/manager.ts +++ b/src/services/code-index/manager.ts @@ -264,6 +264,7 @@ export class CodeIndexManager { this.context, this._cacheManager!, ignoreInstance, + this, // Pass this CodeIndexManager instance for auto-indexing ) // Validate embedder configuration before proceeding diff --git a/src/services/code-index/processors/file-watcher.ts b/src/services/code-index/processors/file-watcher.ts index 6dc1cd1835d..5c8b0b0f472 100644 --- a/src/services/code-index/processors/file-watcher.ts +++ b/src/services/code-index/processors/file-watcher.ts @@ -26,6 +26,7 @@ import { isPathInIgnoredDirectory } from "../../glob/ignore-utils" import { TelemetryService } from "@roo-code/telemetry" import { TelemetryEventName } from "@roo-code/types" import { sanitizeErrorMessage } from "../shared/validation-helpers" +import { AutoIndexingService } from "../auto-indexing/AutoIndexingService" /** * Implementation of the file watcher interface @@ -78,6 +79,7 @@ export class FileWatcher implements IFileWatcher { private vectorStore?: IVectorStore, ignoreInstance?: Ignore, ignoreController?: RooIgnoreController, + private codeIndexManager?: any, // CodeIndexManager reference for auto-indexing ) { this.ignoreController = ignoreController || new RooIgnoreController(workspacePath) if (ignoreInstance) { @@ -123,6 +125,9 @@ export class FileWatcher implements IFileWatcher { private async handleFileCreated(uri: vscode.Uri): Promise { this.accumulatedEvents.set(uri.fsPath, { uri, type: "create" }) this.scheduleBatchProcessing() + + // Check if this file creation should trigger automatic indexing + this.checkForBusinessLogicChange(uri.fsPath, "File created") } /** @@ -132,6 +137,9 @@ export class FileWatcher implements IFileWatcher { private async handleFileChanged(uri: vscode.Uri): Promise { this.accumulatedEvents.set(uri.fsPath, { uri, type: "change" }) this.scheduleBatchProcessing() + + // Check if this file change should trigger automatic indexing + this.checkForBusinessLogicChange(uri.fsPath, "File changed") } /** @@ -141,6 +149,9 @@ export class FileWatcher implements IFileWatcher { private async handleFileDeleted(uri: vscode.Uri): Promise { this.accumulatedEvents.set(uri.fsPath, { uri, type: "delete" }) this.scheduleBatchProcessing() + + // Check if this file deletion should trigger automatic indexing + this.checkForBusinessLogicChange(uri.fsPath, "File deleted") } /** @@ -575,4 +586,29 @@ export class FileWatcher implements IFileWatcher { } } } + + /** + * Checks if a file change represents a business logic change that should trigger automatic indexing + * @param filePath Path to the changed file + * @param reason Reason for the change (for logging) + */ + private checkForBusinessLogicChange(filePath: string, reason: string): void { + if (!this.codeIndexManager) { + return + } + + try { + const autoIndexingService = AutoIndexingService.getInstance(this.codeIndexManager) + const shouldTriggerIndexing = autoIndexingService.shouldTriggerIndexingForFileChange(filePath) + + if (shouldTriggerIndexing) { + // Trigger indexing asynchronously without blocking file processing + autoIndexingService.triggerAutomaticIndexing(`${reason}: ${filePath}`).catch((error) => { + console.warn("[FileWatcher] Failed to trigger automatic indexing for business logic change:", error) + }) + } + } catch (error) { + console.warn("[FileWatcher] Error checking for business logic change:", error) + } + } } diff --git a/src/services/code-index/service-factory.ts b/src/services/code-index/service-factory.ts index a741aaf72a7..4fc642d792b 100644 --- a/src/services/code-index/service-factory.ts +++ b/src/services/code-index/service-factory.ts @@ -158,8 +158,18 @@ export class CodeIndexServiceFactory { vectorStore: IVectorStore, cacheManager: CacheManager, ignoreInstance: Ignore, + codeIndexManager?: any, // CodeIndexManager reference for auto-indexing ): IFileWatcher { - return new FileWatcher(this.workspacePath, context, cacheManager, embedder, vectorStore, ignoreInstance) + return new FileWatcher( + this.workspacePath, + context, + cacheManager, + embedder, + vectorStore, + ignoreInstance, + undefined, + codeIndexManager, + ) } /** @@ -170,6 +180,7 @@ export class CodeIndexServiceFactory { context: vscode.ExtensionContext, cacheManager: CacheManager, ignoreInstance: Ignore, + codeIndexManager?: any, // CodeIndexManager reference for auto-indexing ): { embedder: IEmbedder vectorStore: IVectorStore @@ -185,7 +196,14 @@ export class CodeIndexServiceFactory { const vectorStore = this.createVectorStore() const parser = codeParser const scanner = this.createDirectoryScanner(embedder, vectorStore, parser, ignoreInstance) - const fileWatcher = this.createFileWatcher(context, embedder, vectorStore, cacheManager, ignoreInstance) + const fileWatcher = this.createFileWatcher( + context, + embedder, + vectorStore, + cacheManager, + ignoreInstance, + codeIndexManager, + ) return { embedder,