diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 13121531a7..57ca4999a2 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -37,9 +37,7 @@ import { formatLanguage } from "../../shared/language" import { Terminal } from "../../integrations/terminal/Terminal" import { downloadTask } from "../../integrations/misc/export-markdown" import { getTheme } from "../../integrations/theme/getTheme" -import WorkspaceTracker from "../../integrations/workspace/WorkspaceTracker" import { McpHub } from "../../services/mcp/McpHub" -import { McpServerManager } from "../../services/mcp/McpServerManager" import { ShadowCheckpointService } from "../../services/checkpoints/ShadowCheckpointService" import { CodeIndexManager } from "../../services/code-index/manager" import type { IndexProgressUpdate } from "../../services/code-index/interfaces/manager" @@ -47,8 +45,8 @@ import { fileExistsAtPath } from "../../utils/fs" import { setTtsEnabled, setTtsSpeed } from "../../utils/tts" import { ContextProxy } from "../config/ContextProxy" import { ProviderSettingsManager } from "../config/ProviderSettingsManager" -import { CustomModesManager } from "../config/CustomModesManager" import { buildApiHandler } from "../../api" +import { ComponentManager } from "./ComponentManager" import { Task, TaskOptions } from "../task/Task" import { getNonce } from "./getNonce" import { getUri } from "./getUri" @@ -82,17 +80,24 @@ export class ClineProvider private view?: vscode.WebviewView | vscode.WebviewPanel private clineStack: Task[] = [] private codeIndexStatusSubscription?: vscode.Disposable - private _workspaceTracker?: WorkspaceTracker // workSpaceTracker read-only for access outside this class - public get workspaceTracker(): WorkspaceTracker | undefined { - return this._workspaceTracker + private componentManager: ComponentManager + + public get workspaceTracker() { + return this.componentManager.workspaceTracker + } + + private get mcpHub() { + return this.componentManager.mcpHub + } + + public get customModesManager() { + return this.componentManager.customModesManager } - protected mcpHub?: McpHub // Change from private to protected public isViewLaunched = false public settingsImportedAt?: number public readonly latestAnnouncementId = "may-21-2025-3-18" // Update for v3.18.0 announcement public readonly providerSettingsManager: ProviderSettingsManager - public readonly customModesManager: CustomModesManager constructor( readonly context: vscode.ExtensionContext, @@ -116,23 +121,10 @@ export class ClineProvider // properties like mode and provider. telemetryService.setProvider(this) - this._workspaceTracker = new WorkspaceTracker(this) - this.providerSettingsManager = new ProviderSettingsManager(this.context) - this.customModesManager = new CustomModesManager(this.context, async () => { - await this.postStateToWebview() - }) - - // Initialize MCP Hub through the singleton manager - McpServerManager.getInstance(this.context, this) - .then((hub) => { - this.mcpHub = hub - this.mcpHub.registerClient() - }) - .catch((error) => { - this.log(`Failed to initialize MCP Hub: ${error}`) - }) + // Initialize ComponentManager to handle workspaceTracker, mcpHub, and customModesManager + this.componentManager = new ComponentManager(this.context, this) } // Adds a new Cline instance to clineStack, marking the start of a new task. @@ -217,8 +209,6 @@ 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() @@ -233,16 +223,17 @@ export class ClineProvider } } - this._workspaceTracker?.dispose() - this._workspaceTracker = undefined - await this.mcpHub?.unregisterClient() - this.mcpHub = undefined - this.customModesManager?.dispose() + await this.componentManager.dispose() + + await this.removeClineFromStack() + this.log("Cleared task") + this.log("Disposed all disposables") - ClineProvider.activeInstances.delete(this) - // Unregister from McpServerManager - McpServerManager.unregisterProvider(this) + if (this.componentManager.isDisposed) { + // As resolveWebviewView is not executed during dispose, the instance can be safely removed. + ClineProvider.activeInstances.delete(this) + } } public static getVisibleInstance(): ClineProvider | undefined { @@ -336,6 +327,11 @@ export class ClineProvider async resolveWebviewView(webviewView: vscode.WebviewView | vscode.WebviewPanel) { this.log("Resolving webview view") + ClineProvider.activeInstances.add(this) + if (this.componentManager.isDisposed) { + this.componentManager = new ComponentManager(this.context, this) + } + this.view = webviewView // Set panel reference according to webview type diff --git a/src/core/webview/ComponentManager.ts b/src/core/webview/ComponentManager.ts new file mode 100644 index 0000000000..d892399c8a --- /dev/null +++ b/src/core/webview/ComponentManager.ts @@ -0,0 +1,106 @@ +import * as vscode from "vscode" +import WorkspaceTracker from "../../integrations/workspace/WorkspaceTracker" +import { McpHub } from "../../services/mcp/McpHub" +import { McpServerManager } from "../../services/mcp/McpServerManager" +import { CustomModesManager } from "../config/CustomModesManager" +import { ClineProvider } from "./ClineProvider" + +/** + * Manages the lifecycle and access to core components used by ClineProvider: + * - WorkspaceTracker: Tracks workspace file changes and open tabs + * - McpHub: Manages MCP server connections and communication + * - CustomModesManager: Handles custom mode configurations + */ +export class ComponentManager { + private _workspaceTracker?: WorkspaceTracker + private _mcpHub?: McpHub + private _customModesManager: CustomModesManager + private _isDisposed = false + + constructor( + private readonly context: vscode.ExtensionContext, + private readonly provider: ClineProvider, + ) { + // Initialize WorkspaceTracker + this._workspaceTracker = new WorkspaceTracker(this.provider) + + // Initialize CustomModesManager (always available) + this._customModesManager = new CustomModesManager(this.context, async () => { + await this.provider.postStateToWebview() + }) + + // Initialize MCP Hub through the singleton manager + this.initializeMcpHub() + } + + /** + * Initializes the MCP Hub + */ + private initializeMcpHub(): void { + McpServerManager.getInstance(this.context, this.provider) + .then((hub) => { + this._mcpHub = hub + this._mcpHub.registerClient() + }) + .catch((error) => { + console.error(`Failed to initialize MCP Hub: ${error}`) + }) + } + + /** + * Gets the WorkspaceTracker instance + */ + get workspaceTracker(): WorkspaceTracker | undefined { + return this._workspaceTracker + } + + /** + * Gets the McpHub instance + */ + get mcpHub(): McpHub | undefined { + return this._mcpHub + } + + /** + * Gets the CustomModesManager instance + */ + get customModesManager(): CustomModesManager { + return this._customModesManager + } + + /** + * Checks if the ComponentManager is disposed + */ + get isDisposed(): boolean { + return this._isDisposed + } + + /** + * Disposes all managed components and cleans up resources + */ + async dispose(): Promise { + if (this._isDisposed) { + return + } + + this._isDisposed = true + + // Dispose WorkspaceTracker + if (this._workspaceTracker) { + this._workspaceTracker.dispose() + this._workspaceTracker = undefined + } + + // Dispose CustomModesManager + this._customModesManager.dispose() + + // Dispose MCP Hub + if (this._mcpHub) { + await this._mcpHub.unregisterClient() + this._mcpHub = undefined + } + + // Unregister from McpServerManager + McpServerManager.unregisterProvider(this.provider) + } +} diff --git a/src/core/webview/__tests__/ClineProvider.test.ts b/src/core/webview/__tests__/ClineProvider.test.ts index f141dace36..508fea90f1 100644 --- a/src/core/webview/__tests__/ClineProvider.test.ts +++ b/src/core/webview/__tests__/ClineProvider.test.ts @@ -321,7 +321,7 @@ describe("ClineProvider", () => { updateGlobalStateSpy = jest.spyOn(provider.contextProxy, "setValue") // @ts-ignore - Accessing private property for testing. - provider.customModesManager = mockCustomModesManager + provider.componentManager._customModesManager = mockCustomModesManager }) test("constructor initializes correctly", () => { @@ -1592,7 +1592,7 @@ describe("ClineProvider", () => { const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0] // Mock CustomModesManager methods - ;(provider as any).customModesManager = { + ;(provider as any).componentManager._customModesManager = { updateCustomMode: jest.fn().mockResolvedValue(undefined), getCustomModes: jest.fn().mockResolvedValue([ {