Skip to content
Merged
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
10 changes: 3 additions & 7 deletions src/core/webview/ClineProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,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 type { CodeIndexManager } from "../../services/code-index/manager"
import { CodeIndexManagerRegistry } from "../../services/code-index/code-index-manager-registry"
import type { CodeIndexWorkspaceScope } from "../../services/code-index/code-index-workspace-scope"
import { MdmService } from "../../services/mdm/MdmService"
import { SkillsManager } from "../../services/skills/SkillsManager"

Expand Down Expand Up @@ -3219,12 +3219,8 @@ export class ClineProvider
return true
}

/**
* Gets the CodeIndexManager for the current active workspace
* @returns CodeIndexManager instance for the current workspace or the default one
*/
public getCurrentWorkspaceCodeIndexManager(): CodeIndexManager | undefined {
return CodeIndexManagerRegistry.getOrCreate(this.context)
public getCurrentWorkspaceCodeIndexScope(): CodeIndexWorkspaceScope | undefined {
return CodeIndexManagerRegistry.getOrCreateScope(this.context)
}

/**
Expand Down
133 changes: 123 additions & 10 deletions src/core/webview/__tests__/ClineProvider.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { defaultModeSlug } from "../../../shared/modes"
import { experimentDefault } from "../../../shared/experiments"
import { setTtsEnabled } from "../../../utils/tts"
import { ContextProxy } from "../../config/ContextProxy"
import { WorkspaceIndexingEnablementManager } from "../../../services/code-index/workspace-indexing-enablement-manager"
import { Task, TaskOptions } from "../../task/Task"
import { safeWriteJson } from "../../../utils/safeWriteJson"

Expand Down Expand Up @@ -2966,7 +2967,7 @@ describe("webviewMessageHandler no-floating-promises coverage", () => {
postMessageToWebview: vi.fn().mockResolvedValue(true),
postStateToWebview: vi.fn().mockResolvedValue(undefined),
getCurrentTask: vi.fn(),
getCurrentWorkspaceCodeIndexManager: vi.fn(),
getCurrentWorkspaceCodeIndexScope: vi.fn(),
getMcpHub: vi.fn().mockReturnValue({
getMcpSettingsFilePath: vi.fn().mockResolvedValue("/test/mcp.json"),
}),
Expand Down Expand Up @@ -3022,10 +3023,11 @@ describe("webviewMessageHandler no-floating-promises coverage", () => {
startIndexing: vi.fn().mockReturnValue(indexingPromise),
})
const provider = createProvider({
getCurrentWorkspaceCodeIndexManager: vi.fn().mockReturnValue(manager),
getCurrentWorkspaceCodeIndexScope: vi.fn().mockReturnValue({ codeIndexManager: manager }),
})

await expect(webviewMessageHandler(provider, { type: "startIndexing" })).resolves.toBeUndefined()
expect(provider.getCurrentWorkspaceCodeIndexScope).toHaveBeenCalledOnce()
expect(manager.startIndexing).toHaveBeenCalledOnce()

rejectIndexing(new Error("boom"))
Expand Down Expand Up @@ -3179,14 +3181,29 @@ describe("webviewMessageHandler no-floating-promises coverage", () => {

it("covers changed indexing status, secret, and missing-manager responses", async () => {
const manager = createIndexManager()
const getManager = vi.fn().mockReturnValueOnce(undefined).mockReturnValue(manager)
const provider = createProvider({ getCurrentWorkspaceCodeIndexManager: getManager })
const getScope = vi.fn().mockReturnValueOnce(undefined).mockReturnValue({ codeIndexManager: manager })
const provider = createProvider({
getCurrentWorkspaceCodeIndexScope: getScope,
})

await webviewMessageHandler(provider, { type: "requestIndexingStatus" })
expect(manager.getCurrentStatus).not.toHaveBeenCalled()
expect(provider.postMessageToWebview).toHaveBeenCalledWith({
type: "indexingStatusUpdate",
values: expect.objectContaining({ systemStatus: "Error", message: expect.any(String) }),
})
await webviewMessageHandler(provider, { type: "requestIndexingStatus" })
expect(getScope).toHaveBeenCalledTimes(2)
expect(manager.getCurrentStatus).toHaveBeenCalledOnce()
expect(provider.postMessageToWebview).toHaveBeenLastCalledWith({
type: "indexingStatusUpdate",
values: manager.getCurrentStatus.mock.results[0].value,
})
await webviewMessageHandler(provider, { type: "requestCodeIndexSecretStatus" })
getManager.mockReturnValueOnce(undefined)
getScope.mockReturnValueOnce(undefined)
await webviewMessageHandler(provider, { type: "startIndexing" })
expect(getScope).toHaveBeenCalledTimes(3)
expect(manager.setWorkspaceEnabled).not.toHaveBeenCalled()

expect(provider.postMessageToWebview).toHaveBeenCalledWith(
expect.objectContaining({ type: "codeIndexSecretStatus" }),
Expand All @@ -3203,7 +3220,7 @@ describe("webviewMessageHandler no-floating-promises coverage", () => {
.mockRejectedValueOnce(new Error("second failure")),
})
const provider = createProvider({
getCurrentWorkspaceCodeIndexManager: vi.fn().mockReturnValue(manager),
getCurrentWorkspaceCodeIndexScope: vi.fn().mockReturnValue({ codeIndexManager: manager }),
})

await webviewMessageHandler(provider, { type: "startIndexing" })
Expand All @@ -3219,10 +3236,18 @@ describe("webviewMessageHandler no-floating-promises coverage", () => {
startIndexing: vi.fn().mockRejectedValue(new Error("toggle failure")),
})
const provider = createProvider({
getCurrentWorkspaceCodeIndexManager: vi.fn().mockReturnValue(manager),
getCurrentWorkspaceCodeIndexScope: vi.fn().mockReturnValue({
codeIndexManager: manager,
workspaceIndexingEnablementManager: new WorkspaceIndexingEnablementManager(manager),
}),
})

await webviewMessageHandler(provider, { type: "stopIndexing" })
expect(provider.getCurrentWorkspaceCodeIndexScope).toHaveBeenCalledOnce()
expect(provider.postMessageToWebview).toHaveBeenCalledExactlyOnceWith({
type: "indexingStatusUpdate",
values: manager.getCurrentStatus(),
})
await webviewMessageHandler(provider, { type: "toggleWorkspaceIndexing", bool: true })
await Promise.resolve()

Expand All @@ -3233,6 +3258,85 @@ describe("webviewMessageHandler no-floating-promises coverage", () => {
)
})

it("does not toggle indexing or publish status without a workspace scope", async () => {
const provider = createProvider()
await webviewMessageHandler(provider, { type: "toggleWorkspaceIndexing", bool: true })
expect(provider.getCurrentWorkspaceCodeIndexScope).toHaveBeenCalledOnce()
expect(provider.log).toHaveBeenCalledExactlyOnceWith(
"Cannot toggle workspace indexing: No workspace folder open",
)
expect(provider.postMessageToWebview).not.toHaveBeenCalled()
})

it.each([
{ bool: true, expected: true },
{ bool: false, expected: false },
{ bool: undefined, expected: false },
])("delegates workspace enablement with bool=$bool as $expected", async ({ bool, expected }) => {
const setEnabled = vi.fn().mockResolvedValue(undefined)
const provider = createProvider({
getCurrentWorkspaceCodeIndexScope: vi.fn().mockReturnValue({
workspaceIndexingEnablementManager: { setEnabled },
}),
})
const message: WebviewMessage = { type: "toggleWorkspaceIndexing" }
if (bool !== undefined) {
message.bool = bool
}
await webviewMessageHandler(provider, message)
expect(setEnabled).toHaveBeenCalledExactlyOnceWith(expected, provider)
// Status publication belongs to the enablement manager, not the handler.
expect(provider.postMessageToWebview).not.toHaveBeenCalled()
expect(provider.log).not.toHaveBeenCalled()
})

it("does not stop indexing or publish status without a workspace scope", async () => {
const provider = createProvider()
await webviewMessageHandler(provider, { type: "stopIndexing" })
expect(provider.getCurrentWorkspaceCodeIndexScope).toHaveBeenCalledOnce()
expect(provider.log).toHaveBeenCalledWith("Cannot stop indexing: No workspace folder open")
expect(provider.postMessageToWebview).not.toHaveBeenCalled()
})

it.each([true, false])("resolves the scope after saving index settings (workspace=%s)", async (hasWorkspace) => {
const handleSettingsChange = vi.fn().mockResolvedValue(undefined)
const manager = createIndexManager({ handleSettingsChange, isFeatureEnabled: false })
const provider = createProvider({
getCurrentWorkspaceCodeIndexScope: vi
.fn()
.mockReturnValue(hasWorkspace ? { codeIndexManager: manager } : undefined),
})

await webviewMessageHandler(provider, {
type: "saveCodeIndexSettingsAtomic",
codeIndexSettings: {
codebaseIndexEnabled: false,
codebaseIndexQdrantUrl: "http://localhost:6333",
codebaseIndexEmbedderProvider: providerIdentifiers.openai,
codebaseIndexEmbedderModelId: "text-embedding-3-small",
},
})

expect(provider.getCurrentWorkspaceCodeIndexScope).toHaveBeenCalledOnce()
expect(provider.postMessageToWebview).toHaveBeenCalledWith(
expect.objectContaining({ type: "codeIndexSettingsSaved", success: true }),
)
if (hasWorkspace) {
expect(handleSettingsChange).toHaveBeenCalledOnce()
} else {
expect(handleSettingsChange).not.toHaveBeenCalled()
expect(provider.log).toHaveBeenCalledWith("Cannot save code index settings: No workspace folder open")
}
})

it("does not update auto-enable defaults without a workspace scope", async () => {
const provider = createProvider()
await webviewMessageHandler(provider, { type: "setAutoEnableDefault", bool: true })
expect(provider.getCurrentWorkspaceCodeIndexScope).toHaveBeenCalledOnce()
expect(provider.log).toHaveBeenCalledWith("Cannot set auto-enable default: No workspace folder open")
expect(provider.postMessageToWebview).not.toHaveBeenCalled()
})

it("catches auto-enabled indexing failures and posts the resulting status", async () => {
const { CodeIndexManagerRegistry } = await import("../../../services/code-index/code-index-manager-registry")
let workspaceEnabled = false
Expand All @@ -3247,13 +3351,14 @@ describe("webviewMessageHandler no-floating-promises coverage", () => {
.spyOn(CodeIndexManagerRegistry, "getAllInstances")
.mockReturnValue([manager] as unknown as ReturnType<typeof CodeIndexManagerRegistry.getAllInstances>)
const provider = createProvider({
getCurrentWorkspaceCodeIndexManager: vi.fn().mockReturnValue(manager),
getCurrentWorkspaceCodeIndexScope: vi.fn().mockReturnValue({ codeIndexManager: manager }),
})

try {
await webviewMessageHandler(provider, { type: "setAutoEnableDefault", bool: true })
await Promise.resolve()

expect(provider.getCurrentWorkspaceCodeIndexScope).toHaveBeenCalledOnce()
expect(manager.startIndexing).toHaveBeenCalledOnce()
expect(provider.log).toHaveBeenCalledWith("Indexing error: Error: auto-enable failure")
expect(provider.postMessageToWebview).toHaveBeenCalledWith(
Expand All @@ -3266,14 +3371,22 @@ describe("webviewMessageHandler no-floating-promises coverage", () => {

it("covers changed clear-index response paths", async () => {
const manager = createIndexManager()
const getManager = vi.fn().mockReturnValueOnce(undefined).mockReturnValue(manager)
const provider = createProvider({ getCurrentWorkspaceCodeIndexManager: getManager })
const getScope = vi.fn().mockReturnValueOnce(undefined).mockReturnValue({ codeIndexManager: manager })
const provider = createProvider({ getCurrentWorkspaceCodeIndexScope: getScope })

await webviewMessageHandler(provider, { type: "clearIndexData" })
expect(manager.clearIndexData).not.toHaveBeenCalled()
expect(provider.log).toHaveBeenCalledWith("Cannot clear index data: No workspace folder open")
expect(provider.postMessageToWebview).toHaveBeenCalledExactlyOnceWith({
type: "indexCleared",
values: { success: false, error: expect.any(String) },
})
await webviewMessageHandler(provider, { type: "clearIndexData" })
manager.clearIndexData.mockRejectedValueOnce(new Error("clear failed"))
await webviewMessageHandler(provider, { type: "clearIndexData" })

expect(getScope).toHaveBeenCalledTimes(3)
expect(manager.clearIndexData).toHaveBeenCalledTimes(2)
expect(provider.postMessageToWebview).toHaveBeenCalledWith({
type: "indexCleared",
values: { success: true },
Expand Down
43 changes: 18 additions & 25 deletions src/core/webview/webviewMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3135,7 +3135,8 @@ export const webviewMessageHandler = async (
await provider.postStateToWebview()

// Then handle validation and initialization for the current workspace
const currentCodeIndexManager = provider.getCurrentWorkspaceCodeIndexManager()
const scope = provider.getCurrentWorkspaceCodeIndexScope()
const currentCodeIndexManager = scope?.codeIndexManager
if (currentCodeIndexManager) {
// If embedder provider changed, perform proactive validation
if (embedderProviderChanged) {
Expand Down Expand Up @@ -3214,7 +3215,8 @@ export const webviewMessageHandler = async (
}

case "requestIndexingStatus": {
const manager = provider.getCurrentWorkspaceCodeIndexManager()
const scope = provider.getCurrentWorkspaceCodeIndexScope()
const manager = scope?.codeIndexManager
if (!manager) {
// No workspace open - send error status
await provider.postMessageToWebview({
Expand Down Expand Up @@ -3278,7 +3280,8 @@ export const webviewMessageHandler = async (
}
case "startIndexing": {
try {
const manager = provider.getCurrentWorkspaceCodeIndexManager()
const scope = provider.getCurrentWorkspaceCodeIndexScope()
const manager = scope?.codeIndexManager
if (!manager) {
await provider.postMessageToWebview({
type: "indexingStatusUpdate",
Expand Down Expand Up @@ -3319,15 +3322,15 @@ export const webviewMessageHandler = async (
}
case "stopIndexing": {
try {
const manager = provider.getCurrentWorkspaceCodeIndexManager()
if (!manager) {
const scope = provider.getCurrentWorkspaceCodeIndexScope()
if (!scope) {
provider.log("Cannot stop indexing: No workspace folder open")
return
}
manager.stopIndexing()
scope.codeIndexManager.stopIndexing()
await provider.postMessageToWebview({
type: "indexingStatusUpdate",
values: manager.getCurrentStatus(),
values: scope.codeIndexManager.getCurrentStatus(),
})
} catch (error) {
provider.log(`Error stopping indexing: ${error instanceof Error ? error.message : String(error)}`)
Expand All @@ -3336,23 +3339,12 @@ export const webviewMessageHandler = async (
}
case "toggleWorkspaceIndexing": {
try {
const manager = provider.getCurrentWorkspaceCodeIndexManager()
if (!manager) {
const scope = provider.getCurrentWorkspaceCodeIndexScope()
if (!scope) {
provider.log("Cannot toggle workspace indexing: No workspace folder open")
return
}
const enabled = message.bool ?? false
await manager.setWorkspaceEnabled(enabled)
if (enabled && manager.isFeatureEnabled && manager.isFeatureConfigured) {
await manager.initialize(provider.contextProxy)
void manager.startIndexing().catch((err) => provider.log(`Indexing error: ${err}`))
} else if (!enabled) {
manager.stopIndexing()
}
await provider.postMessageToWebview({
type: "indexingStatusUpdate",
values: manager.getCurrentStatus(),
})
await scope.workspaceIndexingEnablementManager.setEnabled(message.bool ?? false, provider)
} catch (error) {
provider.log(
`Error toggling workspace indexing: ${error instanceof Error ? error.message : String(error)}`,
Expand All @@ -3362,7 +3354,8 @@ export const webviewMessageHandler = async (
}
case "setAutoEnableDefault": {
try {
const manager = provider.getCurrentWorkspaceCodeIndexManager()
const scope = provider.getCurrentWorkspaceCodeIndexScope()
const manager = scope?.codeIndexManager
if (!manager) {
provider.log("Cannot set auto-enable default: No workspace folder open")
return
Expand Down Expand Up @@ -3395,8 +3388,8 @@ export const webviewMessageHandler = async (
}
case "clearIndexData": {
try {
const manager = provider.getCurrentWorkspaceCodeIndexManager()
if (!manager) {
const scope = provider.getCurrentWorkspaceCodeIndexScope()
if (!scope) {
provider.log("Cannot clear index data: No workspace folder open")
await provider.postMessageToWebview({
type: "indexCleared",
Expand All @@ -3407,7 +3400,7 @@ export const webviewMessageHandler = async (
})
return
}
await manager.clearIndexData()
await scope.codeIndexManager.clearIndexData()
await provider.postMessageToWebview({ type: "indexCleared", values: { success: true } })
} catch (error) {
provider.log(`Error clearing index data: ${error instanceof Error ? error.message : String(error)}`)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,21 @@ describe("CodeIndexManagerRegistry", () => {
it.each([{ folders: undefined }, { folders: [] }])("returns no manager with folders=$folders", ({ folders }) => {
Object.defineProperty(vscode.workspace, "workspaceFolders", { configurable: true, value: folders })
expect(CodeIndexManagerRegistry.getOrCreate(context)).toBeUndefined()
expect(CodeIndexManagerRegistry.getOrCreateScope(context)).toBeUndefined()
expect(CodeIndexManager).not.toHaveBeenCalled()
})

it("returns the complete cached scope shared with the manager API", () => {
const scope = CodeIndexManagerRegistry.getOrCreateScope(context)!
expect(scope).toBeInstanceOf(CodeIndexWorkspaceScope)
expect(scope.workspaceIndexingEnablementManager).toBeDefined()
expect(CodeIndexManagerRegistry.getOrCreateScope(context, "/first")).toBe(scope)
expect(CodeIndexManagerRegistry.getOrCreate(context)).toBe(scope.codeIndexManager)
expect(CodeIndexManagerRegistry.getOrCreateScope(context, "/second")).not.toBe(scope)
CodeIndexManagerRegistry.disposeAll()
expect(CodeIndexManagerRegistry.getOrCreateScope(context)).not.toBe(scope)
})

it("uses the first workspace when there is no active editor", () => {
CodeIndexManagerRegistry.getOrCreate(context)
expect(CodeIndexManager).toHaveBeenCalledWith("/first", first.uri, context, expect.any(CodeIndexStateManager))
Expand Down
Loading
Loading