feat: implement automated codebase indexing based on user prompts and business logic changes

- Add AutoIndexingService with singleton pattern for automatic indexing triggers
- Analyze user prompts for keywords suggesting codebase exploration needs
- Monitor business logic file changes (reducers, API handlers, components, etc.)
- Integrate with Task processing pipeline to trigger indexing on relevant user prompts
- Integrate with FileWatcher to trigger indexing on business logic file modifications
- Add cooldown mechanism to prevent over-indexing (30-second intervals)
- Include comprehensive test suite with 10 passing tests
- Maintain backward compatibility with existing manual indexing functionality

Fixes #5690
This commit is contained in:
Roo Code 2025-07-14 11:32:05 +00:00
parent a163053430
commit a6c45fa858
6 changed files with 517 additions and 2 deletions

View file

@ -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<ClineEvents> {
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

View file

@ -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<boolean> {
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<boolean> {
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 = ["<task>", "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
}
}

View file

@ -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)
})
})
})

View file

@ -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

View file

@ -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<void> {
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<void> {
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<void> {
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)
}
}
}

View file

@ -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,