Skip to content

Conversation

@diarmidmackenzie
Copy link
Contributor

@diarmidmackenzie diarmidmackenzie commented Apr 1, 2025

Context

PR #2149 split ClineProvider into two files.

This required making certain private properties of the ClineProvider class public.

For this PR, I have carefully reviewed all of those, and tidied up everything I can to make the public interface of ClineProvider as clean as possible.

Implementation

Approach:

  1. accept that the property should be public
  2. make the property public, but read-only
  3. adjust the code in webviewMessageHandler to use an alternative public method (in some cases, an appropriate public method didn't exist, but it was sensibe to create it).

For specific details, see line-by-line comments in the code review.

Screenshots

N/A

How to Test

As with previous refactoring, given that the TS compiles cleanly, risk seems low.

No obvious targeted testing that would be valuable. The changes are wide-ranging, so a lot of code paths will be altered, but all in pretty minimal ways that should not impact function.

Get in Touch

@diarmidm on Discord, or the Cline.ts modularization Discord channel.


Important

Refactor ClineProvider to improve encapsulation and state management, adjusting public interfaces and utilizing contextProxy for cleaner code.

  • Encapsulation:
    • Made view, workspaceTracker, and mcpHub private or read-only in ClineProvider.
    • Added getMcpHub() in ClineProvider to access mcpHub.
  • Refactoring:
    • Replaced outputChannel.appendLine with log() in ClineProvider and webviewMessageHandler.
    • Utilized contextProxy for state management in webviewMessageHandler.
  • State Management:
    • Updated ExtensionState in ExtensionMessage.ts to use Partial<ExtensionState>.
    • Modified mergeExtensionState in ExtensionStateContext.tsx to handle partial state updates.

This description was created by Ellipsis for 783138c. It will automatically update as commits are pushed.

Increase type flexibility so we can directly use postMessageToWebview()
expose new public writeDataToCache() method to commonize code.
also decided to leave updateApiConfiguration as public, since widely used in diverse ways.
Symmetric naming of functions that complement each other.
Stop using deprecated (private) method.  Use contextProxy set/getValue methods instead.
Utility functions provided for brevity.
log is already public, outputChannel should be private.
But also prefer log internally for the additional value of console logging, plus code brevity.
@changeset-bot
Copy link

changeset-bot bot commented Apr 1, 2025

⚠️ No Changeset found

Latest commit: e506dcf

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

import { ExtensionMessage } from "../../shared/ExtensionMessage"
import { Mode, PromptComponent, defaultModeSlug, getModeBySlug, getGroupName } from "../../shared/modes"
import { EXPERIMENT_IDS, experiments as Experiments, experimentDefault, ExperimentId } from "../../shared/experiments"
import { experimentDefault } from "../../shared/experiments"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Imports ahould have been removed in previous PR, but I overlooked them

// not private, so it can be accessed from webviewMessageHandler
// callers could update to get viewLaunched() getter function
isViewLaunched = false
private view?: vscode.WebviewView | vscode.WebviewPanel
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed use of this by changing code to use proper public API: postMessageToWebview

latestAnnouncementId = "mar-30-2025-3-11" // update for v3.11.0 announcement
// not private, so it can be accessed from webviewMessageHandler
settingsImportedAt?: number
private _workspaceTracker?: WorkspaceTracker // workSpaceTracker read-only for access outside this class
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

workspaceTracker needs to be public to allow access to initializeFilePaths() but we can make it readonly externally.

can't apply the TS readonly property because we need to write to this field in our dispose() method, so this is the next best option in terms of limiting public access.

public get workspaceTracker(): WorkspaceTracker | undefined {
return this._workspaceTracker
}
protected mcpHub?: McpHub // Change from private to protected
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Switched back to "protected" as code was before. I don't understand the value of this being "protected" as I don't see any sub-classes of ClineProvider, but pre-existing comment shows this is intentional.

Could consider switching to same model as workspaceTracker with an internal property _mcpHub and a getter so that external components can access provider.mcpHub rather than provider,getMcpHub().

I think that's a nicer API to offer, but it's orthogonal to this refactor, so I've not made that change.

return this._workspaceTracker
}
protected mcpHub?: McpHub // Change from private to protected

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reordered properties so they are grouped by access privileges.

These ones are accessed one-time in webviewMessageHandler. Simplest solution is to make them public (and readonly for the ones that don't need to be writeable)

readonly context: vscode.ExtensionContext,
// not private, so it can be accessed from webviewMessageHandler
readonly outputChannel: vscode.OutputChannel,
private readonly outputChannel: vscode.OutputChannel,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made this private again.

Prefer provider.log() over provider.outputChannel.appendLine()

super()

this.outputChannel.appendLine("ClineProvider instantiated")
this.log("ClineProvider instantiated")
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made this change throughout this module, as well as in webviewMessageHandler - it's more concise and cleaner.

Minor functional change in that we also log to console, but volume will be minimal, and probably useful to log to console as well as to the outputChannel.

telemetryService.setProvider(this)

this.workspaceTracker = new WorkspaceTracker(this)
this._workspaceTracker = new WorkspaceTracker(this)
Copy link
Contributor Author

@diarmidmackenzie diarmidmackenzie Apr 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above - internal field is now _workspaceTracker.

workspaceTracker is public, but read-only via a getter.

}

// not private, so it can be accessed from webviewMessageHandler
async writeModelsToCache<T>(filename: string, data: T) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New function, avoids need to make ensureCacheDirectoryExists public, and offers a higher level of abstraction to message handlers.

