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
20 changes: 16 additions & 4 deletions src/__tests__/extension.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,10 +139,15 @@ vi.mock("../services/mcp/McpServerManager", () => ({
},
}))

vi.mock("../services/code-index/manager", () => ({
CodeIndexManager: {
getInstance: vi.fn().mockReturnValue(null),
},
const codeIndexScope = {
init: vi.fn().mockResolvedValue(undefined),
dispose: vi.fn().mockResolvedValue(undefined),
}

vi.mock("../services/code-index/code-index-scope", () => ({
CodeIndexScope: vi.fn().mockImplementation(function () {
return codeIndexScope
}),
}))

vi.mock("../services/mdm/MdmService", () => ({
Expand Down Expand Up @@ -459,6 +464,13 @@ describe("extension.ts", () => {
vi.resetModules()
})

test("disposes the code index lifecycle service on deactivation", async () => {
const { activate, deactivate } = await import("../extension")
await activate(mockContext)
await deactivate()
expect(codeIndexScope.dispose).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
19 changes: 12 additions & 7 deletions src/activate/__tests__/registerCommands.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Mock } from "vitest"
import * as vscode from "vscode"
import { ClineProvider } from "../../core/webview/ClineProvider"
import type { CodeIndexScope } from "../../services/code-index/code-index-scope"

import { getVisibleProviderOrLog, openClineInNewTab, registerCommands, setPanel } from "../registerCommands"

Expand Down Expand Up @@ -67,12 +68,6 @@ vi.mock("../../core/config/importExport", () => ({
importSettingsWithFeedback: vi.fn(),
}))

vi.mock("../../services/code-index/manager", () => ({
CodeIndexManager: {
getInstance: vi.fn(),
},
}))

vi.mock("../../services/mdm/MdmService", () => ({
MdmService: {
getInstance: vi.fn(),
Expand Down Expand Up @@ -412,7 +407,17 @@ describe("openClineInNewTab", () => {
})

it("creates a webview panel with title 'Zoo Code'", async () => {
await openClineInNewTab({ context: mockContext, outputChannel: mockOutputChannel })
// Only identity matters here: the mocked provider owns the consumer registration.
const codeIndexScope = {} as CodeIndexScope
await openClineInNewTab({ context: mockContext, outputChannel: mockOutputChannel, codeIndexScope })
expect(ClineProvider).toHaveBeenCalledWith(
mockContext,
mockOutputChannel,
"editor",
undefined,
undefined,
codeIndexScope,
)

expect(vscode.window.createWebviewPanel).toHaveBeenCalledWith(
"zoo-code.TabPanelProvider",
Expand Down
17 changes: 11 additions & 6 deletions src/activate/registerCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@
import { ContextProxy } from "../core/config/ContextProxy"
import { focusPanel } from "../utils/focusPanel"
import { handleNewTask } from "./handleTask"
import { CodeIndexManager } from "../services/code-index/manager"
import { importSettingsWithFeedback } from "../core/config/importExport"
import { MdmService } from "../services/mdm/MdmService"
import { registerRipgrepDiagnosticCommand } from "../services/ripgrep/diagnostic"
import { t } from "../i18n"
import type { CodeIndexScope } from "../services/code-index/code-index-scope"

/**
* Helper to get the visible ClineProvider instance or log if not found.
Expand Down Expand Up @@ -60,6 +60,7 @@
context: vscode.ExtensionContext
outputChannel: vscode.OutputChannel
provider: ClineProvider
codeIndexScope?: CodeIndexScope
}

export const registerCommands = (options: RegisterCommandOptions) => {
Expand Down Expand Up @@ -89,6 +90,7 @@
context,
outputChannel,
provider,
codeIndexScope,
}: RegisterCommandOptions): Record<Exclude<CommandId, "showRipgrepDiagnostic">, CommandCallback> => ({
activationCompleted: () => {},
plusButtonClicked: async () => {
Expand All @@ -110,9 +112,9 @@
popoutButtonClicked: () => {
TelemetryService.instance.captureTitleButtonClicked("popout")

return openClineInNewTab({ context, outputChannel })
return openClineInNewTab({ context, outputChannel, codeIndexScope })

Check warning on line 115 in src/activate/registerCommands.ts

View workflow job for this annotation

GitHub Actions / mutation-diff

Mutation test advisory

src/activate/registerCommands.ts:115: NoCoverage ObjectLiteral mutant (replacement: {}). See the job summary for the complete list and resolution guidance.
},
openInNewTab: () => openClineInNewTab({ context, outputChannel }),
openInNewTab: () => openClineInNewTab({ context, outputChannel, codeIndexScope }),

Check warning on line 117 in src/activate/registerCommands.ts

View workflow job for this annotation

GitHub Actions / mutation-diff

Mutation test advisory

src/activate/registerCommands.ts:117: 2 mutation test gaps; example: NoCoverage ObjectLiteral mutant (replacement: {}). See the job summary for the complete list and resolution guidance.
settingsButtonClicked: () => {
const visibleProvider = getVisibleProviderOrLog(outputChannel)

Expand Down Expand Up @@ -221,13 +223,16 @@
},
})

export const openClineInNewTab = async ({ context, outputChannel }: Omit<RegisterCommandOptions, "provider">) => {
export const openClineInNewTab = async ({
context,
outputChannel,
codeIndexScope,
}: Omit<RegisterCommandOptions, "provider">) => {
// (This example uses webviewProvider activation event which is necessary to
// deserialize cached webview, but since we use retainContextWhenHidden, we
// 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)

// Get the existing MDM service instance to ensure consistent policy enforcement
let mdmService: MdmService | undefined
Expand All @@ -238,7 +243,7 @@
mdmService = undefined
}

const tabProvider = new ClineProvider(context, outputChannel, "editor", contextProxy, mdmService)
const tabProvider = new ClineProvider(context, outputChannel, "editor", contextProxy, mdmService, codeIndexScope)
const lastCol = Math.max(...vscode.window.visibleTextEditors.map((editor) => editor.viewColumn || 0))

// Check if there are any visible text editors, otherwise open a new group
Expand Down
3 changes: 0 additions & 3 deletions src/core/prompts/system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { formatLanguage } from "../../shared/language"
import { isEmpty } from "../../utils/object"

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

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

const codeIndexManager = CodeIndexManager.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
6 changes: 4 additions & 2 deletions src/core/task/build-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,10 @@
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 codeIndexManager = provider.codeIndexScope?.workspaceRegistry.getScope(

Check warning on line 99 in src/core/task/build-tools.ts

View workflow job for this annotation

GitHub Actions / mutation-diff

Mutation test advisory

src/core/task/build-tools.ts:99: Survived OptionalChaining mutant (replacement: provider.codeIndexScope?.workspaceRegistry.getScope(provider.context, cwd).codeIndexManager). See the job summary for the complete list and resolution guidance.
provider.context,
cwd,
)?.codeIndexManager

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

import { Task } from "../task/Task"
import { CodeIndexManager } from "../../services/code-index/manager"
import { getWorkspacePath } from "../../utils/path"
import { formatResponse } from "../prompts/responses"
import { VectorStoreSearchResult } from "../../services/code-index/interfaces"
Expand Down Expand Up @@ -57,7 +56,9 @@
throw new Error("Extension context is not available.")
}

const manager = CodeIndexManager.getInstance(context)
const manager = task.providerRef

Check warning on line 59 in src/core/tools/CodebaseSearchTool.ts

View workflow job for this annotation

GitHub Actions / mutation-diff

Mutation test advisory

src/core/tools/CodebaseSearchTool.ts:59: 3 mutation test gaps; example: NoCoverage OptionalChaining mutant (replacement: task.providerRef.deref()?.codeIndexScope?.workspaceRegistry.getScope(context).codeIndexManager). See the job summary for the complete list and resolution guidance.
.deref()
?.codeIndexScope?.workspaceRegistry.getScope(context)?.codeIndexManager

if (!manager) {
throw new Error("CodeIndexManager is not available.")
Expand Down
85 changes: 19 additions & 66 deletions src/core/webview/ClineProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,9 @@
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 type { IndexProgressUpdate } from "../../services/code-index/interfaces/manager"
import type { CodeIndexWorkspaceScope } from "../../services/code-index/code-index-workspace-scope"
import type { CodeIndexScope } from "../../services/code-index/code-index-scope"
import type { CodeIndexStatus, CodeIndexStatusConsumer } from "../../services/code-index/interfaces/status-consumer"
import { MdmService } from "../../services/mdm/MdmService"
import { SkillsManager } from "../../services/skills/SkillsManager"

Expand Down Expand Up @@ -175,7 +176,7 @@

export class ClineProvider
extends EventEmitter<TaskProviderEvents>
implements vscode.WebviewViewProvider, TelemetryPropertiesProvider, TaskProviderLike
implements vscode.WebviewViewProvider, TelemetryPropertiesProvider, TaskProviderLike, CodeIndexStatusConsumer
{
// Used in package.json as the view's id. This value cannot be changed due
// to how VSCode caches views based on their id, and updating the id would
Expand All @@ -199,8 +200,8 @@
private taskScheduler = new TaskScheduler()
private delegationTransitionLocks?: Map<string, Promise<void>>
private cancelledDelegationChildIds = new Set<string>()
private codeIndexStatusSubscription?: vscode.Disposable
private codeIndexManager?: CodeIndexManager
private readonly codeIndexWebviewReadyEmitter = new vscode.EventEmitter<void>()
public readonly onDidCodeIndexWebviewReady = this.codeIndexWebviewReadyEmitter.event
private _workspaceTracker?: WorkspaceTracker // workSpaceTracker read-only for access outside this class
protected mcpHub?: McpHub // Change from private to protected
protected skillsManager?: SkillsManager
Expand Down Expand Up @@ -319,13 +320,18 @@
private readonly renderContext: "sidebar" | "editor" = "sidebar",
public readonly contextProxy: ContextProxy,
mdmService?: MdmService,
public readonly codeIndexScope?: CodeIndexScope,
) {
super()
this.currentWorkspacePath = getWorkspacePath()
this.pendingEditOperations = new PendingEditOperationStore(
ClineProvider.PENDING_OPERATION_TIMEOUT_MS,
(message) => this.log(message),
)
this.disposables.push(this.codeIndexWebviewReadyEmitter)
if (codeIndexScope) {
this.disposables.push(codeIndexScope.statusManager.addConsumer(this))
}

ClineProvider.activeInstances.add(this)

Expand All @@ -333,7 +339,7 @@
void this.updateGlobalState("codebaseIndexModels", EMBEDDING_MODEL_PROFILES)

// Initialize the per-task file-based history store.
// The globalState write-through is debounced separately (not on every mutation)

Check warning on line 342 in src/core/webview/ClineProvider.ts

View workflow job for this annotation

GitHub Actions / mutation-diff

Mutation test advisory

src/core/webview/ClineProvider.ts:342: Survived CallExpression mutant (replacement: ;). See the job summary for the complete list and resolution guidance.
// since per-task files are authoritative and globalState is only for downgrade compat.
this.taskHistoryStore = new TaskHistoryStore(this.contextProxy.globalStorageUri.fsPath, {
onWrite: async () => {
Expand Down Expand Up @@ -1070,17 +1076,6 @@
// and executes code based on the message that is received.
this.setWebviewMessageListener(webviewView.webview)

// Initialize code index status subscription for the current workspace.
this.updateCodeIndexStatusSubscription()

// Listen for active editor changes to update code index status for the
// current workspace.
const activeEditorSubscription = vscode.window.onDidChangeActiveTextEditor(() => {
// Update subscription when workspace might have changed.
this.updateCodeIndexStatusSubscription()
})
this.webviewDisposables.push(activeEditorSubscription)

// Listen for when the panel becomes visible.
// https://github.com/microsoft/vscode-discussions/discussions/840
if ("onDidChangeViewState" in webviewView) {
Expand Down Expand Up @@ -1118,8 +1113,6 @@
} else {
this.log("Clearing webview resources for sidebar view")
this.clearWebviewResources()
// Reset current workspace manager reference when view is disposed
this.codeIndexManager = undefined
}
},
null,
Expand Down Expand Up @@ -3285,58 +3278,18 @@
}

/**
* Gets the CodeIndexManager for the current active workspace
* @returns CodeIndexManager instance for the current workspace or the default one
* Gets the workspace scope for the current active workspace or the default one.
*/
public getCurrentWorkspaceCodeIndexManager(): CodeIndexManager | undefined {
return CodeIndexManager.getInstance(this.context)
public getCurrentWorkspaceCodeIndexScope(): CodeIndexWorkspaceScope | undefined {
return this.codeIndexScope?.workspaceRegistry.getScope(this.context)
}

/**
* Updates the code index status subscription to listen to the current workspace manager
*/
private updateCodeIndexStatusSubscription(): void {
// Get the current workspace manager
const currentManager = this.getCurrentWorkspaceCodeIndexManager()

// If the manager hasn't changed, no need to update subscription
if (currentManager === this.codeIndexManager) {
return
}

// Dispose the old subscription if it exists
if (this.codeIndexStatusSubscription) {
this.codeIndexStatusSubscription.dispose()
this.codeIndexStatusSubscription = undefined
}

// Update the current workspace manager reference
this.codeIndexManager = currentManager

// Subscribe to the new manager's progress updates if it exists
if (currentManager) {
this.codeIndexStatusSubscription = currentManager.onProgressUpdate((update: IndexProgressUpdate) => {
// Only send updates if this manager is still the current one
if (currentManager === this.getCurrentWorkspaceCodeIndexManager()) {
// Get the full status from the manager to ensure we have all fields correctly formatted
const fullStatus = currentManager.getCurrentStatus()
void this.postMessageToWebview({
type: "indexingStatusUpdate",
values: fullStatus,
})
}
})

if (this.view) {
this.webviewDisposables.push(this.codeIndexStatusSubscription)
}
public notifyCodeIndexWebviewReady(): void {
this.codeIndexWebviewReadyEmitter.fire()
}

// Send initial status for the current workspace
void this.postMessageToWebview({
type: "indexingStatusUpdate",
values: currentManager.getCurrentStatus(),
})
}
public async postCodeIndexStatus(status: CodeIndexStatus): Promise<void> {
await this.postMessageToWebview({ type: "indexingStatusUpdate", values: status })
}

/**
Expand All @@ -3346,7 +3299,7 @@
public getCurrentTask(): Task | undefined {
return this.taskRegistry.current
}

Check warning on line 3302 in src/core/webview/ClineProvider.ts

View workflow job for this annotation

GitHub Actions / mutation-diff

Mutation test advisory

src/core/webview/ClineProvider.ts:3302: NoCoverage OptionalChaining mutant (replacement: this.codeIndexScope.workspaceRegistry). See the job summary for the complete list and resolution guidance.
private logWebviewHiddenDiagnostics(): void {
const task = this.getCurrentTask()
if (!task || task.abort || task.abandoned) {
Expand All @@ -3354,7 +3307,7 @@
}
this.log(
`[Zoo Code] Webview hidden during active task.\n` +
` taskId: ${task.taskId}\n` +

Check warning on line 3310 in src/core/webview/ClineProvider.ts

View workflow job for this annotation

GitHub Actions / mutation-diff

Mutation test advisory

src/core/webview/ClineProvider.ts:3310: 2 mutation test gaps; example: NoCoverage ObjectLiteral mutant (replacement: {}). See the job summary for the complete list and resolution guidance.
` messageCount: ${task.clineMessages.length}\n` +
` stackDepth: ${this.taskRegistry.length}\n` +
` timestamp: ${new Date().toISOString()}\n` +
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { makeEventEmitter } from "../../../test-utils/vscode"
// npx vitest core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts

import * as vscode from "vscode"
Expand Down Expand Up @@ -40,6 +41,9 @@ vi.mock("delay", () => {
})

vi.mock("vscode", () => ({
EventEmitter: vi.fn().mockImplementation(function () {
return makeEventEmitter()
}),
ExtensionContext: vi.fn(),
OutputChannel: vi.fn(),
WebviewView: vi.fn(),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { makeEventEmitter } from "../../../test-utils/vscode"
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"
import * as vscode from "vscode"

Expand Down Expand Up @@ -48,10 +49,7 @@ vi.mock("vscode", () => {
language: "en",
},
EventEmitter: vi.fn().mockImplementation(function () {
return {
event: vi.fn(),
fire: vi.fn(),
}
return makeEventEmitter()
}),
Disposable: {
from: vi.fn(),
Expand Down
Loading
Loading