Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 29 additions & 3 deletions src/__tests__/extension.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,9 +139,10 @@ vi.mock("../services/mcp/McpServerManager", () => ({
},
}))

vi.mock("../services/code-index/manager", () => ({
CodeIndexManager: {
getInstance: vi.fn().mockReturnValue(null),
vi.mock("../services/code-index/manager-registry", () => ({
CodeIndexManagerRegistry: {
getInstance: vi.fn().mockReturnValue(undefined),
disposeAll: vi.fn(),
},
}))

Expand Down Expand Up @@ -459,6 +460,31 @@ describe("extension.ts", () => {
vi.resetModules()
})

test("disposes the code index registry on deactivation", async () => {
const { CodeIndexManagerRegistry } = await import("../services/code-index/manager-registry")
const { activate, deactivate } = await import("../extension")
await activate(mockContext)
await deactivate()
expect(CodeIndexManagerRegistry.disposeAll).toHaveBeenCalledTimes(1)
})

test("continues cleanup when disposing the code index registry fails", async () => {
const vscode = await import("vscode")
const { CodeIndexManagerRegistry } = await import("../services/code-index/manager-registry")
const { TerminalRegistry } = await import("../integrations/terminal/TerminalRegistry")
const { activate, deactivate } = await import("../extension")
await activate(mockContext)
vi.mocked(CodeIndexManagerRegistry.disposeAll).mockImplementationOnce(() => {
throw new Error("index cleanup failed")
})
await expect(deactivate()).resolves.toBeUndefined()
const channel = vi.mocked(vscode.window.createOutputChannel).mock.results.at(-1)?.value
expect(channel?.appendLine).toHaveBeenCalledWith(
"Failed to dispose code index managers: index cleanup failed",
)
expect(TerminalRegistry.cleanup).toHaveBeenCalledTimes(1)
})

test("still runs terminal cleanup when telemetry shutdown rejects", async () => {
const { TelemetryService } = await import("@roo-code/telemetry")
const { Terminal } = await import("../integrations/terminal/Terminal")
Expand Down
4 changes: 2 additions & 2 deletions src/activate/__tests__/registerCommands.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,8 @@ vi.mock("../../core/config/importExport", () => ({
importSettingsWithFeedback: vi.fn(),
}))

vi.mock("../../services/code-index/manager", () => ({
CodeIndexManager: {
vi.mock("../../services/code-index/manager-registry", () => ({
CodeIndexManagerRegistry: {
getInstance: vi.fn(),
},
}))
Expand Down
4 changes: 2 additions & 2 deletions src/activate/registerCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { ClineProvider } from "../core/webview/ClineProvider"
import { ContextProxy } from "../core/config/ContextProxy"
import { focusPanel } from "../utils/focusPanel"
import { handleNewTask } from "./handleTask"
import { CodeIndexManager } from "../services/code-index/manager"
import { CodeIndexManagerRegistry } from "../services/code-index/manager-registry"
import { importSettingsWithFeedback } from "../core/config/importExport"
import { MdmService } from "../services/mdm/MdmService"
import { registerRipgrepDiagnosticCommand } from "../services/ripgrep/diagnostic"
Expand Down Expand Up @@ -227,7 +227,7 @@ export const openClineInNewTab = async ({ context, outputChannel }: Omit<Registe
// don't need to use that event).
// https://github.com/microsoft/vscode-extension-samples/blob/main/webview-sample/src/extension.ts
const contextProxy = await ContextProxy.getInstance(context)
const codeIndexManager = CodeIndexManager.getInstance(context)
const codeIndexManager = CodeIndexManagerRegistry.getInstance(context)

// Get the existing MDM service instance to ensure consistent policy enforcement
let mdmService: MdmService | undefined
Expand Down
4 changes: 2 additions & 2 deletions src/core/prompts/system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { formatLanguage } from "../../shared/language"
import { isEmpty } from "../../utils/object"

import { McpHub } from "../../services/mcp/McpHub"
import { CodeIndexManager } from "../../services/code-index/manager"
import { CodeIndexManagerRegistry } from "../../services/code-index/manager-registry"
import { SkillsManager } from "../../services/skills/SkillsManager"

import type { SystemPromptSettings } from "./types"
Expand Down Expand Up @@ -79,7 +79,7 @@ async function generatePrompt(
}
const shouldIncludeMcp = hasMcpGroup && hasMcpServers

const codeIndexManager = CodeIndexManager.getInstance(context, cwd)
const codeIndexManager = CodeIndexManagerRegistry.getInstance(context, cwd)

// Tool calling is native-only.
const effectiveProtocol = "native"
Expand Down
4 changes: 3 additions & 1 deletion src/core/task/__tests__/Task.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,8 @@ vi.mock("p-wait-for", () => ({
default: vi.fn().mockImplementation(async () => Promise.resolve()),
}))

vi.mock("vscode", () => {
vi.mock("vscode", async () => {
const { makeUri } = await import("../../../test-utils/vscode")
const mockDisposable = { dispose: vi.fn() }
const mockEventEmitter = { event: vi.fn(), fire: vi.fn() }
const mockTextDocument = { uri: { fsPath: "/mock/workspace/path/file.ts" } }
Expand All @@ -139,6 +140,7 @@ vi.mock("vscode", () => {
const mockTabGroup = { tabs: [mockTab] }

return {
Uri: { file: vi.fn((filePath: string) => makeUri(filePath)) },
TabInputTextDiff: vi.fn(),
CodeActionKind: {
QuickFix: { value: "quickfix" },
Expand Down
4 changes: 2 additions & 2 deletions src/core/task/build-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,8 @@ export async function buildNativeToolsArrayWithRestrictions(options: BuildToolsO
const mcpHub = provider.getMcpHub()

// Get CodeIndexManager for feature checking.
const { CodeIndexManager } = await import("../../services/code-index/manager")
const codeIndexManager = CodeIndexManager.getInstance(provider.context, cwd)
const { CodeIndexManagerRegistry } = await import("../../services/code-index/manager-registry")
const codeIndexManager = CodeIndexManagerRegistry.getInstance(provider.context, cwd)

// Build settings object for tool filtering.
const filterSettings = {
Expand Down
4 changes: 2 additions & 2 deletions src/core/tools/CodebaseSearchTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as vscode from "vscode"
import path from "path"

import { Task } from "../task/Task"
import { CodeIndexManager } from "../../services/code-index/manager"
import { CodeIndexManagerRegistry } from "../../services/code-index/manager-registry"
import { getWorkspacePath } from "../../utils/path"
import { formatResponse } from "../prompts/responses"
import { VectorStoreSearchResult } from "../../services/code-index/interfaces"
Expand Down Expand Up @@ -57,7 +57,7 @@ export class CodebaseSearchTool extends BaseTool<"codebase_search"> {
throw new Error("Extension context is not available.")
}

const manager = CodeIndexManager.getInstance(context)
const manager = CodeIndexManagerRegistry.getInstance(context)

if (!manager) {
throw new Error("CodeIndexManager is not available.")
Expand Down
5 changes: 3 additions & 2 deletions src/core/webview/ClineProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,8 @@ import { McpHub } from "../../services/mcp/McpHub"
import { McpServerManager } from "../../services/mcp/McpServerManager"
import { MarketplaceManager } from "../../services/marketplace"
import { ShadowCheckpointService } from "../../services/checkpoints/ShadowCheckpointService"
import { CodeIndexManager } from "../../services/code-index/manager"
import { CodeIndexManagerRegistry } from "../../services/code-index/manager-registry"
import type { CodeIndexManager } from "../../services/code-index/manager"
import type { IndexProgressUpdate } from "../../services/code-index/interfaces/manager"
import { MdmService } from "../../services/mdm/MdmService"
import { SkillsManager } from "../../services/skills/SkillsManager"
Expand Down Expand Up @@ -3289,7 +3290,7 @@ export class ClineProvider
* @returns CodeIndexManager instance for the current workspace or the default one
*/
public getCurrentWorkspaceCodeIndexManager(): CodeIndexManager | undefined {
return CodeIndexManager.getInstance(this.context)
return CodeIndexManagerRegistry.getInstance(this.context)
}

/**
Expand Down
6 changes: 3 additions & 3 deletions src/core/webview/__tests__/ClineProvider.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3204,7 +3204,7 @@ describe("webviewMessageHandler no-floating-promises coverage", () => {
})

it("catches auto-enabled indexing failures and posts the resulting status", async () => {
const { CodeIndexManager } = await import("../../../services/code-index/manager")
const { CodeIndexManagerRegistry } = await import("../../../services/code-index/manager-registry")
let workspaceEnabled = false
const manager = createIndexManager({
setAutoEnableDefault: vi.fn().mockImplementation(async () => {
Expand All @@ -3214,8 +3214,8 @@ describe("webviewMessageHandler no-floating-promises coverage", () => {
})
Object.defineProperty(manager, "isWorkspaceEnabled", { get: () => workspaceEnabled })
const getAllInstances = vi
.spyOn(CodeIndexManager, "getAllInstances")
.mockReturnValue([manager] as unknown as ReturnType<typeof CodeIndexManager.getAllInstances>)
.spyOn(CodeIndexManagerRegistry, "getAllInstances")
.mockReturnValue([manager] as unknown as ReturnType<typeof CodeIndexManagerRegistry.getAllInstances>)
const provider = createProvider({
getCurrentWorkspaceCodeIndexManager: vi.fn().mockReturnValue(manager),
})
Expand Down
4 changes: 2 additions & 2 deletions src/core/webview/webviewMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ import { Package } from "../../shared/package"
import { type RouterName, toRouterName } from "../../shared/api"
import { MessageEnhancer } from "./messageEnhancer"

import { CodeIndexManager } from "../../services/code-index/manager"
import { CodeIndexManagerRegistry } from "../../services/code-index/manager-registry"
import { checkExistKey } from "../../shared/checkExistApiConfig"
import { getRouterRemovalMessage, getRouterUnavailableSignInMessage } from "../config/routerRemoval"
import { experimentDefault } from "../../shared/experiments"
Expand Down Expand Up @@ -3311,7 +3311,7 @@ export const webviewMessageHandler = async (
return
}
// Capture prior state for every manager before persisting the global change
const allManagers = CodeIndexManager.getAllInstances()
const allManagers = CodeIndexManagerRegistry.getAllInstances()
const priorStates = new Map(allManagers.map((m) => [m, m.isWorkspaceEnabled]))
await manager.setAutoEnableDefault(message.bool ?? true)
// Apply stop/start to every affected manager
Expand Down
2 changes: 1 addition & 1 deletion src/eslint-suppressions.json
Original file line number Diff line number Diff line change
Expand Up @@ -1301,7 +1301,7 @@
},
"services/code-index/__tests__/manager.spec.ts": {
"@typescript-eslint/no-explicit-any": {
"count": 89
"count": 81
}
},
"services/code-index/__tests__/orchestrator.spec.ts": {
Expand Down
15 changes: 11 additions & 4 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ import { TerminalRegistry } from "./integrations/terminal/TerminalRegistry"
import { openAiCodexOAuthManager } from "./integrations/openai-codex/oauth"
import { kimiCodeOAuthManager } from "./integrations/kimi-code/oauth"
import { McpServerManager } from "./services/mcp/McpServerManager"
import { CodeIndexManager } from "./services/code-index/manager"
import { CodeIndexManagerRegistry } from "./services/code-index/manager-registry"
import type { CodeIndexManager } from "./services/code-index/manager"
import { MdmService } from "./services/mdm/MdmService"
import { migrateSettings } from "./utils/migrateSettings"
import { autoImportSettings } from "./utils/autoImportSettings"
Expand Down Expand Up @@ -200,7 +201,7 @@ export async function activate(context: vscode.ExtensionContext) {

if (vscode.workspace.workspaceFolders) {
for (const folder of vscode.workspace.workspaceFolders) {
const manager = CodeIndexManager.getInstance(context, folder.uri.fsPath)
const manager = CodeIndexManagerRegistry.getInstance(context, folder.uri.fsPath)
Comment thread
WebMad marked this conversation as resolved.

if (manager) {
codeIndexManagers.push(manager)
Expand All @@ -212,8 +213,6 @@ export async function activate(context: vscode.ExtensionContext) {
`[CodeIndexManager] Error during background CodeIndexManager configuration/indexing for ${folder.uri.fsPath}: ${message}`,
)
})

context.subscriptions.push(manager)
}
}
}
Expand Down Expand Up @@ -384,6 +383,14 @@ export async function activate(context: vscode.ExtensionContext) {
export async function deactivate() {
outputChannel.appendLine(`${Package.name} extension deactivated`)

try {
CodeIndexManagerRegistry.disposeAll()
} catch (error) {
outputChannel.appendLine(
`Failed to dispose code index managers: ${error instanceof Error ? error.message : String(error)}`,
Comment thread
WebMad marked this conversation as resolved.
)
}

if (cloudService && CloudService.hasInstance()) {
try {
if (settingsUpdatedHandler) {
Expand Down
147 changes: 147 additions & 0 deletions src/services/code-index/__tests__/manager-registry.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import * as vscode from "vscode"
import { makeExtensionContext, makeTextEditor, makeUri } from "../../../test-utils/vscode"
import { CodeIndexManager } from "../manager"
import { CodeIndexManagerRegistry } from "../manager-registry"

vi.mock("vscode", () => ({
window: { activeTextEditor: undefined },
workspace: { workspaceFolders: undefined, getWorkspaceFolder: vi.fn() },
Uri: { file: vi.fn() },
}))

vi.mock("../manager", () => ({
CodeIndexManager: vi.fn().mockImplementation(function () {
return { dispose: vi.fn() }
}),
}))

describe("CodeIndexManagerRegistry", () => {
let context: vscode.ExtensionContext
const first: vscode.WorkspaceFolder = { uri: makeUri("/first"), name: "first", index: 0 }
const second: vscode.WorkspaceFolder = {
uri: makeUri("/second", { scheme: "vscode-remote", authority: "ssh-remote+host" }),
name: "second",
index: 1,
}

beforeEach(() => {
vi.clearAllMocks()
context = makeExtensionContext()
vi.mocked(vscode.Uri.file).mockImplementation((value) => makeUri(value))
Object.defineProperty(vscode.window, "activeTextEditor", { value: undefined, configurable: true })
Object.defineProperty(vscode.workspace, "workspaceFolders", {
value: [first, second],
configurable: true,
})
vi.mocked(vscode.workspace.getWorkspaceFolder).mockReturnValue(undefined)
})

afterEach(() => CodeIndexManagerRegistry.disposeAll())

it("returns no manager without a workspace or explicit path", () => {
Object.defineProperty(vscode.workspace, "workspaceFolders", { value: undefined })
expect(CodeIndexManagerRegistry.getInstance(context)).toBeUndefined()
expect(CodeIndexManager).not.toHaveBeenCalled()
})

it("defaults to the first workspace and reuses its manager", () => {
const manager = CodeIndexManagerRegistry.getInstance(context)
expect(CodeIndexManagerRegistry.getInstance(context, first.uri.fsPath)).toBe(manager)
expect(CodeIndexManager).toHaveBeenCalledExactlyOnceWith(first.uri.fsPath, first.uri, context)
})

it("uses the active editor workspace and preserves its remote URI", () => {
const editor = makeTextEditor()
Object.defineProperty(vscode.window, "activeTextEditor", { value: editor })
vi.mocked(vscode.workspace.getWorkspaceFolder).mockReturnValue(second)
CodeIndexManagerRegistry.getInstance(context)
expect(vscode.workspace.getWorkspaceFolder).toHaveBeenCalledWith(editor.document.uri)
expect(CodeIndexManager).toHaveBeenCalledWith(second.uri.fsPath, second.uri, context)
})

it("falls back to the first workspace when the active editor is outside it", () => {
Object.defineProperty(vscode.window, "activeTextEditor", { value: makeTextEditor() })
CodeIndexManagerRegistry.getInstance(context)
expect(CodeIndexManager).toHaveBeenCalledWith(first.uri.fsPath, first.uri, context)
})

it("prefers an explicit workspace over the active editor", () => {
Object.defineProperty(vscode.window, "activeTextEditor", { value: makeTextEditor() })
vi.mocked(vscode.workspace.getWorkspaceFolder).mockReturnValue(first)
CodeIndexManagerRegistry.getInstance(context, second.uri.fsPath)
expect(CodeIndexManager).toHaveBeenCalledWith(second.uri.fsPath, second.uri, context)
expect(vscode.workspace.getWorkspaceFolder).not.toHaveBeenCalled()
})

it("creates a file URI for an explicit path outside workspace folders", () => {
Object.defineProperty(vscode.workspace, "workspaceFolders", { value: undefined })
const uri = makeUri("/outside")
vi.mocked(vscode.Uri.file).mockReturnValue(uri)
CodeIndexManagerRegistry.getInstance(context, "/outside")
expect(vscode.Uri.file).toHaveBeenCalledWith("/outside")
expect(CodeIndexManager).toHaveBeenCalledWith("/outside", uri, context)
})

it("creates distinct managers for different workspaces", () => {
const a = CodeIndexManagerRegistry.getInstance(context, first.uri.fsPath)!
const b = CodeIndexManagerRegistry.getInstance(context, second.uri.fsPath)!
expect(a).not.toBe(b)
})

it("lists all registered managers", () => {
const a = CodeIndexManagerRegistry.getInstance(context, first.uri.fsPath)!
const b = CodeIndexManagerRegistry.getInstance(context, second.uri.fsPath)!
expect(CodeIndexManagerRegistry.getAllInstances()).toEqual([a, b])
})

it("disposes every registered manager", () => {
const a = CodeIndexManagerRegistry.getInstance(context, first.uri.fsPath)!
const b = CodeIndexManagerRegistry.getInstance(context, second.uri.fsPath)!
CodeIndexManagerRegistry.disposeAll()
expect(a.dispose).toHaveBeenCalledTimes(1)
expect(b.dispose).toHaveBeenCalledTimes(1)
})

it("removes all managers from the registry on disposal", () => {
CodeIndexManagerRegistry.getInstance(context, first.uri.fsPath)
CodeIndexManagerRegistry.getInstance(context, second.uri.fsPath)
CodeIndexManagerRegistry.disposeAll()
expect(CodeIndexManagerRegistry.getAllInstances()).toEqual([])
})

it("does not dispose managers again when cleanup is repeated", () => {
const manager = CodeIndexManagerRegistry.getInstance(context, first.uri.fsPath)!
CodeIndexManagerRegistry.disposeAll()
CodeIndexManagerRegistry.disposeAll()
expect(manager.dispose).toHaveBeenCalledTimes(1)
})

it("creates a new manager for the same workspace after disposal", () => {
const manager = CodeIndexManagerRegistry.getInstance(context, first.uri.fsPath)!
CodeIndexManagerRegistry.disposeAll()
expect(CodeIndexManagerRegistry.getInstance(context, first.uri.fsPath)).not.toBe(manager)
})

it("attempts every disposal and rethrows the first error", () => {
const a = CodeIndexManagerRegistry.getInstance(context, first.uri.fsPath)!
const b = CodeIndexManagerRegistry.getInstance(context, second.uri.fsPath)!
const firstError = new Error("first cleanup failed")
vi.mocked(a.dispose).mockImplementation(() => {
throw firstError
})
vi.mocked(b.dispose).mockImplementation(() => {
throw new Error("second cleanup failed")
})
expect(() => CodeIndexManagerRegistry.disposeAll()).toThrow(firstError)
expect(b.dispose).toHaveBeenCalledTimes(1)
expect(CodeIndexManagerRegistry.getAllInstances()).toEqual([])
})

it("clears the registry before disposal callbacks run", () => {
const manager = CodeIndexManagerRegistry.getInstance(context, first.uri.fsPath)!
vi.mocked(manager.dispose).mockImplementation(() => {
expect(CodeIndexManagerRegistry.getAllInstances()).toEqual([])
})
CodeIndexManagerRegistry.disposeAll()
})
})
Loading
Loading