Skip to content
Closed
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
62 changes: 29 additions & 33 deletions src/core/webview/ClineProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,18 +37,16 @@ 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"
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"
Expand Down Expand Up @@ -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,
Expand All @@ -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.
Expand Down Expand Up @@ -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()
Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
106 changes: 106 additions & 0 deletions src/core/webview/ComponentManager.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
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)
}
}
4 changes: 2 additions & 2 deletions src/core/webview/__tests__/ClineProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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([
{
Expand Down