Symmetry with pre-existing readModelsFromCache function.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe in the future we can remove file management from this file.

// @deprecated - Use `ContextProxy#setValue` instead.
// not private, so it can be accessed from webviewMessageHandler
async updateGlobalState<K extends keyof GlobalState>(key: K, value: GlobalState[K]) {
private async updateGlobalState<K extends keyof GlobalState>(key: K, value: GlobalState[K]) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-instate private status here. message handlers use ContextProxy.setValue as per comment.

// @deprecated - Use `ContextProxy#getValue` instead.
// not private, so it can be accessed from webviewMessageHandler
getGlobalState<K extends keyof GlobalState>(key: K) {
private getGlobalState<K extends keyof GlobalState>(key: K) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-instate private status here. message handlers use ContextProxy.getValue as per comment.

import { checkoutDiffPayloadSchema, checkoutRestorePayloadSchema, WebviewMessage } from "../../shared/WebviewMessage"
import { checkExistKey } from "../../shared/checkExistApiConfig"
import { EXPERIMENT_IDS, experiments as Experiments, experimentDefault, ExperimentId } from "../../shared/experiments"
import { EXPERIMENT_IDS, experimentDefault, ExperimentId } from "../../shared/experiments"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Superfluous imports - overlooked in previous PR.

import { TelemetrySetting } from "../../shared/TelemetrySetting"
import { getWorkspacePath } from "../../utils/path"
import { Mode, PromptComponent, defaultModeSlug, getModeBySlug, getGroupName } from "../../shared/modes"
import { Mode, defaultModeSlug, getModeBySlug, getGroupName } from "../../shared/modes"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Superfluous import - overlooked in previous PR

import { GlobalState } from "../../schemas"

export const webviewMessageHandler = async (provider: ClineProvider, message: WebviewMessage) => {
// Utility functions provided for concise get/update of global state via contextProxy API.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

provider.get/updateGlobalState is marked as deprecated in ClineProvider.ts, so don't use it.

getGobalState is more concise than provider.contextProxy.getValue (and used a lot throughout this file, so worthwhile to offer a more concise syntax).

// Load custom modes first
const customModes = await provider.customModesManager.getCustomModes()
await provider.updateGlobalState("customModes", customModes)
await updateGlobalState("customModes", customModes)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Uses new local updateGlobalState utility function, over (deprecated, private) provider.updateGlobalState method. You'll see this change many times in this file.


// If MCP Hub is already initialized, update the webview with current server list
if (provider.mcpHub) {
const mcpHub = provider.getMcpHub()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

provider.mcpHub -> protected again. This is the public method to access it.

})
}

const cacheDir = await provider.ensureCacheDirectoryExists()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is folded into the new higher-level writeModelsToCache() method.

path.join(cacheDir, GlobalFileNames.openRouterModels),
JSON.stringify(openRouterModels),
)
await provider.writeModelsToCache(GlobalFileNames.openRouterModels, openRouterModels)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One of several instances where we have moved to the new higher-level writeModelsToCache() method, alowing provider.ensureCacheDirectoryExists() to become private again.

const currentState = await provider.getState()
const stateWithPrompts = { ...currentState, customModePrompts: updatedPrompts }
provider.view?.webview.postMessage({ type: "state", state: stateWithPrompts })
provider.postMessageToWebview({ type: "state", state: stateWithPrompts })
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use public API, rather than accessing private view property. Had to slightly adjust types on postMessageToWebview to enable this - see later.

That might be why the code had been written this way previously.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change of plan - use provider.getStateToPostToWebview() instead, then no type relaxation needed.

| "didBecomeVisible"
invoke?: "newChat" | "sendMessage" | "primaryButtonClick" | "secondaryButtonClick" | "setChatBoxMessage"
state?: ExtensionState
state?: Partial<ExtensionState>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Relaxed type here, so that we could move away from using private view property.

See: https://github.com/RooVetGit/Roo-Code/pull/2182/files#r2022956143

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change of plan - not relaxing type after all.

@diarmidmackenzie diarmidmackenzie changed the title Tidy cline provider refactor Tidy up following ClineProvider refactor Apr 1, 2025
@diarmidmackenzie diarmidmackenzie marked this pull request as ready for review April 1, 2025 14:46
@dosubot dosubot bot added the size:L This PR changes 100-499 lines, ignoring generated files. label Apr 1, 2025
x.dispose()
}
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wondered 🤔 why there were areas with output channels and this.log
We also need to remove the console.log to this.log in the future

}

// not private, so it can be accessed from webviewMessageHandler
async writeModelsToCache<T>(filename: string, data: T) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe in the future we can remove file management from this file.

@hannesrudolph hannesrudolph moved this from New to PR [Pre Approval Review] in Roo Code Roadmap Apr 1, 2025
monotykamary pushed a commit to monotykamary/Roo-Code that referenced this pull request Apr 4, 2025
@dosubot dosubot bot added the lgtm This PR has been approved by a maintainer label Apr 5, 2025
@mrubens mrubens merged commit 7661ef9 into RooCodeInc:main Apr 5, 2025
12 checks passed
@github-project-automation github-project-automation bot moved this from PR [Pre Approval Review] to Done in Roo Code Roadmap Apr 5, 2025
@bramburn
Copy link
Contributor

bramburn commented Apr 5, 2025 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

lgtm This PR has been approved by a maintainer size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants