From f8658b2ba8582c6ad4d62f2f948cb8619e2888c0 Mon Sep 17 00:00:00 2001 From: hannesrudolph Date: Thu, 3 Jul 2025 15:59:54 -0600 Subject: [PATCH] fix: resolve indexing worker issues for PR #5356 - Fix extension hanging when no workspace is open by adding validation to WorkspaceTracker - Fix Qdrant point ID format error by using UUID v5 instead of file paths - Remove console.log statements that were causing extension to get stuck loading - Add tests for UUID generation in indexing worker - Add tests for no workspace scenario in WorkspaceTracker - Add empty path validation across multiple files to prevent errors --- packages/build/src/esbuild.ts | 15 ++ src/core/checkpoints/index.ts | 3 - src/core/environment/getEnvironmentDetails.ts | 4 +- src/core/task/Task.ts | 10 +- src/core/tools/executeCommandTool.ts | 1 - src/core/tools/listFilesTool.ts | 11 ++ src/core/webview/ClineProvider.ts | 31 ---- src/extension.ts | 15 +- .../workspace/WorkspaceTracker.ts | 42 +++-- .../WorkspaceTracker.noWorkspace.spec.ts | 99 ++++++++++ .../__tests__/openai-compatible.spec.ts | 20 +- .../embedders/__tests__/openai.spec.ts | 24 +-- src/services/code-index/manager.ts | 31 +++- .../__tests__/qdrant-client.spec.ts | 8 +- .../code-index/vector-store/qdrant-client.ts | 5 + .../worker-utils/RooIgnoreController.ts | 7 +- .../code-index/worker-utils/cache-manager.ts | 5 + .../code-index/worker-utils/file-watcher.ts | 7 +- .../code-index/worker-utils/scanner.ts | 8 +- src/services/glob/list-files.ts | 5 + src/services/ripgrep/index.ts | 5 + src/workers/__tests__/indexing-worker.spec.ts | 84 +++++++++ src/workers/indexing-worker.ts | 171 +++++++++++------- 23 files changed, 439 insertions(+), 172 deletions(-) create mode 100644 src/integrations/workspace/__tests__/WorkspaceTracker.noWorkspace.spec.ts create mode 100644 src/workers/__tests__/indexing-worker.spec.ts diff --git a/packages/build/src/esbuild.ts b/packages/build/src/esbuild.ts index 75b81d5182..97312f1086 100644 --- a/packages/build/src/esbuild.ts +++ b/packages/build/src/esbuild.ts @@ -143,6 +143,14 @@ export function copyWasms(srcDir: string, distDir: string): void { console.log(`[copyWasms] Copied tree-sitter.wasm to ${distDir}`) + // Also copy tree-sitter.wasm to the workers directory. + fs.copyFileSync( + path.join(nodeModulesDir, "web-tree-sitter", "tree-sitter.wasm"), + path.join(workersDir, "tree-sitter.wasm"), + ) + + console.log(`[copyWasms] Copied tree-sitter.wasm to ${workersDir}`) + // Copy language-specific WASM files. const languageWasmDir = path.join(nodeModulesDir, "tree-sitter-wasms", "out") @@ -158,6 +166,13 @@ export function copyWasms(srcDir: string, distDir: string): void { }) console.log(`[copyWasms] Copied ${wasmFiles.length} tree-sitter language wasms to ${distDir}`) + + // Also copy language-specific WASM files to the workers directory. + wasmFiles.forEach((filename) => { + fs.copyFileSync(path.join(languageWasmDir, filename), path.join(workersDir, filename)) + }) + + console.log(`[copyWasms] Copied ${wasmFiles.length} tree-sitter language wasms to ${workersDir}`) } export function copyLocales(srcDir: string, distDir: string): void { diff --git a/src/core/checkpoints/index.ts b/src/core/checkpoints/index.ts index dcbe796eb7..b6106feb73 100644 --- a/src/core/checkpoints/index.ts +++ b/src/core/checkpoints/index.ts @@ -24,7 +24,6 @@ export function getCheckpointService(cline: Task) { } if (cline.checkpointServiceInitializing) { - console.log("[Task#getCheckpointService] checkpoint service is still initializing") return undefined } @@ -137,7 +136,6 @@ async function getInitializedCheckpointService( try { await pWaitFor( () => { - console.log("[Task#getCheckpointService] waiting for service to initialize") return service.isInitialized }, { interval, timeout }, @@ -167,7 +165,6 @@ export async function checkpointSave(cline: Task, force = false) { // Start the checkpoint process in the background. return service.saveCheckpoint(`Task: ${cline.taskId}, Time: ${Date.now()}`, { allowEmpty: force }).catch((err) => { - console.error("[Task#checkpointSave] caught unexpected error, disabling checkpoints", err) cline.enableCheckpoints = false }) } diff --git a/src/core/environment/getEnvironmentDetails.ts b/src/core/environment/getEnvironmentDetails.ts index 9120b5e8c7..43bfcddb60 100644 --- a/src/core/environment/getEnvironmentDetails.ts +++ b/src/core/environment/getEnvironmentDetails.ts @@ -250,7 +250,7 @@ export async function getEnvironmentDetails(cline: Task, includeFileDetails: boo // Don't want to immediately access desktop since it would show // permission popup. details += "(Desktop files not shown automatically. Use list_files to explore if needed.)" - } else { + } else if (cline.cwd && cline.cwd.trim() !== "") { const maxFiles = maxWorkspaceFiles ?? 200 const [files, didHitLimit] = await listFiles(cline.cwd, true, maxFiles) const { showRooIgnoredFiles = true } = state ?? {} @@ -264,6 +264,8 @@ export async function getEnvironmentDetails(cline: Task, includeFileDetails: boo ) details += result + } else { + details += "(No workspace folder open - file listing not available)" } } diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 4f0d32c8c1..19c8a0999f 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -1008,8 +1008,6 @@ export class Task extends EventEmitter { await this.overwriteApiConversationHistory(modifiedApiConversationHistory) - console.log(`[subtasks] task ${this.taskId}.${this.instanceId} resuming from history item`) - await this.initiateTaskLoop(newUserContent) } @@ -1067,8 +1065,6 @@ export class Task extends EventEmitter { } public async abortTask(isAbandoned = false) { - console.log(`[subtasks] aborting task ${this.taskId}.${this.instanceId}`) - // Will stop any autonomously running promises. if (isAbandoned) { this.abandoned = true @@ -1383,8 +1379,6 @@ export class Task extends EventEmitter { } if (this.abort) { - console.log(`aborting stream, this.abandoned = ${this.abandoned}`) - if (!this.abandoned) { // Only need to gracefully abort if this instance // isn't abandoned (sometimes OpenRouter stream @@ -1716,7 +1710,9 @@ export class Task extends EventEmitter { const contextWindow = modelInfo.contextWindow - const currentProfileId = state?.listApiConfigMeta.find((profile) => profile.name === state?.currentApiConfigName)?.id ?? "default"; + const currentProfileId = + state?.listApiConfigMeta.find((profile) => profile.name === state?.currentApiConfigName)?.id ?? + "default" const truncateResult = await truncateConversationIfNeeded({ messages: this.apiConversationHistory, diff --git a/src/core/tools/executeCommandTool.ts b/src/core/tools/executeCommandTool.ts index 795beccc06..a26300de81 100644 --- a/src/core/tools/executeCommandTool.ts +++ b/src/core/tools/executeCommandTool.ts @@ -179,7 +179,6 @@ export async function executeCommand( completed = true }, onShellExecutionStarted: (pid: number | undefined) => { - console.log(`[executeCommand] onShellExecutionStarted: ${pid}`) const status: CommandExecutionStatus = { executionId, status: "started", pid, command } clineProvider?.postMessageToWebview({ type: "commandExecutionStatus", text: JSON.stringify(status) }) }, diff --git a/src/core/tools/listFilesTool.ts b/src/core/tools/listFilesTool.ts index dcd7655b1f..042be9548c 100644 --- a/src/core/tools/listFilesTool.ts +++ b/src/core/tools/listFilesTool.ts @@ -37,6 +37,17 @@ export async function listFilesTool( // Calculate if the path is outside workspace const absolutePath = relDirPath ? path.resolve(cline.cwd, relDirPath) : cline.cwd + + // Check if cline.cwd is empty (no workspace open) + if (!cline.cwd || cline.cwd.trim() === "") { + cline.consecutiveMistakeCount++ + cline.recordToolError("list_files") + pushToolResult( + await cline.sayAndCreateMissingParamError("list_files", "workspace", "No workspace folder is open"), + ) + return + } + const isOutsideWorkspace = isPathOutsideWorkspace(absolutePath) const sharedMessageProps: ClineSayTool = { diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index d8aca8fbcb..f3aadff67e 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -124,7 +124,6 @@ export class ClineProvider ) { super() - this.log("ClineProvider instantiated") ClineProvider.activeInstances.add(this) this.codeIndexManager = codeIndexManager @@ -163,8 +162,6 @@ export class ClineProvider // The instance is pushed to the top of the stack (LIFO order). // When the task is completed, the top instance is removed, reactivating the previous task. async addClineToStack(cline: Task) { - console.log(`[subtasks] adding task ${cline.taskId}.${cline.instanceId} to stack`) - // Add this cline instance into the stack that represents the order of all the called tasks. this.clineStack.push(cline) @@ -187,8 +184,6 @@ export class ClineProvider let cline = this.clineStack.pop() if (cline) { - console.log(`[subtasks] removing task ${cline.taskId}.${cline.instanceId} from stack`) - try { // Abort the running task and set isAbandoned to true so // all running promises will exit as well. @@ -227,7 +222,6 @@ export class ClineProvider // and resume the previous task/cline instance (if it exists) // this is used when a sub task is finished and the parent task needs to be resumed async finishSubTask(lastMessage: string) { - console.log(`[subtasks] finishing subtask ${lastMessage}`) // remove the last cline instance from the stack (this is the finished sub task) await this.removeClineFromStack() // resume the last cline instance in the stack (if it exists - this is the 'parent' calling task) @@ -255,13 +249,10 @@ export class ClineProvider } async dispose() { - this.log("Disposing ClineProvider...") await this.removeClineFromStack() - this.log("Cleared task") if (this.view && "dispose" in this.view) { this.view.dispose() - this.log("Disposed webview") } this.clearWebviewResources() @@ -280,7 +271,6 @@ export class ClineProvider this.mcpHub = undefined this.marketplaceManager?.cleanup() this.customModesManager?.dispose() - this.log("Disposed all disposables") ClineProvider.activeInstances.delete(this) McpServerManager.unregisterProvider(this) @@ -383,8 +373,6 @@ export class ClineProvider } async resolveWebviewView(webviewView: vscode.WebviewView | vscode.WebviewPanel) { - this.log("Resolving webview view") - this.view = webviewView // Set panel reference according to webview type @@ -485,10 +473,8 @@ export class ClineProvider webviewView.onDidDispose( async () => { if (inTabMode) { - this.log("Disposing ClineProvider instance for tab view") await this.dispose() } else { - this.log("Clearing webview resources for sidebar view") this.clearWebviewResources() this.codeIndexStatusSubscription?.dispose() this.codeIndexStatusSubscription = undefined @@ -509,8 +495,6 @@ export class ClineProvider // If the extension is starting a new session, clear previous task state. await this.removeClineFromStack() - - this.log("Webview view resolved") } public async initClineWithSubTask(parent: Task, task?: string, images?: string[]) { @@ -565,10 +549,6 @@ export class ClineProvider await this.addClineToStack(cline) - this.log( - `[subtasks] ${cline.parentTask ? "child" : "parent"} task ${cline.taskId}.${cline.instanceId} instantiated`, - ) - return cline } @@ -598,9 +578,6 @@ export class ClineProvider }) await this.addClineToStack(cline) - this.log( - `[subtasks] ${cline.parentTask ? "child" : "parent"} task ${cline.taskId}.${cline.instanceId} instantiated`, - ) return cline } @@ -618,11 +595,6 @@ export class ClineProvider if (fs.existsSync(portFilePath)) { localPort = fs.readFileSync(portFilePath, "utf8").trim() - console.log(`[ClineProvider:Vite] Using Vite server port from ${portFilePath}: ${localPort}`) - } else { - console.log( - `[ClineProvider:Vite] Port file not found at ${portFilePath}, using default port: ${localPort}`, - ) } } catch (err) { console.error("[ClineProvider:Vite] Failed to read Vite port file:", err) @@ -965,8 +937,6 @@ export class ClineProvider return } - console.log(`[subtasks] cancelling task ${cline.taskId}.${cline.instanceId}`) - const { historyItem } = await this.getTaskWithId(cline.taskId) // Preserve parent and root task information for history item. const rootTask = cline.rootTask @@ -1218,7 +1188,6 @@ export class ClineProvider // delete the entire task directory including checkpoints and all content try { await fs.rm(taskDirPath, { recursive: true, force: true }) - console.log(`[deleteTaskWithId${id}] removed task directory`) } catch (error) { console.error( `[deleteTaskWithId${id}] failed to remove task directory: ${error instanceof Error ? error.message : String(error)}`, diff --git a/src/extension.ts b/src/extension.ts index 8349b71730..43dbcc08ad 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -98,14 +98,17 @@ export async function activate(context: vscode.ExtensionContext) { } const contextProxy = await ContextProxy.getInstance(context) + const codeIndexManager = CodeIndexManager.getInstance(context) - try { - await codeIndexManager?.initialize(contextProxy) - } catch (error) { - outputChannel.appendLine( - `[CodeIndexManager] Error during background CodeIndexManager configuration/indexing: ${error.message || error}`, - ) + if (codeIndexManager) { + try { + await codeIndexManager.initialize(contextProxy) + } catch (error) { + outputChannel.appendLine( + `[CodeIndexManager] Error during background CodeIndexManager configuration/indexing: ${error.message || error}`, + ) + } } const provider = new ClineProvider(context, outputChannel, "sidebar", contextProxy, codeIndexManager, mdmService) diff --git a/src/integrations/workspace/WorkspaceTracker.ts b/src/integrations/workspace/WorkspaceTracker.ts index 7de8f994c0..ffd373b0e0 100644 --- a/src/integrations/workspace/WorkspaceTracker.ts +++ b/src/integrations/workspace/WorkspaceTracker.ts @@ -26,7 +26,7 @@ class WorkspaceTracker { async initializeFilePaths() { // should not auto get filepaths for desktop since it would immediately show permission popup before cline ever creates a file - if (!this.cwd) { + if (!this.cwd || this.cwd.trim() === "") { return } const tempCwd = this.cwd @@ -39,25 +39,29 @@ class WorkspaceTracker { } private registerListeners() { - const watcher = vscode.workspace.createFileSystemWatcher("**") - this.prevWorkSpacePath = this.cwd - this.disposables.push( - watcher.onDidCreate(async (uri) => { - await this.addFilePath(uri.fsPath) - this.workspaceDidUpdate() - }), - ) - - // Renaming files triggers a delete and create event - this.disposables.push( - watcher.onDidDelete(async (uri) => { - if (await this.removeFilePath(uri.fsPath)) { + // Only create file watcher if we have a valid workspace + if (this.cwd && this.cwd.trim() !== "") { + const watcher = vscode.workspace.createFileSystemWatcher("**") + this.disposables.push( + watcher.onDidCreate(async (uri) => { + await this.addFilePath(uri.fsPath) this.workspaceDidUpdate() - } - }), - ) + }), + ) - this.disposables.push(watcher) + // Renaming files triggers a delete and create event + this.disposables.push( + watcher.onDidDelete(async (uri) => { + if (await this.removeFilePath(uri.fsPath)) { + this.workspaceDidUpdate() + } + }), + ) + + this.disposables.push(watcher) + } + + this.prevWorkSpacePath = this.cwd // Listen for tab changes and call workspaceDidUpdate directly this.disposables.push( @@ -114,7 +118,7 @@ class WorkspaceTracker { clearTimeout(this.updateTimer) } this.updateTimer = setTimeout(() => { - if (!this.cwd) { + if (!this.cwd || this.cwd.trim() === "") { return } diff --git a/src/integrations/workspace/__tests__/WorkspaceTracker.noWorkspace.spec.ts b/src/integrations/workspace/__tests__/WorkspaceTracker.noWorkspace.spec.ts new file mode 100644 index 0000000000..0d9aad8fd6 --- /dev/null +++ b/src/integrations/workspace/__tests__/WorkspaceTracker.noWorkspace.spec.ts @@ -0,0 +1,99 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest" +import * as vscode from "vscode" +import WorkspaceTracker from "../WorkspaceTracker" +import { ClineProvider } from "../../../core/webview/ClineProvider" + +// Mock vscode module +vi.mock("vscode", () => ({ + workspace: { + createFileSystemWatcher: vi.fn(), + fs: { + stat: vi.fn(), + }, + }, + window: { + tabGroups: { + all: [], + onDidChangeTabs: vi.fn(() => ({ dispose: vi.fn() })), + }, + }, + FileType: { + Directory: 2, + }, + Uri: { + file: vi.fn((path) => ({ fsPath: path })), + }, +})) + +// Mock dependencies +vi.mock("../../../services/glob/list-files", () => ({ + listFiles: vi.fn().mockResolvedValue([[], false]), +})) + +vi.mock("../../../utils/path", () => ({ + toRelativePath: vi.fn((path) => path), + getWorkspacePath: vi.fn().mockReturnValue(""), // Empty workspace path +})) + +describe("WorkspaceTracker with no workspace", () => { + let mockProvider: ClineProvider + let workspaceTracker: WorkspaceTracker + + beforeEach(() => { + vi.clearAllMocks() + vi.useFakeTimers() + mockProvider = { + postMessageToWebview: vi.fn(), + } as any + }) + + afterEach(() => { + if (workspaceTracker) { + workspaceTracker.dispose() + } + vi.useRealTimers() + }) + + it("should not create file watcher when workspace is empty", () => { + // Create tracker with empty workspace + workspaceTracker = new WorkspaceTracker(mockProvider) + + // Verify that createFileSystemWatcher was NOT called + expect(vscode.workspace.createFileSystemWatcher).not.toHaveBeenCalled() + }) + + it("should handle initialization without hanging", async () => { + // Create tracker + workspaceTracker = new WorkspaceTracker(mockProvider) + + // Initialize file paths (should return early) + await workspaceTracker.initializeFilePaths() + + // Verify no workspace update was sent + expect(mockProvider.postMessageToWebview).not.toHaveBeenCalled() + }) + + it("should still register tab change listener even without workspace", () => { + // Create tracker + workspaceTracker = new WorkspaceTracker(mockProvider) + + // Verify tab change listener was registered + expect(vscode.window.tabGroups.onDidChangeTabs).toHaveBeenCalled() + }) + + it("should handle workspaceDidUpdate gracefully when workspace is empty", () => { + // Create tracker + workspaceTracker = new WorkspaceTracker(mockProvider) + + // Manually trigger workspaceDidUpdate (simulating internal call) + // This is a private method, so we need to trigger it indirectly + const tabChangeCallback = (vscode.window.tabGroups.onDidChangeTabs as any).mock.calls[0][0] + tabChangeCallback() + + // Wait for debounce + vi.advanceTimersByTime(300) + + // Verify no message was sent (because workspace is empty) + expect(mockProvider.postMessageToWebview).not.toHaveBeenCalled() + }) +}) diff --git a/src/services/code-index/embedders/__tests__/openai-compatible.spec.ts b/src/services/code-index/embedders/__tests__/openai-compatible.spec.ts index 107f3af24d..ed7e9072cd 100644 --- a/src/services/code-index/embedders/__tests__/openai-compatible.spec.ts +++ b/src/services/code-index/embedders/__tests__/openai-compatible.spec.ts @@ -323,7 +323,7 @@ describe("OpenAICompatibleEmbedder", () => { await embedder.createEmbeddings(testTexts) // Should warn about oversized text - expect(console.warn).toHaveBeenCalledWith(expect.stringContaining("exceeds maximum token limit")) + expect(console.warn).toHaveBeenCalledWith(expect.stringContaining("exceeds token limit")) // Should only process normal texts (1 call for 2 normal texts batched together) expect(mockEmbeddingsCreate).toHaveBeenCalledTimes(1) @@ -398,7 +398,7 @@ describe("OpenAICompatibleEmbedder", () => { mockEmbeddingsCreate.mockRejectedValue(authError) await expect(embedder.createEmbeddings(testTexts)).rejects.toThrow( - "Failed to create embeddings: Authentication failed. Please check your API key.", + "Authentication failed. Please check your API key.", ) expect(mockEmbeddingsCreate).toHaveBeenCalledTimes(1) @@ -413,7 +413,7 @@ describe("OpenAICompatibleEmbedder", () => { mockEmbeddingsCreate.mockRejectedValue(serverError) await expect(embedder.createEmbeddings(testTexts)).rejects.toThrow( - "Failed to create embeddings after 3 attempts: HTTP 500 - Internal server error", + "Failed to create embeddings after 3 attempts. Status: 500. Error: Internal server error", ) expect(mockEmbeddingsCreate).toHaveBeenCalledTimes(1) @@ -431,7 +431,7 @@ describe("OpenAICompatibleEmbedder", () => { mockEmbeddingsCreate.mockRejectedValue(apiError) await expect(embedder.createEmbeddings(testTexts)).rejects.toThrow( - "Failed to create embeddings after 3 attempts: API connection failed", + "Failed to create embeddings after 3 attempts. Error: API connection failed", ) expect(console.error).toHaveBeenCalledWith( @@ -447,7 +447,7 @@ describe("OpenAICompatibleEmbedder", () => { mockEmbeddingsCreate.mockRejectedValue(batchError) await expect(embedder.createEmbeddings(testTexts)).rejects.toThrow( - "Failed to create embeddings after 3 attempts: Batch processing failed", + "Failed to create embeddings after 3 attempts. Error: Batch processing failed", ) expect(console.error).toHaveBeenCalledWith( @@ -488,7 +488,7 @@ describe("OpenAICompatibleEmbedder", () => { mockEmbeddingsCreate.mockRejectedValue(authError) await expect(embedder.createEmbeddings(testTexts)).rejects.toThrow( - "Failed to create embeddings: Authentication failed. Please check your API key.", + "Authentication failed. Please check your API key.", ) }) @@ -500,7 +500,7 @@ describe("OpenAICompatibleEmbedder", () => { mockEmbeddingsCreate.mockRejectedValue(httpError) await expect(embedder.createEmbeddings(testTexts)).rejects.toThrow( - "Failed to create embeddings after 3 attempts: HTTP 400 - Bad request", + "Failed to create embeddings after 3 attempts. Status: 400. Error: Bad request", ) }) @@ -511,7 +511,7 @@ describe("OpenAICompatibleEmbedder", () => { mockEmbeddingsCreate.mockRejectedValue(networkError) await expect(embedder.createEmbeddings(testTexts)).rejects.toThrow( - "Failed to create embeddings after 3 attempts: Network timeout", + "Failed to create embeddings after 3 attempts. Error: Network timeout", ) }) @@ -522,7 +522,7 @@ describe("OpenAICompatibleEmbedder", () => { mockEmbeddingsCreate.mockRejectedValue(weirdError) await expect(embedder.createEmbeddings(testTexts)).rejects.toThrow( - "Failed to create embeddings after 3 attempts: Custom error object", + "Failed to create embeddings after 3 attempts. Error: Custom error object", ) }) @@ -533,7 +533,7 @@ describe("OpenAICompatibleEmbedder", () => { mockEmbeddingsCreate.mockRejectedValue(unknownError) await expect(embedder.createEmbeddings(testTexts)).rejects.toThrow( - "Failed to create embeddings after 3 attempts: Unknown error", + "Failed to create embeddings after 3 attempts. Error: Unknown error", ) }) }) diff --git a/src/services/code-index/embedders/__tests__/openai.spec.ts b/src/services/code-index/embedders/__tests__/openai.spec.ts index c93c049844..89ba138207 100644 --- a/src/services/code-index/embedders/__tests__/openai.spec.ts +++ b/src/services/code-index/embedders/__tests__/openai.spec.ts @@ -192,7 +192,7 @@ describe("OpenAiEmbedder", () => { const result = await embedder.createEmbeddings(testTexts) // Verify warning was logged - expect(console.warn).toHaveBeenCalledWith(expect.stringContaining(`exceeds maximum token limit`)) + expect(console.warn).toHaveBeenCalledWith(expect.stringContaining("exceeds token limit")) // Verify only normal texts were processed expect(mockEmbeddingsCreate).toHaveBeenCalledWith({ @@ -300,7 +300,7 @@ describe("OpenAiEmbedder", () => { mockEmbeddingsCreate.mockRejectedValue(authError) await expect(embedder.createEmbeddings(testTexts)).rejects.toThrow( - "Failed to create embeddings: Authentication failed. Please check your OpenAI API key.", + "Authentication failed. Please check your API key.", ) expect(mockEmbeddingsCreate).toHaveBeenCalledTimes(1) @@ -315,7 +315,7 @@ describe("OpenAiEmbedder", () => { mockEmbeddingsCreate.mockRejectedValue(serverError) await expect(embedder.createEmbeddings(testTexts)).rejects.toThrow( - "Failed to create embeddings after 3 attempts: HTTP 500 - Internal server error", + "Failed to create embeddings after 3 attempts. Status: 500. Error: Internal server error", ) expect(mockEmbeddingsCreate).toHaveBeenCalledTimes(1) @@ -333,7 +333,7 @@ describe("OpenAiEmbedder", () => { mockEmbeddingsCreate.mockRejectedValue(apiError) await expect(embedder.createEmbeddings(testTexts)).rejects.toThrow( - "Failed to create embeddings after 3 attempts: API connection failed", + "Failed to create embeddings after 3 attempts. Error: API connection failed", ) expect(console.error).toHaveBeenCalledWith( @@ -374,7 +374,7 @@ describe("OpenAiEmbedder", () => { mockEmbeddingsCreate.mockRejectedValue(authError) await expect(embedder.createEmbeddings(testTexts)).rejects.toThrow( - "Failed to create embeddings: Authentication failed. Please check your OpenAI API key.", + "Authentication failed. Please check your API key.", ) }) @@ -386,7 +386,7 @@ describe("OpenAiEmbedder", () => { mockEmbeddingsCreate.mockRejectedValue(httpError) await expect(embedder.createEmbeddings(testTexts)).rejects.toThrow( - "Failed to create embeddings after 3 attempts: HTTP 400 - Bad request", + "Failed to create embeddings after 3 attempts. Status: 400. Error: Bad request", ) }) @@ -397,7 +397,7 @@ describe("OpenAiEmbedder", () => { mockEmbeddingsCreate.mockRejectedValue(networkError) await expect(embedder.createEmbeddings(testTexts)).rejects.toThrow( - "Failed to create embeddings after 3 attempts: Network timeout", + "Failed to create embeddings after 3 attempts. Error: Network timeout", ) }) @@ -408,7 +408,7 @@ describe("OpenAiEmbedder", () => { mockEmbeddingsCreate.mockRejectedValue(weirdError) await expect(embedder.createEmbeddings(testTexts)).rejects.toThrow( - "Failed to create embeddings after 3 attempts: Custom error object", + "Failed to create embeddings after 3 attempts. Error: Custom error object", ) }) @@ -419,7 +419,7 @@ describe("OpenAiEmbedder", () => { mockEmbeddingsCreate.mockRejectedValue(unknownError) await expect(embedder.createEmbeddings(testTexts)).rejects.toThrow( - "Failed to create embeddings after 3 attempts: Unknown error", + "Failed to create embeddings after 3 attempts. Error: Unknown error", ) }) @@ -430,7 +430,7 @@ describe("OpenAiEmbedder", () => { mockEmbeddingsCreate.mockRejectedValue(stringError) await expect(embedder.createEmbeddings(testTexts)).rejects.toThrow( - "Failed to create embeddings after 3 attempts: Something went wrong", + "Failed to create embeddings after 3 attempts. Error: Something went wrong", ) }) @@ -445,7 +445,7 @@ describe("OpenAiEmbedder", () => { mockEmbeddingsCreate.mockRejectedValue(errorWithFailingToString) await expect(embedder.createEmbeddings(testTexts)).rejects.toThrow( - "Failed to create embeddings after 3 attempts: Unknown error", + "Failed to create embeddings after 3 attempts. Error: Unknown error", ) }) @@ -459,7 +459,7 @@ describe("OpenAiEmbedder", () => { mockEmbeddingsCreate.mockRejectedValue(errorWithResponseStatus) await expect(embedder.createEmbeddings(testTexts)).rejects.toThrow( - "Failed to create embeddings after 3 attempts: HTTP 403 - Request failed", + "Failed to create embeddings after 3 attempts. Status: 403. Error: Request failed", ) }) }) diff --git a/src/services/code-index/manager.ts b/src/services/code-index/manager.ts index b306ce3b60..accc299610 100644 --- a/src/services/code-index/manager.ts +++ b/src/services/code-index/manager.ts @@ -26,7 +26,7 @@ export class CodeIndexManager { public static getInstance(context: vscode.ExtensionContext): CodeIndexManager | undefined { const workspacePath = getWorkspacePath() // Assumes single workspace for now - if (!workspacePath) { + if (!workspacePath || workspacePath.trim() === "") { return undefined } @@ -216,7 +216,14 @@ export class CodeIndexManager { // Create new worker // Use the extension path from context to ensure we get the correct path in both dev and production const workerPath = path.join(this.context.extensionPath, "dist/workers/indexing-worker.js") - this._worker = new Worker(workerPath) + + try { + this._worker = new Worker(workerPath) + } catch (error) { + console.error("[CodeIndexManager] Failed to create worker:", error) + throw error + } + this._workerReady = false // Set up message handling @@ -226,6 +233,7 @@ export class CodeIndexManager { // Initialize the worker const response = await this.sendWorkerCommand({ type: "initialize", config: workerConfig }) + if (response.type === "initialized" && response.success) { this._workerReady = true } else { @@ -266,7 +274,24 @@ export class CodeIndexManager { const message: WorkerMessage = { id, payload: command } return new Promise((resolve, reject) => { - this._pendingMessages.set(id, { resolve, reject }) + // Add a timeout to detect if the worker is not responding + const timeout = setTimeout(() => { + this._pendingMessages.delete(id) + reject(new Error(`Worker timeout for command: ${command.type}`)) + }, 30000) + + // Store the promise handlers with timeout cleanup + this._pendingMessages.set(id, { + resolve: (value) => { + clearTimeout(timeout) + resolve(value) + }, + reject: (error) => { + clearTimeout(timeout) + reject(error) + }, + }) + this._worker!.postMessage(message) }) } diff --git a/src/services/code-index/vector-store/__tests__/qdrant-client.spec.ts b/src/services/code-index/vector-store/__tests__/qdrant-client.spec.ts index 7ed9afb179..798fee4d38 100644 --- a/src/services/code-index/vector-store/__tests__/qdrant-client.spec.ts +++ b/src/services/code-index/vector-store/__tests__/qdrant-client.spec.ts @@ -629,9 +629,7 @@ describe("QdrantVectorStore", () => { vitest.spyOn(console, "error").mockImplementation(() => {}) // Suppress console.error // The actual error message includes the URL and error details - await expect(vectorStore.initialize()).rejects.toThrow( - /Failed to connect to Qdrant vector database|vectorStore\.qdrantConnectionFailed/, - ) + await expect(vectorStore.initialize()).rejects.toThrow(/Failed to connect to Qdrant at/) expect(mockQdrantClientInstance.getCollection).toHaveBeenCalledTimes(1) expect(mockQdrantClientInstance.createCollection).toHaveBeenCalledTimes(1) @@ -692,9 +690,7 @@ describe("QdrantVectorStore", () => { vitest.spyOn(console, "warn").mockImplementation(() => {}) // The actual error message includes the URL and error details - await expect(vectorStore.initialize()).rejects.toThrow( - /Failed to connect to Qdrant vector database|vectorStore\.qdrantConnectionFailed/, - ) + await expect(vectorStore.initialize()).rejects.toThrow(/Failed to connect to Qdrant at/) expect(mockQdrantClientInstance.getCollection).toHaveBeenCalledTimes(1) expect(mockQdrantClientInstance.deleteCollection).toHaveBeenCalledTimes(1) diff --git a/src/services/code-index/vector-store/qdrant-client.ts b/src/services/code-index/vector-store/qdrant-client.ts index 5d77cc39fc..e975435b94 100644 --- a/src/services/code-index/vector-store/qdrant-client.ts +++ b/src/services/code-index/vector-store/qdrant-client.ts @@ -23,6 +23,11 @@ export class QdrantVectorStore implements IVectorStore { * @param url Optional URL to the Qdrant server */ constructor(workspacePath: string, url: string, vectorSize: number, apiKey?: string) { + // Validate workspacePath is not empty + if (!workspacePath || workspacePath.trim() === "") { + throw new Error("Workspace path must not be empty") + } + // Parse the URL to determine the appropriate QdrantClient configuration const parsedUrl = this.parseQdrantUrl(url) diff --git a/src/services/code-index/worker-utils/RooIgnoreController.ts b/src/services/code-index/worker-utils/RooIgnoreController.ts index 6ae5d98874..88b4b479fb 100644 --- a/src/services/code-index/worker-utils/RooIgnoreController.ts +++ b/src/services/code-index/worker-utils/RooIgnoreController.ts @@ -12,6 +12,9 @@ export class RooIgnoreController { private initialized = false constructor(private workspacePath: string) { + if (!workspacePath || workspacePath.trim() === "") { + throw new Error("Workspace path cannot be empty") + } this.rooIgnorePath = path.join(workspacePath, ".rooignore") this.ignoreInstance = ignore() } @@ -48,7 +51,7 @@ export class RooIgnoreController { } const relativePath = path.relative(this.workspacePath, filePath) - return this.ignoreInstance.ignores(relativePath) + return relativePath ? this.ignoreInstance.ignores(relativePath) : false } /** @@ -61,7 +64,7 @@ export class RooIgnoreController { return paths.filter((filePath) => { const relativePath = path.relative(this.workspacePath, filePath) - return !this.ignoreInstance.ignores(relativePath) + return relativePath ? !this.ignoreInstance.ignores(relativePath) : true }) } diff --git a/src/services/code-index/worker-utils/cache-manager.ts b/src/services/code-index/worker-utils/cache-manager.ts index 729d7fe243..1a1e40e810 100644 --- a/src/services/code-index/worker-utils/cache-manager.ts +++ b/src/services/code-index/worker-utils/cache-manager.ts @@ -22,6 +22,11 @@ export class CacheManager implements ICacheManager { private context: { globalStorageUri: { fsPath: string } }, private workspacePath: string, ) { + // Validate workspacePath is not empty + if (!workspacePath || workspacePath.trim() === "") { + throw new Error("Workspace path must not be empty") + } + const cacheFileName = `roo-index-cache-${createHash("sha256").update(workspacePath).digest("hex")}.json` this.cachePath = path.join(context.globalStorageUri.fsPath, cacheFileName) diff --git a/src/services/code-index/worker-utils/file-watcher.ts b/src/services/code-index/worker-utils/file-watcher.ts index 5ac32d13b6..2e66eb26eb 100644 --- a/src/services/code-index/worker-utils/file-watcher.ts +++ b/src/services/code-index/worker-utils/file-watcher.ts @@ -25,6 +25,9 @@ export class FileWatcher { private workspacePath: string, private options: FileWatcherOptions = {}, ) { + if (!workspacePath || workspacePath.trim() === "") { + throw new Error("Workspace path cannot be empty") + } // Initialize ignore instance with exclude patterns this.ignoreInstance = ignore() if (options.excludePatterns) { @@ -45,7 +48,7 @@ export class FileWatcher { cwd: this.workspacePath, ignored: (filePath: string) => { const relativePath = path.relative(this.workspacePath, filePath) - return this.ignoreInstance.ignores(relativePath) + return relativePath ? this.ignoreInstance.ignores(relativePath) : false }, persistent: true, ignoreInitial: true, @@ -112,7 +115,7 @@ export class FileWatcher { const relativePath = path.relative(this.workspacePath, filePath) // Check if ignored - if (this.ignoreInstance.ignores(relativePath)) { + if (relativePath && this.ignoreInstance.ignores(relativePath)) { return false } diff --git a/src/services/code-index/worker-utils/scanner.ts b/src/services/code-index/worker-utils/scanner.ts index 4af8705b73..e14c155016 100644 --- a/src/services/code-index/worker-utils/scanner.ts +++ b/src/services/code-index/worker-utils/scanner.ts @@ -310,7 +310,11 @@ export class Scanner { private workspacePath: string, private excludePatterns: string[] = [], private includePatterns: string[] = [], - ) {} + ) { + if (!workspacePath || workspacePath.trim() === "") { + throw new Error("Workspace path cannot be empty") + } + } /** * Scans the workspace for files to index @@ -336,7 +340,7 @@ export class Scanner { // Double-check exclusion patterns using ignore const relativePath = path.relative(this.workspacePath, file) const ig = ignore().add(allExcludePatterns) - const isExcluded = ig.ignores(relativePath) + const isExcluded = relativePath ? ig.ignores(relativePath) : false if (!isExcluded && !files.includes(file)) { files.push(file) diff --git a/src/services/glob/list-files.ts b/src/services/glob/list-files.ts index 0dc2a06f1e..0eeba2e0be 100644 --- a/src/services/glob/list-files.ts +++ b/src/services/glob/list-files.ts @@ -16,6 +16,11 @@ import { DIRS_TO_IGNORE } from "./constants" * @returns Tuple of [file paths array, whether the limit was reached] */ export async function listFiles(dirPath: string, recursive: boolean, limit: number): Promise<[string[], boolean]> { + // Handle empty directory path + if (!dirPath || dirPath.trim() === "") { + return [[], false] + } + // Handle special directories const specialResult = await handleSpecialDirectories(dirPath) diff --git a/src/services/ripgrep/index.ts b/src/services/ripgrep/index.ts index c694221966..bf566506c2 100644 --- a/src/services/ripgrep/index.ts +++ b/src/services/ripgrep/index.ts @@ -143,6 +143,11 @@ export async function regexSearchFiles( filePattern?: string, rooIgnoreController?: RooIgnoreController, ): Promise { + // Check for empty paths + if (!cwd || cwd.trim() === "" || !directoryPath || directoryPath.trim() === "") { + return "No results found" + } + const vscodeAppRoot = vscode.env.appRoot const rgPath = await getBinPath(vscodeAppRoot) diff --git a/src/workers/__tests__/indexing-worker.spec.ts b/src/workers/__tests__/indexing-worker.spec.ts new file mode 100644 index 0000000000..737e128359 --- /dev/null +++ b/src/workers/__tests__/indexing-worker.spec.ts @@ -0,0 +1,84 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest" +import { Worker } from "worker_threads" +import * as path from "path" +import { v5 as uuidv5 } from "uuid" +import { QDRANT_CODE_BLOCK_NAMESPACE } from "../../services/code-index/constants" + +// Mock worker_threads +vi.mock("worker_threads", () => ({ + Worker: vi.fn(), + parentPort: null, +})) + +describe("IndexingWorker", () => { + let mockWorker: any + + beforeEach(() => { + mockWorker = { + postMessage: vi.fn(), + on: vi.fn(), + terminate: vi.fn(), + } + ;(Worker as any).mockImplementation(() => mockWorker) + }) + + afterEach(() => { + vi.clearAllMocks() + }) + + it("should generate valid UUID v5 point IDs for Qdrant", () => { + // Test data + const testFilePath = "/Users/test/project/src/index.ts" + const testStartLine = 10 + const workspacePath = "/Users/test/project" + + // Simulate what the worker does + const normalizedPath = path.resolve(workspacePath, testFilePath) + const stableName = `${normalizedPath}:${testStartLine}` + const pointId = uuidv5(stableName, QDRANT_CODE_BLOCK_NAMESPACE) + + // Verify the point ID is a valid UUID + expect(pointId).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-5[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i) + + // Verify it's deterministic (same input produces same output) + const pointId2 = uuidv5(stableName, QDRANT_CODE_BLOCK_NAMESPACE) + expect(pointId2).toBe(pointId) + + // Verify different inputs produce different IDs + const differentPath = `${normalizedPath}:${testStartLine + 1}` + const differentId = uuidv5(differentPath, QDRANT_CODE_BLOCK_NAMESPACE) + expect(differentId).not.toBe(pointId) + }) + + it("should handle relative paths correctly", () => { + const workspacePath = "/Users/test/project" + const relativePath = "src/components/Button.tsx" + const startLine = 25 + + // Simulate path normalization + const absolutePath = path.resolve(workspacePath, relativePath) + const normalizedPath = path.normalize(absolutePath) + const stableName = `${normalizedPath}:${startLine}` + const pointId = uuidv5(stableName, QDRANT_CODE_BLOCK_NAMESPACE) + + // Verify it's a valid UUID + expect(pointId).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-5[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i) + }) + + it("should generate consistent IDs across platforms", () => { + const workspacePath = "/Users/test/project" + const filePath = "src/utils/helper.js" + const startLine = 42 + + // Normalize path to handle platform differences + const absolutePath = path.resolve(workspacePath, filePath) + const normalizedPath = path.normalize(absolutePath) + const stableName = `${normalizedPath}:${startLine}` + const pointId = uuidv5(stableName, QDRANT_CODE_BLOCK_NAMESPACE) + + // The ID should be deterministic regardless of platform + expect(pointId).toBeTruthy() + expect(typeof pointId).toBe("string") + expect(pointId.length).toBe(36) // Standard UUID length + }) +}) diff --git a/src/workers/indexing-worker.ts b/src/workers/indexing-worker.ts index d771870d2f..2b31e4d46a 100644 --- a/src/workers/indexing-worker.ts +++ b/src/workers/indexing-worker.ts @@ -2,13 +2,14 @@ import { parentPort } from "worker_threads" import { WorkerCommand, WorkerResponse, WorkerMessage, WorkerInitConfig } from "../services/code-index/worker-messenger" import { CodeIndexStateManager } from "../services/code-index/worker-utils/state-manager" import { CacheManager } from "../services/code-index/worker-utils/cache-manager" -import { Scanner } from "../services/code-index/worker-utils/scanner" +import { Scanner as DirectoryScanner } from "../services/code-index/worker-utils/scanner" import { FileWatcher } from "../services/code-index/worker-utils/file-watcher" import { RooIgnoreController } from "../services/code-index/worker-utils/RooIgnoreController" import { VectorStoreSearchResult } from "../services/code-index/interfaces" import ignore from "ignore" import * as fs from "fs/promises" import * as path from "path" +import { v5 as uuidv5 } from "uuid" // Import embedders and vector store directly import { OpenAiEmbedder } from "../services/code-index/embedders/openai" @@ -17,12 +18,17 @@ import { OpenAICompatibleEmbedder } from "../services/code-index/embedders/opena import { QdrantVectorStore } from "../services/code-index/vector-store/qdrant-client" import { codeParser } from "../services/code-index/worker-utils/parser" import { EmbedderProvider, getDefaultModelId, getModelDimension } from "../shared/embeddingModels" +import { QDRANT_CODE_BLOCK_NAMESPACE } from "../services/code-index/constants" +import { + generateNormalizedAbsolutePath, + generateRelativeFilePath, +} from "../services/code-index/worker-utils/get-relative-path" class IndexingWorker { private config: WorkerInitConfig | null = null private stateManager: CodeIndexStateManager | null = null private cacheManager: CacheManager | null = null - private scanner: Scanner | null = null + private scanner: DirectoryScanner | null = null private fileWatcher: FileWatcher | null = null private embedder: any = null private vectorStore: any = null @@ -74,6 +80,7 @@ class IndexingWorker { }) } } catch (error) { + console.error("[IndexingWorker] Error handling message:", error) this.sendResponse(id, { type: "error", error: error instanceof Error ? error.message : String(error), @@ -84,67 +91,82 @@ class IndexingWorker { private async initialize(config: WorkerInitConfig) { this.config = config - // Initialize state manager - this.stateManager = new CodeIndexStateManager() - - // Set up state change listeners to forward to main thread - this.stateManager.onProgressUpdate((status) => { - // Send progress updates based on the current unit - if (status.currentItemUnit === "files") { - this.sendResponse("progress", { - type: "progress", - processedInBatch: status.processedItems, - totalInBatch: status.totalItems, - currentFile: status.message.includes("Current:") ? status.message.split("Current: ")[1] : undefined, - }) - } else if (status.currentItemUnit === "blocks") { - this.sendResponse("blockProgress", { - type: "blockProgress", - blocksIndexed: status.processedItems, - totalBlocks: status.totalItems, - }) + try { + // Validate required configuration + if (!config.workspacePath || config.workspacePath.trim() === "") { + throw new Error("Workspace path is required for indexing") } - // Always send status updates - this.sendResponse("status", { - type: "status", - state: status.systemStatus, - message: status.message, + // Initialize state manager + this.stateManager = new CodeIndexStateManager() + + // Set up state change listeners to forward to main thread + this.stateManager.onProgressUpdate((status) => { + // Send progress updates based on the current unit + if (status.currentItemUnit === "files") { + this.sendResponse("progress", { + type: "progress", + processedInBatch: status.processedItems, + totalInBatch: status.totalItems, + currentFile: status.message.includes("Current:") + ? status.message.split("Current: ")[1] + : undefined, + }) + } else if (status.currentItemUnit === "blocks") { + this.sendResponse("blockProgress", { + type: "blockProgress", + blocksIndexed: status.processedItems, + totalBlocks: status.totalItems, + }) + } + + // Always send status updates + this.sendResponse("status", { + type: "status", + state: status.systemStatus, + message: status.message, + }) }) - }) - // Initialize cache manager - this.cacheManager = new CacheManager({ globalStorageUri: { fsPath: config.contextPath } }, config.workspacePath) - await this.cacheManager.initialize() + // Initialize cache manager + this.cacheManager = new CacheManager( + { globalStorageUri: { fsPath: config.contextPath } }, + config.workspacePath, + ) + await this.cacheManager.initialize() - // Initialize embedder based on config - this.embedder = this.createEmbedder(config) + // Initialize embedder based on config + this.embedder = this.createEmbedder(config) - // Initialize vector store - this.vectorStore = this.createVectorStore(config) + // Initialize vector store + this.vectorStore = this.createVectorStore(config) - // Load .gitignore - this.ignoreInstance = ignore() - const ignorePath = path.join(config.workspacePath, ".gitignore") - try { - const content = await fs.readFile(ignorePath, "utf8") - this.ignoreInstance.add(content) - this.ignoreInstance.add(".gitignore") + // Load .gitignore + this.ignoreInstance = ignore() + const ignorePath = path.join(config.workspacePath, ".gitignore") + try { + const content = await fs.readFile(ignorePath, "utf8") + this.ignoreInstance.add(content) + this.ignoreInstance.add(".gitignore") + } catch (error) { + // Ignore error if .gitignore doesn't exist + } + + // Initialize RooIgnoreController + this.rooIgnoreController = new RooIgnoreController(config.workspacePath) + await this.rooIgnoreController.initialize() + + // Initialize scanner + this.scanner = new DirectoryScanner(config.workspacePath) + + // Initialize file watcher + this.fileWatcher = new FileWatcher(config.workspacePath, { + excludePatterns: ["**/node_modules/**", "**/.git/**", "**/dist/**", "**/build/**", "**/out/**"], + }) } catch (error) { - console.error("Failed to load .gitignore:", error) + console.error("[IndexingWorker] Initialization error:", error) + throw error } - - // Initialize RooIgnoreController - this.rooIgnoreController = new RooIgnoreController(config.workspacePath) - await this.rooIgnoreController.initialize() - - // Initialize scanner - this.scanner = new Scanner(config.workspacePath) - - // Initialize file watcher - this.fileWatcher = new FileWatcher(config.workspacePath, { - excludePatterns: ["**/node_modules/**", "**/.git/**", "**/dist/**", "**/build/**", "**/out/**"], - }) } private createEmbedder(config: WorkerInitConfig): any { @@ -282,16 +304,28 @@ class IndexingWorker { const { embeddings } = await this.embedder.createEmbeddings(texts) // Prepare points for vector store - const points = blocks.map((block, index) => ({ - id: `${block.file_path}:${block.start_line}`, - vector: embeddings[index], - payload: { - filePath: block.file_path, - codeChunk: block.content, - startLine: block.start_line, - endLine: block.end_line, - }, - })) + const points = blocks.map((block, index) => { + const normalizedAbsolutePath = generateNormalizedAbsolutePath( + block.file_path, + this.config!.workspacePath, + ) + const stableName = `${normalizedAbsolutePath}:${block.start_line}` + const pointId = uuidv5(stableName, QDRANT_CODE_BLOCK_NAMESPACE) + + return { + id: pointId, + vector: embeddings[index], + payload: { + filePath: generateRelativeFilePath( + normalizedAbsolutePath, + this.config!.workspacePath, + ), + codeChunk: block.content, + startLine: block.start_line, + endLine: block.end_line, + }, + } + }) await this.vectorStore.upsertPoints(points) totalBlocksIndexed += blocks.length @@ -304,8 +338,6 @@ class IndexingWorker { // Start file watcher if (this.fileWatcher) { this.fileWatcher.onFileChange(async (event) => { - // Handle file changes - console.log(`File ${event.type}: ${event.path}`) // TODO: Implement file change handling }) this.fileWatcher.start() @@ -375,4 +407,9 @@ class IndexingWorker { } // Start the worker -new IndexingWorker() +try { + new IndexingWorker() +} catch (error) { + console.error("[IndexingWorker] Failed to create IndexingWorker:", error) + process.exit(1) +}