refactor: Pull WebviewProvider and Controller out of ClineProvider, rename Cline to Task (#2564)

* refactor: Replace ClineProvider with Controller for improved architecture

- Replaced instances of ClineProvider with Controller in extension.ts and related files to enhance code organization and maintainability.
- Introduced a new Controller class to manage interactions previously handled by ClineProvider, streamlining the extension's functionality.
- Updated command registrations and message handling to utilize the new Controller structure, ensuring consistent behavior across the extension.
- Removed the ClineProvider class and its associated methods, consolidating functionality within the Controller class.
- Added new state management and task handling capabilities within the Controller to support the updated architecture.

* clean up

* refactor: Update Task class to use Controller reference

- Replaced all instances of ClineProvider with Controller in the Task class to align with the recent architectural changes.
- Updated references for context management, task history, and message handling to utilize the new Controller structure.
- Ensured consistent behavior across the Task class by adapting to the Controller's methods and properties.

* refactor: Simplify WebviewProvider listeners structure

* Fixes

* Make controller a dependency of webview

* refactor: Improve message listener in WebviewProvider

- Updated the setWebviewMessageListener method to use an arrow function for the message handler, preserving the 'this' context of the controller.
- Added detailed comments explaining the importance of maintaining the correct 'this' context when passing methods as callbacks in JavaScript/TypeScript.

* Add doc

* Add to chat for visible webview

* Fixes
This commit is contained in:
Saoud Rizwan
2025-03-30 18:48:34 -07:00
committed by GitHub
parent a80022795e
commit 55f7ce9b20
14 changed files with 2743 additions and 2639 deletions
+11
View File
@@ -0,0 +1,11 @@
# Core Architecture
Extension entry point (extension.ts) -> webview -> controller -> task
```tree
core/
├── webview/ # Manages webview lifecycle
├── controller/ # Handles webview messages and task management
├── task/ # Executes API requests and tool operations
└── ... # Additional components to help with context, parsing user/assistant messages, etc.
```
File diff suppressed because it is too large Load Diff
+421
View File
@@ -0,0 +1,421 @@
import * as vscode from "vscode"
import { DEFAULT_CHAT_SETTINGS } from "../../shared/ChatSettings"
import { DEFAULT_BROWSER_SETTINGS } from "../../shared/BrowserSettings"
import { DEFAULT_AUTO_APPROVAL_SETTINGS } from "../../shared/AutoApprovalSettings"
import { GlobalStateKey, SecretKey } from "./keys"
import { ApiConfiguration, ApiProvider, ModelInfo } from "../../shared/api"
import { HistoryItem } from "../../shared/HistoryItem"
import { AutoApprovalSettings } from "../../shared/AutoApprovalSettings"
import { BrowserSettings } from "../../shared/BrowserSettings"
import { ChatSettings } from "../../shared/ChatSettings"
import { TelemetrySetting } from "../../shared/TelemetrySetting"
import { UserInfo } from "../../shared/UserInfo"
/*
Storage
https://dev.to/kompotkot/how-to-use-secretstorage-in-your-vscode-extensions-2hco
https://www.eliostruyf.com/devhack-code-extension-storage-options/
*/
// global
export async function updateGlobalState(context: vscode.ExtensionContext, key: GlobalStateKey, value: any) {
await context.globalState.update(key, value)
}
export async function getGlobalState(context: vscode.ExtensionContext, key: GlobalStateKey) {
return await context.globalState.get(key)
}
// secrets
export async function storeSecret(context: vscode.ExtensionContext, key: SecretKey, value?: string) {
if (value) {
await context.secrets.store(key, value)
} else {
await context.secrets.delete(key)
}
}
export async function getSecret(context: vscode.ExtensionContext, key: SecretKey) {
return await context.secrets.get(key)
}
// workspace
export async function updateWorkspaceState(context: vscode.ExtensionContext, key: string, value: any) {
await context.workspaceState.update(key, value)
}
export async function getWorkspaceState(context: vscode.ExtensionContext, key: string) {
return await context.workspaceState.get(key)
}
export async function getAllExtensionState(context: vscode.ExtensionContext) {
const [
storedApiProvider,
apiModelId,
apiKey,
openRouterApiKey,
clineApiKey,
awsAccessKey,
awsSecretKey,
awsSessionToken,
awsRegion,
awsUseCrossRegionInference,
awsBedrockUsePromptCache,
awsBedrockEndpoint,
awsProfile,
awsUseProfile,
vertexProjectId,
vertexRegion,
openAiBaseUrl,
openAiApiKey,
openAiModelId,
openAiModelInfo,
ollamaModelId,
ollamaBaseUrl,
ollamaApiOptionsCtxNum,
lmStudioModelId,
lmStudioBaseUrl,
anthropicBaseUrl,
geminiApiKey,
openAiNativeApiKey,
deepSeekApiKey,
requestyApiKey,
requestyModelId,
togetherApiKey,
togetherModelId,
qwenApiKey,
mistralApiKey,
azureApiVersion,
openRouterModelId,
openRouterModelInfo,
openRouterProviderSorting,
lastShownAnnouncementId,
customInstructions,
taskHistory,
autoApprovalSettings,
browserSettings,
chatSettings,
vsCodeLmModelSelector,
liteLlmBaseUrl,
liteLlmModelId,
userInfo,
previousModeApiProvider,
previousModeModelId,
previousModeModelInfo,
previousModeVsCodeLmModelSelector,
previousModeThinkingBudgetTokens,
qwenApiLine,
liteLlmApiKey,
telemetrySetting,
asksageApiKey,
asksageApiUrl,
xaiApiKey,
thinkingBudgetTokens,
sambanovaApiKey,
planActSeparateModelsSettingRaw,
] = await Promise.all([
getGlobalState(context, "apiProvider") as Promise<ApiProvider | undefined>,
getGlobalState(context, "apiModelId") as Promise<string | undefined>,
getSecret(context, "apiKey") as Promise<string | undefined>,
getSecret(context, "openRouterApiKey") as Promise<string | undefined>,
getSecret(context, "clineApiKey") as Promise<string | undefined>,
getSecret(context, "awsAccessKey") as Promise<string | undefined>,
getSecret(context, "awsSecretKey") as Promise<string | undefined>,
getSecret(context, "awsSessionToken") as Promise<string | undefined>,
getGlobalState(context, "awsRegion") as Promise<string | undefined>,
getGlobalState(context, "awsUseCrossRegionInference") as Promise<boolean | undefined>,
getGlobalState(context, "awsBedrockUsePromptCache") as Promise<boolean | undefined>,
getGlobalState(context, "awsBedrockEndpoint") as Promise<string | undefined>,
getGlobalState(context, "awsProfile") as Promise<string | undefined>,
getGlobalState(context, "awsUseProfile") as Promise<boolean | undefined>,
getGlobalState(context, "vertexProjectId") as Promise<string | undefined>,
getGlobalState(context, "vertexRegion") as Promise<string | undefined>,
getGlobalState(context, "openAiBaseUrl") as Promise<string | undefined>,
getSecret(context, "openAiApiKey") as Promise<string | undefined>,
getGlobalState(context, "openAiModelId") as Promise<string | undefined>,
getGlobalState(context, "openAiModelInfo") as Promise<ModelInfo | undefined>,
getGlobalState(context, "ollamaModelId") as Promise<string | undefined>,
getGlobalState(context, "ollamaBaseUrl") as Promise<string | undefined>,
getGlobalState(context, "ollamaApiOptionsCtxNum") as Promise<string | undefined>,
getGlobalState(context, "lmStudioModelId") as Promise<string | undefined>,
getGlobalState(context, "lmStudioBaseUrl") as Promise<string | undefined>,
getGlobalState(context, "anthropicBaseUrl") as Promise<string | undefined>,
getSecret(context, "geminiApiKey") as Promise<string | undefined>,
getSecret(context, "openAiNativeApiKey") as Promise<string | undefined>,
getSecret(context, "deepSeekApiKey") as Promise<string | undefined>,
getSecret(context, "requestyApiKey") as Promise<string | undefined>,
getGlobalState(context, "requestyModelId") as Promise<string | undefined>,
getSecret(context, "togetherApiKey") as Promise<string | undefined>,
getGlobalState(context, "togetherModelId") as Promise<string | undefined>,
getSecret(context, "qwenApiKey") as Promise<string | undefined>,
getSecret(context, "mistralApiKey") as Promise<string | undefined>,
getGlobalState(context, "azureApiVersion") as Promise<string | undefined>,
getGlobalState(context, "openRouterModelId") as Promise<string | undefined>,
getGlobalState(context, "openRouterModelInfo") as Promise<ModelInfo | undefined>,
getGlobalState(context, "openRouterProviderSorting") as Promise<string | undefined>,
getGlobalState(context, "lastShownAnnouncementId") as Promise<string | undefined>,
getGlobalState(context, "customInstructions") as Promise<string | undefined>,
getGlobalState(context, "taskHistory") as Promise<HistoryItem[] | undefined>,
getGlobalState(context, "autoApprovalSettings") as Promise<AutoApprovalSettings | undefined>,
getGlobalState(context, "browserSettings") as Promise<BrowserSettings | undefined>,
getGlobalState(context, "chatSettings") as Promise<ChatSettings | undefined>,
getGlobalState(context, "vsCodeLmModelSelector") as Promise<vscode.LanguageModelChatSelector | undefined>,
getGlobalState(context, "liteLlmBaseUrl") as Promise<string | undefined>,
getGlobalState(context, "liteLlmModelId") as Promise<string | undefined>,
getGlobalState(context, "userInfo") as Promise<UserInfo | undefined>,
getGlobalState(context, "previousModeApiProvider") as Promise<ApiProvider | undefined>,
getGlobalState(context, "previousModeModelId") as Promise<string | undefined>,
getGlobalState(context, "previousModeModelInfo") as Promise<ModelInfo | undefined>,
getGlobalState(context, "previousModeVsCodeLmModelSelector") as Promise<vscode.LanguageModelChatSelector | undefined>,
getGlobalState(context, "previousModeThinkingBudgetTokens") as Promise<number | undefined>,
getGlobalState(context, "qwenApiLine") as Promise<string | undefined>,
getSecret(context, "liteLlmApiKey") as Promise<string | undefined>,
getGlobalState(context, "telemetrySetting") as Promise<TelemetrySetting | undefined>,
getSecret(context, "asksageApiKey") as Promise<string | undefined>,
getGlobalState(context, "asksageApiUrl") as Promise<string | undefined>,
getSecret(context, "xaiApiKey") as Promise<string | undefined>,
getGlobalState(context, "thinkingBudgetTokens") as Promise<number | undefined>,
getSecret(context, "sambanovaApiKey") as Promise<string | undefined>,
getGlobalState(context, "planActSeparateModelsSetting") as Promise<boolean | undefined>,
])
let apiProvider: ApiProvider
if (storedApiProvider) {
apiProvider = storedApiProvider
} else {
// Either new user or legacy user that doesn't have the apiProvider stored in state
// (If they're using OpenRouter or Bedrock, then apiProvider state will exist)
if (apiKey) {
apiProvider = "anthropic"
} else {
// New users should default to openrouter, since they've opted to use an API key instead of signing in
apiProvider = "openrouter"
}
}
const o3MiniReasoningEffort = vscode.workspace.getConfiguration("cline.modelSettings.o3Mini").get("reasoningEffort", "medium")
const mcpMarketplaceEnabled = vscode.workspace.getConfiguration("cline").get<boolean>("mcpMarketplace.enabled", true)
// Plan/Act separate models setting is a boolean indicating whether the user wants to use different models for plan and act. Existing users expect this to be enabled, while we want new users to opt in to this being disabled by default.
// On win11 state sometimes initializes as empty string instead of undefined
let planActSeparateModelsSetting: boolean | undefined = undefined
if (planActSeparateModelsSettingRaw === true || planActSeparateModelsSettingRaw === false) {
planActSeparateModelsSetting = planActSeparateModelsSettingRaw
} else {
// default to true for existing users
if (storedApiProvider) {
planActSeparateModelsSetting = true
} else {
// default to false for new users
planActSeparateModelsSetting = false
}
// this is a special case where it's a new state, but we want it to default to different values for existing and new users.
// persist so next time state is retrieved it's set to the correct value.
await updateGlobalState(context, "planActSeparateModelsSetting", planActSeparateModelsSetting)
}
return {
apiConfiguration: {
apiProvider,
apiModelId,
apiKey,
openRouterApiKey,
clineApiKey,
awsAccessKey,
awsSecretKey,
awsSessionToken,
awsRegion,
awsUseCrossRegionInference,
awsBedrockUsePromptCache,
awsBedrockEndpoint,
awsProfile,
awsUseProfile,
vertexProjectId,
vertexRegion,
openAiBaseUrl,
openAiApiKey,
openAiModelId,
openAiModelInfo,
ollamaModelId,
ollamaBaseUrl,
ollamaApiOptionsCtxNum,
lmStudioModelId,
lmStudioBaseUrl,
anthropicBaseUrl,
geminiApiKey,
openAiNativeApiKey,
deepSeekApiKey,
requestyApiKey,
requestyModelId,
togetherApiKey,
togetherModelId,
qwenApiKey,
qwenApiLine,
mistralApiKey,
azureApiVersion,
openRouterModelId,
openRouterModelInfo,
openRouterProviderSorting,
vsCodeLmModelSelector,
o3MiniReasoningEffort,
thinkingBudgetTokens,
liteLlmBaseUrl,
liteLlmModelId,
liteLlmApiKey,
asksageApiKey,
asksageApiUrl,
xaiApiKey,
sambanovaApiKey,
},
lastShownAnnouncementId,
customInstructions,
taskHistory,
autoApprovalSettings: autoApprovalSettings || DEFAULT_AUTO_APPROVAL_SETTINGS, // default value can be 0 or empty string
browserSettings: browserSettings || DEFAULT_BROWSER_SETTINGS,
chatSettings: chatSettings || DEFAULT_CHAT_SETTINGS,
userInfo,
previousModeApiProvider,
previousModeModelId,
previousModeModelInfo,
previousModeVsCodeLmModelSelector,
previousModeThinkingBudgetTokens,
mcpMarketplaceEnabled,
telemetrySetting: telemetrySetting || "unset",
planActSeparateModelsSetting,
}
}
export async function updateApiConfiguration(context: vscode.ExtensionContext, apiConfiguration: ApiConfiguration) {
const {
apiProvider,
apiModelId,
apiKey,
openRouterApiKey,
awsAccessKey,
awsSecretKey,
awsSessionToken,
awsRegion,
awsUseCrossRegionInference,
awsBedrockUsePromptCache,
awsBedrockEndpoint,
awsProfile,
awsUseProfile,
vertexProjectId,
vertexRegion,
openAiBaseUrl,
openAiApiKey,
openAiModelId,
openAiModelInfo,
ollamaModelId,
ollamaBaseUrl,
ollamaApiOptionsCtxNum,
lmStudioModelId,
lmStudioBaseUrl,
anthropicBaseUrl,
geminiApiKey,
openAiNativeApiKey,
deepSeekApiKey,
requestyApiKey,
requestyModelId,
togetherApiKey,
togetherModelId,
qwenApiKey,
mistralApiKey,
azureApiVersion,
openRouterModelId,
openRouterModelInfo,
openRouterProviderSorting,
vsCodeLmModelSelector,
liteLlmBaseUrl,
liteLlmModelId,
liteLlmApiKey,
qwenApiLine,
asksageApiKey,
asksageApiUrl,
xaiApiKey,
thinkingBudgetTokens,
clineApiKey,
sambanovaApiKey,
} = apiConfiguration
await updateGlobalState(context, "apiProvider", apiProvider)
await updateGlobalState(context, "apiModelId", apiModelId)
await storeSecret(context, "apiKey", apiKey)
await storeSecret(context, "openRouterApiKey", openRouterApiKey)
await storeSecret(context, "awsAccessKey", awsAccessKey)
await storeSecret(context, "awsSecretKey", awsSecretKey)
await storeSecret(context, "awsSessionToken", awsSessionToken)
await updateGlobalState(context, "awsRegion", awsRegion)
await updateGlobalState(context, "awsUseCrossRegionInference", awsUseCrossRegionInference)
await updateGlobalState(context, "awsBedrockUsePromptCache", awsBedrockUsePromptCache)
await updateGlobalState(context, "awsBedrockEndpoint", awsBedrockEndpoint)
await updateGlobalState(context, "awsProfile", awsProfile)
await updateGlobalState(context, "awsUseProfile", awsUseProfile)
await updateGlobalState(context, "vertexProjectId", vertexProjectId)
await updateGlobalState(context, "vertexRegion", vertexRegion)
await updateGlobalState(context, "openAiBaseUrl", openAiBaseUrl)
await storeSecret(context, "openAiApiKey", openAiApiKey)
await updateGlobalState(context, "openAiModelId", openAiModelId)
await updateGlobalState(context, "openAiModelInfo", openAiModelInfo)
await updateGlobalState(context, "ollamaModelId", ollamaModelId)
await updateGlobalState(context, "ollamaBaseUrl", ollamaBaseUrl)
await updateGlobalState(context, "ollamaApiOptionsCtxNum", ollamaApiOptionsCtxNum)
await updateGlobalState(context, "lmStudioModelId", lmStudioModelId)
await updateGlobalState(context, "lmStudioBaseUrl", lmStudioBaseUrl)
await updateGlobalState(context, "anthropicBaseUrl", anthropicBaseUrl)
await storeSecret(context, "geminiApiKey", geminiApiKey)
await storeSecret(context, "openAiNativeApiKey", openAiNativeApiKey)
await storeSecret(context, "deepSeekApiKey", deepSeekApiKey)
await storeSecret(context, "requestyApiKey", requestyApiKey)
await storeSecret(context, "togetherApiKey", togetherApiKey)
await storeSecret(context, "qwenApiKey", qwenApiKey)
await storeSecret(context, "mistralApiKey", mistralApiKey)
await storeSecret(context, "liteLlmApiKey", liteLlmApiKey)
await storeSecret(context, "xaiApiKey", xaiApiKey)
await updateGlobalState(context, "azureApiVersion", azureApiVersion)
await updateGlobalState(context, "openRouterModelId", openRouterModelId)
await updateGlobalState(context, "openRouterModelInfo", openRouterModelInfo)
await updateGlobalState(context, "openRouterProviderSorting", openRouterProviderSorting)
await updateGlobalState(context, "vsCodeLmModelSelector", vsCodeLmModelSelector)
await updateGlobalState(context, "liteLlmBaseUrl", liteLlmBaseUrl)
await updateGlobalState(context, "liteLlmModelId", liteLlmModelId)
await updateGlobalState(context, "qwenApiLine", qwenApiLine)
await updateGlobalState(context, "requestyModelId", requestyModelId)
await updateGlobalState(context, "togetherModelId", togetherModelId)
await storeSecret(context, "asksageApiKey", asksageApiKey)
await updateGlobalState(context, "asksageApiUrl", asksageApiUrl)
await updateGlobalState(context, "thinkingBudgetTokens", thinkingBudgetTokens)
await storeSecret(context, "clineApiKey", clineApiKey)
await storeSecret(context, "sambanovaApiKey", sambanovaApiKey)
}
export async function resetExtensionState(context: vscode.ExtensionContext) {
for (const key of context.globalState.keys()) {
await context.globalState.update(key, undefined)
}
const secretKeys: SecretKey[] = [
"apiKey",
"openRouterApiKey",
"awsAccessKey",
"awsSecretKey",
"awsSessionToken",
"openAiApiKey",
"geminiApiKey",
"openAiNativeApiKey",
"deepSeekApiKey",
"requestyApiKey",
"togetherApiKey",
"qwenApiKey",
"mistralApiKey",
"clineApiKey",
"liteLlmApiKey",
"asksageApiKey",
"xaiApiKey",
"sambanovaApiKey",
]
for (const key of secretKeys) {
await storeSecret(context, key, undefined)
}
}
+67
View File
@@ -0,0 +1,67 @@
export type SecretKey =
| "apiKey"
| "clineApiKey"
| "openRouterApiKey"
| "awsAccessKey"
| "awsSecretKey"
| "awsSessionToken"
| "openAiApiKey"
| "geminiApiKey"
| "openAiNativeApiKey"
| "deepSeekApiKey"
| "requestyApiKey"
| "togetherApiKey"
| "qwenApiKey"
| "mistralApiKey"
| "liteLlmApiKey"
| "authNonce"
| "asksageApiKey"
| "xaiApiKey"
| "sambanovaApiKey"
export type GlobalStateKey =
| "apiProvider"
| "apiModelId"
| "awsRegion"
| "awsUseCrossRegionInference"
| "awsBedrockUsePromptCache"
| "awsBedrockEndpoint"
| "awsProfile"
| "awsUseProfile"
| "vertexProjectId"
| "vertexRegion"
| "lastShownAnnouncementId"
| "customInstructions"
| "taskHistory"
| "openAiBaseUrl"
| "openAiModelId"
| "openAiModelInfo"
| "ollamaModelId"
| "ollamaBaseUrl"
| "ollamaApiOptionsCtxNum"
| "lmStudioModelId"
| "lmStudioBaseUrl"
| "anthropicBaseUrl"
| "azureApiVersion"
| "openRouterModelId"
| "openRouterModelInfo"
| "openRouterProviderSorting"
| "autoApprovalSettings"
| "browserSettings"
| "chatSettings"
| "vsCodeLmModelSelector"
| "userInfo"
| "previousModeApiProvider"
| "previousModeModelId"
| "previousModeThinkingBudgetTokens"
| "previousModeVsCodeLmModelSelector"
| "previousModeModelInfo"
| "liteLlmBaseUrl"
| "liteLlmModelId"
| "qwenApiLine"
| "requestyModelId"
| "togetherModelId"
| "mcpMarketplaceCatalog"
| "telemetrySetting"
| "asksageApiUrl"
| "thinkingBudgetTokens"
| "planActSeparateModelsSetting"
+91 -91
View File
@@ -8,26 +8,26 @@ import pWaitFor from "p-wait-for"
import * as path from "path"
import { serializeError } from "serialize-error"
import * as vscode from "vscode"
import { ApiHandler, buildApiHandler } from "../api"
import { OpenRouterHandler } from "../api/providers/openrouter"
import CheckpointTracker from "../integrations/checkpoints/CheckpointTracker"
import { DIFF_VIEW_URI_SCHEME, DiffViewProvider } from "../integrations/editor/DiffViewProvider"
import { formatContentBlockToMarkdown } from "../integrations/misc/export-markdown"
import { extractTextFromFile } from "../integrations/misc/extract-text"
import { showSystemNotification } from "../integrations/notifications"
import { TerminalManager } from "../integrations/terminal/TerminalManager"
import { BrowserSession } from "../services/browser/BrowserSession"
import { UrlContentFetcher } from "../services/browser/UrlContentFetcher"
import { listFiles } from "../services/glob/list-files"
import { regexSearchFiles } from "../services/ripgrep"
import { parseSourceCodeForDefinitionsTopLevel } from "../services/tree-sitter"
import { ApiConfiguration } from "../shared/api"
import { findLast, findLastIndex, parsePartialArrayString } from "../shared/array"
import { AutoApprovalSettings } from "../shared/AutoApprovalSettings"
import { BrowserSettings } from "../shared/BrowserSettings"
import { ChatSettings } from "../shared/ChatSettings"
import { combineApiRequests } from "../shared/combineApiRequests"
import { combineCommandSequences, COMMAND_REQ_APP_STRING } from "../shared/combineCommandSequences"
import { ApiHandler, buildApiHandler } from "../../api"
import { OpenRouterHandler } from "../../api/providers/openrouter"
import CheckpointTracker from "../../integrations/checkpoints/CheckpointTracker"
import { DIFF_VIEW_URI_SCHEME, DiffViewProvider } from "../../integrations/editor/DiffViewProvider"
import { formatContentBlockToMarkdown } from "../../integrations/misc/export-markdown"
import { extractTextFromFile } from "../../integrations/misc/extract-text"
import { showSystemNotification } from "../../integrations/notifications"
import { TerminalManager } from "../../integrations/terminal/TerminalManager"
import { BrowserSession } from "../../services/browser/BrowserSession"
import { UrlContentFetcher } from "../../services/browser/UrlContentFetcher"
import { listFiles } from "../../services/glob/list-files"
import { regexSearchFiles } from "../../services/ripgrep"
import { parseSourceCodeForDefinitionsTopLevel } from "../../services/tree-sitter"
import { ApiConfiguration } from "../../shared/api"
import { findLast, findLastIndex, parsePartialArrayString } from "../../shared/array"
import { AutoApprovalSettings } from "../../shared/AutoApprovalSettings"
import { BrowserSettings } from "../../shared/BrowserSettings"
import { ChatSettings } from "../../shared/ChatSettings"
import { combineApiRequests } from "../../shared/combineApiRequests"
import { combineCommandSequences, COMMAND_REQ_APP_STRING } from "../../shared/combineCommandSequences"
import {
BrowserAction,
BrowserActionResult,
@@ -43,41 +43,41 @@ import {
ClineSayBrowserAction,
ClineSayTool,
COMPLETION_RESULT_CHANGES_FLAG,
} from "../shared/ExtensionMessage"
import { getApiMetrics } from "../shared/getApiMetrics"
import { HistoryItem } from "../shared/HistoryItem"
import { ClineAskResponse, ClineCheckpointRestore } from "../shared/WebviewMessage"
import { calculateApiCostAnthropic } from "../utils/cost"
import { fileExistsAtPath, isDirectory } from "../utils/fs"
import { arePathsEqual, getReadablePath } from "../utils/path"
import { fixModelHtmlEscaping, removeInvalidChars } from "../utils/string"
import { AssistantMessageContent, parseAssistantMessage, ToolParamName, ToolUseName } from "./assistant-message"
import { constructNewFileContent } from "./assistant-message/diff"
import { ClineIgnoreController, LOCK_TEXT_SYMBOL } from "./ignore/ClineIgnoreController"
import { parseMentions } from "./mentions"
import { formatResponse } from "./prompts/responses"
import { addUserInstructions, SYSTEM_PROMPT } from "./prompts/system"
import { ContextManager } from "./context-management/ContextManager"
import { OpenAiHandler } from "../api/providers/openai"
import { ApiStream } from "../api/transform/stream"
import { ClineHandler } from "../api/providers/cline"
import { ClineProvider } from "./webview/ClineProvider"
import { DEFAULT_LANGUAGE_SETTINGS, getLanguageKey, LanguageDisplay, LanguageKey } from "../shared/Languages"
import { telemetryService } from "../services/telemetry/TelemetryService"
} from "../../shared/ExtensionMessage"
import { getApiMetrics } from "../../shared/getApiMetrics"
import { HistoryItem } from "../../shared/HistoryItem"
import { ClineAskResponse, ClineCheckpointRestore } from "../../shared/WebviewMessage"
import { calculateApiCostAnthropic } from "../../utils/cost"
import { fileExistsAtPath, isDirectory } from "../../utils/fs"
import { arePathsEqual, getReadablePath } from "../../utils/path"
import { fixModelHtmlEscaping, removeInvalidChars } from "../../utils/string"
import { AssistantMessageContent, parseAssistantMessage, ToolParamName, ToolUseName } from ".././assistant-message"
import { constructNewFileContent } from ".././assistant-message/diff"
import { ClineIgnoreController, LOCK_TEXT_SYMBOL } from ".././ignore/ClineIgnoreController"
import { parseMentions } from ".././mentions"
import { formatResponse } from ".././prompts/responses"
import { addUserInstructions, SYSTEM_PROMPT } from ".././prompts/system"
import { ContextManager } from ".././context-management/ContextManager"
import { OpenAiHandler } from "../../api/providers/openai"
import { ApiStream } from "../../api/transform/stream"
import { ClineHandler } from "../../api/providers/cline"
import { Controller } from "../controller"
import { DEFAULT_LANGUAGE_SETTINGS, getLanguageKey, LanguageDisplay, LanguageKey } from "../../shared/Languages"
import { telemetryService } from "../../services/telemetry/TelemetryService"
import pTimeout from "p-timeout"
import { GlobalFileNames } from "../global-constants"
import { GlobalFileNames } from "../../global-constants"
import {
checkIsAnthropicContextWindowError,
checkIsOpenRouterContextWindowError,
} from "./context-management/context-error-handling"
import { AnthropicHandler } from "../api/providers/anthropic"
} from "../context-management/context-error-handling"
import { AnthropicHandler } from "../../api/providers/anthropic"
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) ?? path.join(os.homedir(), "Desktop") // may or may not exist but fs checking existence would immediately ask for permission which would be bad UX, need to come up with a better solution
type ToolResponse = string | Array<Anthropic.TextBlockParam | Anthropic.ImageBlockParam>
type UserContent = Array<Anthropic.ContentBlockParam>
export class Cline {
export class Task {
readonly taskId: string
readonly apiProvider?: string
api: ApiHandler
@@ -99,7 +99,7 @@ export class Cline {
private lastMessageTs?: number
private consecutiveAutoApprovedRequestsCount: number = 0
private consecutiveMistakeCount: number = 0
private providerRef: WeakRef<ClineProvider>
private controllerRef: WeakRef<Controller>
private abort: boolean = false
didFinishAbortingStream = false
abandoned = false
@@ -126,7 +126,7 @@ export class Cline {
private didAutomaticallyRetryFailedApiRequest = false
constructor(
provider: ClineProvider,
controller: Controller,
apiConfiguration: ApiConfiguration,
autoApprovalSettings: AutoApprovalSettings,
browserSettings: BrowserSettings,
@@ -140,11 +140,11 @@ export class Cline {
this.clineIgnoreController.initialize().catch((error) => {
console.error("Failed to initialize ClineIgnoreController:", error)
})
this.providerRef = new WeakRef(provider)
this.controllerRef = new WeakRef(controller)
this.apiProvider = apiConfiguration.apiProvider
this.terminalManager = new TerminalManager()
this.urlContentFetcher = new UrlContentFetcher(provider.context)
this.browserSession = new BrowserSession(provider.context, browserSettings)
this.urlContentFetcher = new UrlContentFetcher(controller.context)
this.browserSession = new BrowserSession(controller.context, browserSettings)
this.contextManager = new ContextManager()
this.diffViewProvider = new DiffViewProvider(cwd)
this.customInstructions = customInstructions
@@ -196,7 +196,7 @@ export class Cline {
// Storing task to disk for history
private async ensureTaskDirectoryExists(): Promise<string> {
const globalStoragePath = this.providerRef.deref()?.context.globalStorageUri.fsPath
const globalStoragePath = this.controllerRef.deref()?.context.globalStorageUri.fsPath
if (!globalStoragePath) {
throw new Error("Global storage uri is invalid")
}
@@ -284,7 +284,7 @@ export class Cline {
} catch (error) {
console.error("Failed to get task directory size:", taskDir, error)
}
await this.providerRef.deref()?.updateTaskHistory({
await this.controllerRef.deref()?.updateTaskHistory({
id: this.taskId,
ts: lastRelevantMessage.ts,
task: taskMessage.text ?? "",
@@ -321,13 +321,13 @@ export class Cline {
try {
this.checkpointTracker = await CheckpointTracker.create(
this.taskId,
this.providerRef.deref()?.context.globalStorageUri.fsPath,
this.controllerRef.deref()?.context.globalStorageUri.fsPath,
)
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error"
console.error("Failed to initialize checkpoint tracker:", errorMessage)
this.checkpointTrackerErrorMessage = errorMessage
await this.providerRef.deref()?.postStateToWebview()
await this.controllerRef.deref()?.postStateToWebview()
vscode.window.showErrorMessage(errorMessage)
didWorkspaceRestoreFail = true
}
@@ -403,17 +403,17 @@ export class Cline {
await this.saveClineMessages()
await this.providerRef.deref()?.postMessageToWebview({ type: "relinquishControl" })
await this.controllerRef.deref()?.postMessageToWebview({ type: "relinquishControl" })
this.providerRef.deref()?.cancelTask() // the task is already cancelled by the provider beforehand, but we need to re-init to get the updated messages
this.controllerRef.deref()?.cancelTask() // the task is already cancelled by the provider beforehand, but we need to re-init to get the updated messages
} else {
await this.providerRef.deref()?.postMessageToWebview({ type: "relinquishControl" })
await this.controllerRef.deref()?.postMessageToWebview({ type: "relinquishControl" })
}
}
async presentMultifileDiff(messageTs: number, seeNewChangesSinceLastTaskCompletion: boolean) {
const relinquishButton = () => {
this.providerRef.deref()?.postMessageToWebview({ type: "relinquishControl" })
this.controllerRef.deref()?.postMessageToWebview({ type: "relinquishControl" })
}
console.log("presentMultifileDiff", messageTs)
@@ -436,13 +436,13 @@ export class Cline {
try {
this.checkpointTracker = await CheckpointTracker.create(
this.taskId,
this.providerRef.deref()?.context.globalStorageUri.fsPath,
this.controllerRef.deref()?.context.globalStorageUri.fsPath,
)
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error"
console.error("Failed to initialize checkpoint tracker:", errorMessage)
this.checkpointTrackerErrorMessage = errorMessage
await this.providerRef.deref()?.postStateToWebview()
await this.controllerRef.deref()?.postStateToWebview()
vscode.window.showErrorMessage(errorMessage)
relinquishButton()
return
@@ -551,7 +551,7 @@ export class Cline {
try {
this.checkpointTracker = await CheckpointTracker.create(
this.taskId,
this.providerRef.deref()?.context.globalStorageUri.fsPath,
this.controllerRef.deref()?.context.globalStorageUri.fsPath,
)
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error"
@@ -623,8 +623,8 @@ export class Cline {
lastMessage.partial = partial
// todo be more efficient about saving and posting only new data or one whole message at a time so ignore partial for saves, and only post parts of partial message instead of whole array in new listener
// await this.saveClineMessages()
// await this.providerRef.deref()?.postStateToWebview()
await this.providerRef.deref()?.postMessageToWebview({
// await this.controllerRef.deref()?.postStateToWebview()
await this.controllerRef.deref()?.postMessageToWebview({
type: "partialMessage",
partialMessage: lastMessage,
})
@@ -643,7 +643,7 @@ export class Cline {
text,
partial,
})
await this.providerRef.deref()?.postStateToWebview()
await this.controllerRef.deref()?.postStateToWebview()
throw new Error("Current ask promise was ignored 2")
}
} else {
@@ -666,8 +666,8 @@ export class Cline {
lastMessage.text = text
lastMessage.partial = false
await this.saveClineMessages()
// await this.providerRef.deref()?.postStateToWebview()
await this.providerRef.deref()?.postMessageToWebview({
// await this.controllerRef.deref()?.postStateToWebview()
await this.controllerRef.deref()?.postMessageToWebview({
type: "partialMessage",
partialMessage: lastMessage,
})
@@ -684,7 +684,7 @@ export class Cline {
ask: type,
text,
})
await this.providerRef.deref()?.postStateToWebview()
await this.controllerRef.deref()?.postStateToWebview()
}
}
} else {
@@ -701,7 +701,7 @@ export class Cline {
ask: type,
text,
})
await this.providerRef.deref()?.postStateToWebview()
await this.controllerRef.deref()?.postStateToWebview()
}
await pWaitFor(() => this.askResponse !== undefined || this.lastMessageTs !== askTs, { interval: 100 })
@@ -740,7 +740,7 @@ export class Cline {
lastMessage.text = text
lastMessage.images = images
lastMessage.partial = partial
await this.providerRef.deref()?.postMessageToWebview({
await this.controllerRef.deref()?.postMessageToWebview({
type: "partialMessage",
partialMessage: lastMessage,
})
@@ -756,7 +756,7 @@ export class Cline {
images,
partial,
})
await this.providerRef.deref()?.postStateToWebview()
await this.controllerRef.deref()?.postStateToWebview()
}
} else {
// partial=false means its a complete version of a previously partial message
@@ -770,8 +770,8 @@ export class Cline {
// instead of streaming partialMessage events, we do a save and post like normal to persist to disk
await this.saveClineMessages()
// await this.providerRef.deref()?.postStateToWebview()
await this.providerRef.deref()?.postMessageToWebview({
// await this.controllerRef.deref()?.postStateToWebview()
await this.controllerRef.deref()?.postMessageToWebview({
type: "partialMessage",
partialMessage: lastMessage,
}) // more performant than an entire postStateToWebview
@@ -786,7 +786,7 @@ export class Cline {
text,
images,
})
await this.providerRef.deref()?.postStateToWebview()
await this.controllerRef.deref()?.postStateToWebview()
}
}
} else {
@@ -800,7 +800,7 @@ export class Cline {
text,
images,
})
await this.providerRef.deref()?.postStateToWebview()
await this.controllerRef.deref()?.postStateToWebview()
}
}
@@ -819,7 +819,7 @@ export class Cline {
if (lastMessage?.partial && lastMessage.type === type && (lastMessage.ask === askOrSay || lastMessage.say === askOrSay)) {
this.clineMessages.pop()
await this.saveClineMessages()
await this.providerRef.deref()?.postStateToWebview()
await this.controllerRef.deref()?.postStateToWebview()
}
}
@@ -831,7 +831,7 @@ export class Cline {
this.clineMessages = []
this.apiConversationHistory = []
await this.providerRef.deref()?.postStateToWebview()
await this.controllerRef.deref()?.postStateToWebview()
await this.say("text", task, images)
@@ -853,7 +853,7 @@ export class Cline {
private async resumeTaskFromHistory() {
// UPDATE: we don't need this anymore since most tasks are now created with checkpoints enabled
// right now we let users init checkpoints for old tasks, assuming they're continuing them from the same workspace (which we never tied to tasks, so no way for us to know if it's opened in the right workspace)
// const doesShadowGitExist = await CheckpointTracker.doesShadowGitExist(this.taskId, this.providerRef.deref())
// const doesShadowGitExist = await CheckpointTracker.doesShadowGitExist(this.taskId, this.controllerRef.deref())
// if (!doesShadowGitExist) {
// this.checkpointTrackerErrorMessage = "Checkpoints are only available for new tasks"
// }
@@ -1288,11 +1288,11 @@ export class Cline {
async *attemptApiRequest(previousApiReqIndex: number): ApiStream {
// Wait for MCP servers to be connected before generating system prompt
await pWaitFor(() => this.providerRef.deref()?.mcpHub?.isConnecting !== true, { timeout: 10_000 }).catch(() => {
await pWaitFor(() => this.controllerRef.deref()?.mcpHub?.isConnecting !== true, { timeout: 10_000 }).catch(() => {
console.error("MCP servers failed to connect in time")
})
const mcpHub = this.providerRef.deref()?.mcpHub
const mcpHub = this.controllerRef.deref()?.mcpHub
if (!mcpHub) {
throw new Error("MCP hub not available")
}
@@ -1970,7 +1970,7 @@ export class Cline {
}
if (!fileExists) {
this.providerRef.deref()?.workspaceTracker?.populateFilePaths()
this.controllerRef.deref()?.workspaceTracker?.populateFilePaths()
}
await this.diffViewProvider.reset()
@@ -2534,7 +2534,7 @@ export class Cline {
}
// Re-populate file paths in case the command modified the workspace (vscode listeners do not trigger unless the user manually creates/deletes files)
this.providerRef.deref()?.workspaceTracker?.populateFilePaths()
this.controllerRef.deref()?.workspaceTracker?.populateFilePaths()
pushToolResult(result)
@@ -2616,7 +2616,7 @@ export class Cline {
arguments: mcp_arguments,
} satisfies ClineAskUseMcpServer)
const isToolAutoApproved = this.providerRef
const isToolAutoApproved = this.controllerRef
.deref()
?.mcpHub?.connections?.find((conn) => conn.server.name === server_name)
?.server.tools?.find((tool) => tool.name === tool_name)?.autoApprove
@@ -2638,7 +2638,7 @@ export class Cline {
// now execute the tool
await this.say("mcp_server_request_started") // same as browser_action_result
const toolResult = await this.providerRef
const toolResult = await this.controllerRef
.deref()
?.mcpHub?.callTool(server_name, tool_name, parsedArguments)
@@ -2728,7 +2728,7 @@ export class Cline {
// now execute the tool
await this.say("mcp_server_request_started")
const resourceResult = await this.providerRef.deref()?.mcpHub?.readResource(server_name, uri)
const resourceResult = await this.controllerRef.deref()?.mcpHub?.readResource(server_name, uri)
const resourceResultPretty =
resourceResult?.contents
.map((item) => {
@@ -3158,7 +3158,7 @@ export class Cline {
if (!this.checkpointTracker && !this.checkpointTrackerErrorMessage) {
try {
this.checkpointTracker = await pTimeout(
CheckpointTracker.create(this.taskId, this.providerRef.deref()?.context.globalStorageUri.fsPath),
CheckpointTracker.create(this.taskId, this.controllerRef.deref()?.context.globalStorageUri.fsPath),
{
milliseconds: 15_000,
message:
@@ -3200,7 +3200,7 @@ export class Cline {
request: userContent.map((block) => formatContentBlockToMarkdown(block)).join("\n\n"),
} satisfies ClineApiReqInfo)
await this.saveClineMessages()
await this.providerRef.deref()?.postStateToWebview()
await this.controllerRef.deref()?.postStateToWebview()
try {
let cacheWriteTokens = 0
@@ -3360,10 +3360,10 @@ export class Cline {
const errorMessage = this.formatErrorWithStatusCode(error)
await abortStream("streaming_failed", errorMessage)
const history = await this.providerRef.deref()?.getTaskWithId(this.taskId)
const history = await this.controllerRef.deref()?.getTaskWithId(this.taskId)
if (history) {
await this.providerRef.deref()?.initClineWithHistoryItem(history.historyItem)
// await this.providerRef.deref()?.postStateToWebview()
await this.controllerRef.deref()?.initClineWithHistoryItem(history.historyItem)
// await this.controllerRef.deref()?.postStateToWebview()
}
}
} finally {
@@ -3383,7 +3383,7 @@ export class Cline {
}
updateApiReqMsg()
await this.saveClineMessages()
await this.providerRef.deref()?.postStateToWebview()
await this.controllerRef.deref()?.postStateToWebview()
})
}
@@ -3407,7 +3407,7 @@ export class Cline {
updateApiReqMsg()
await this.saveClineMessages()
await this.providerRef.deref()?.postStateToWebview()
await this.controllerRef.deref()?.postStateToWebview()
// now add to apiconversationhistory
// need to save assistant responses to file before proceeding to tool use since user can exit at any moment and we wouldn't be able to save the assistant's response
File diff suppressed because it is too large Load Diff
+322
View File
@@ -0,0 +1,322 @@
import axios from "axios"
import * as vscode from "vscode"
import { getNonce } from "./getNonce"
import { getUri } from "./getUri"
import { getTheme } from "../../integrations/theme/getTheme"
import { Controller } from "../controller"
import { findLast } from "../../shared/array"
/*
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
https://github.com/KumarVariable/vscode-extension-sidebar-html/blob/master/src/customSidebarViewProvider.ts
*/
export class WebviewProvider implements vscode.WebviewViewProvider {
public static readonly sideBarId = "claude-dev.SidebarProvider" // 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 break existing instances of the extension.
public static readonly tabPanelId = "claude-dev.TabPanelProvider"
private static activeInstances: Set<WebviewProvider> = new Set()
public view?: vscode.WebviewView | vscode.WebviewPanel
private disposables: vscode.Disposable[] = []
controller: Controller
constructor(
readonly context: vscode.ExtensionContext,
private readonly outputChannel: vscode.OutputChannel,
) {
WebviewProvider.activeInstances.add(this)
this.controller = new Controller(context, outputChannel, this)
}
async dispose() {
if (this.view && "dispose" in this.view) {
this.view.dispose()
}
while (this.disposables.length) {
const x = this.disposables.pop()
if (x) {
x.dispose()
}
}
await this.controller.dispose()
WebviewProvider.activeInstances.delete(this)
}
public static getVisibleInstance(): WebviewProvider | undefined {
return findLast(Array.from(this.activeInstances), (instance) => instance.view?.visible === true)
}
async resolveWebviewView(webviewView: vscode.WebviewView | vscode.WebviewPanel) {
this.view = webviewView
webviewView.webview.options = {
// Allow scripts in the webview
enableScripts: true,
localResourceRoots: [this.context.extensionUri],
}
webviewView.webview.html =
this.context.extensionMode === vscode.ExtensionMode.Development
? await this.getHMRHtmlContent(webviewView.webview)
: this.getHtmlContent(webviewView.webview)
// Sets up an event listener to listen for messages passed from the webview view context
// and executes code based on the message that is received
this.setWebviewMessageListener(webviewView.webview)
// Logs show up in bottom panel > Debug Console
//console.log("registering listener")
// Listen for when the panel becomes visible
// https://github.com/microsoft/vscode-discussions/discussions/840
if ("onDidChangeViewState" in webviewView) {
// WebviewView and WebviewPanel have all the same properties except for this visibility listener
// panel
webviewView.onDidChangeViewState(
() => {
if (this.view?.visible) {
this.controller.postMessageToWebview({
type: "action",
action: "didBecomeVisible",
})
}
},
null,
this.disposables,
)
} else if ("onDidChangeVisibility" in webviewView) {
// sidebar
webviewView.onDidChangeVisibility(
() => {
if (this.view?.visible) {
this.controller.postMessageToWebview({
type: "action",
action: "didBecomeVisible",
})
}
},
null,
this.disposables,
)
}
// Listen for when the view is disposed
// This happens when the user closes the view or when the view is closed programmatically
webviewView.onDidDispose(
async () => {
await this.dispose()
},
null,
this.disposables,
)
// // if the extension is starting a new session, clear previous task state
// this.clearTask()
{
// Listen for configuration changes
vscode.workspace.onDidChangeConfiguration(
async (e) => {
if (e && e.affectsConfiguration("workbench.colorTheme")) {
// Sends latest theme name to webview
await this.controller.postMessageToWebview({
type: "theme",
text: JSON.stringify(await getTheme()),
})
}
if (e && e.affectsConfiguration("cline.mcpMarketplace.enabled")) {
// Update state when marketplace tab setting changes
await this.controller.postStateToWebview()
}
},
null,
this.disposables,
)
// if the extension is starting a new session, clear previous task state
this.controller.clearTask()
this.outputChannel.appendLine("Webview view resolved")
}
}
/**
* Defines and returns the HTML that should be rendered within the webview panel.
*
* @remarks This is also the place where references to the React webview build files
* are created and inserted into the webview HTML.
*
* @param webview A reference to the extension webview
* @param extensionUri The URI of the directory containing the extension
* @returns A template string literal containing the HTML that should be
* rendered within the webview panel
*/
private getHtmlContent(webview: vscode.Webview): string {
// Get the local path to main script run in the webview,
// then convert it to a uri we can use in the webview.
// The CSS file from the React build output
const stylesUri = getUri(webview, this.context.extensionUri, ["webview-ui", "build", "assets", "index.css"])
// The JS file from the React build output
const scriptUri = getUri(webview, this.context.extensionUri, ["webview-ui", "build", "assets", "index.js"])
// The codicon font from the React build output
// https://github.com/microsoft/vscode-extension-samples/blob/main/webview-codicons-sample/src/extension.ts
// we installed this package in the extension so that we can access it how its intended from the extension (the font file is likely bundled in vscode), and we just import the css fileinto our react app we don't have access to it
// don't forget to add font-src ${webview.cspSource};
const codiconsUri = getUri(webview, this.context.extensionUri, [
"node_modules",
"@vscode",
"codicons",
"dist",
"codicon.css",
])
// const scriptUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, "assets", "main.js"))
// const styleResetUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, "assets", "reset.css"))
// const styleVSCodeUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, "assets", "vscode.css"))
// // Same for stylesheet
// const stylesheetUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, "assets", "main.css"))
// Use a nonce to only allow a specific script to be run.
/*
content security policy of your webview to only allow scripts that have a specific nonce
create a content security policy meta tag so that only loading scripts with a nonce is allowed
As your extension grows you will likely want to add custom styles, fonts, and/or images to your webview. If you do, you will need to update the content security policy meta tag to explicity allow for these resources. E.g.
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${webview.cspSource}; font-src ${webview.cspSource}; img-src ${webview.cspSource} https:; script-src 'nonce-${nonce}';">
- 'unsafe-inline' is required for styles due to vscode-webview-toolkit's dynamic style injection
- since we pass base64 images to the webview, we need to specify img-src ${webview.cspSource} data:;
in meta tag we add nonce attribute: A cryptographic nonce (only used once) to allow scripts. The server must generate a unique nonce value each time it transmits a policy. It is critical to provide a nonce that cannot be guessed as bypassing a resource's policy is otherwise trivial.
*/
const nonce = getNonce()
// Tip: Install the es6-string-html VS Code extension to enable code highlighting below
return /*html*/ `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<link rel="stylesheet" type="text/css" href="${stylesUri}">
<link href="${codiconsUri}" rel="stylesheet" />
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; connect-src https://*.posthog.com https://*.firebaseauth.com https://*.firebaseio.com https://*.googleapis.com https://*.firebase.com; font-src ${webview.cspSource}; style-src ${webview.cspSource} 'unsafe-inline'; img-src ${webview.cspSource} https: data:; script-src 'nonce-${nonce}' 'unsafe-eval';">
<title>Cline</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script type="module" nonce="${nonce}" src="${scriptUri}"></script>
</body>
</html>
`
}
/**
* Connects to the local Vite dev server to allow HMR, with fallback to the bundled assets
*
* @param webview A reference to the extension webview
* @returns A template string literal containing the HTML that should be
* rendered within the webview panel
*/
private async getHMRHtmlContent(webview: vscode.Webview): Promise<string> {
const localPort = 25463
const localServerUrl = `localhost:${localPort}`
// Check if local dev server is running.
try {
await axios.get(`http://${localServerUrl}`)
} catch (error) {
vscode.window.showErrorMessage(
"Cline: Local webview dev server is not running, HMR will not work. Please run 'npm run dev:webview' before launching the extension to enable HMR. Using bundled assets.",
)
return this.getHtmlContent(webview)
}
const nonce = getNonce()
const stylesUri = getUri(webview, this.context.extensionUri, ["webview-ui", "build", "assets", "index.css"])
const codiconsUri = getUri(webview, this.context.extensionUri, [
"node_modules",
"@vscode",
"codicons",
"dist",
"codicon.css",
])
const scriptEntrypoint = "src/main.tsx"
const scriptUri = `http://${localServerUrl}/${scriptEntrypoint}`
const reactRefresh = /*html*/ `
<script nonce="${nonce}" type="module">
import RefreshRuntime from "http://${localServerUrl}/@react-refresh"
RefreshRuntime.injectIntoGlobalHook(window)
window.$RefreshReg$ = () => {}
window.$RefreshSig$ = () => (type) => type
window.__vite_plugin_react_preamble_installed__ = true
</script>
`
const csp = [
"default-src 'none'",
`font-src ${webview.cspSource}`,
`style-src ${webview.cspSource} 'unsafe-inline' https://* http://${localServerUrl} http://0.0.0.0:${localPort}`,
`img-src ${webview.cspSource} https: data:`,
`script-src 'unsafe-eval' https://* http://${localServerUrl} http://0.0.0.0:${localPort} 'nonce-${nonce}'`,
`connect-src https://* ws://${localServerUrl} ws://0.0.0.0:${localPort} http://${localServerUrl} http://0.0.0.0:${localPort}`,
]
return /*html*/ `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no">
<meta http-equiv="Content-Security-Policy" content="${csp.join("; ")}">
<link rel="stylesheet" type="text/css" href="${stylesUri}">
<link href="${codiconsUri}" rel="stylesheet" />
<title>Cline</title>
</head>
<body>
<div id="root"></div>
${reactRefresh}
<script type="module" src="${scriptUri}"></script>
</body>
</html>
`
}
/**
* Sets up an event listener to listen for messages passed from the webview context and
* executes code based on the message that is received.
*
* IMPORTANT: When passing methods as callbacks in JavaScript/TypeScript, the method's
* 'this' context can be lost. This happens because the method is passed as a
* standalone function reference, detached from its original object.
*
* The Problem:
* Doing: webview.onDidReceiveMessage(this.controller.handleWebviewMessage)
* Would cause 'this' inside handleWebviewMessage to be undefined or wrong,
* leading to "TypeError: this.setUserInfo is not a function"
*
* The Solution:
* We wrap the method call in an arrow function, which:
* 1. Preserves the lexical scope's 'this' binding
* 2. Ensures handleWebviewMessage is called as a method on the controller instance
* 3. Maintains access to all controller methods and properties
*
* Alternative solutions could use .bind() or making handleWebviewMessage an arrow
* function property, but this approach is clean and explicit.
*
* @param webview The webview instance to attach the message listener to
*/
private setWebviewMessageListener(webview: vscode.Webview) {
webview.onDidReceiveMessage(
(message) => {
this.controller.handleWebviewMessage(message)
},
null,
this.disposables,
)
}
}
+4 -4
View File
@@ -1,7 +1,7 @@
import * as vscode from "vscode"
import * as fs from "fs/promises"
import * as path from "path"
import { ClineProvider } from "../../core/webview/ClineProvider"
import { Controller } from "../../core/controller"
import { HistoryItem } from "../../shared/HistoryItem"
import { ClineMessage } from "../../shared/ExtensionMessage"
@@ -9,7 +9,7 @@ import { ClineMessage } from "../../shared/ExtensionMessage"
* Registers development-only commands for task manipulation.
* These are only activated in development mode.
*/
export function registerTaskCommands(context: vscode.ExtensionContext, provider: ClineProvider): vscode.Disposable[] {
export function registerTaskCommands(context: vscode.ExtensionContext, controller: Controller): vscode.Disposable[] {
return [
vscode.commands.registerCommand("cline.dev.createTestTasks", async () => {
const count = await vscode.window.showInputBox({
@@ -88,13 +88,13 @@ export function registerTaskCommands(context: vscode.ExtensionContext, provider:
}
// Update task history in global state
await provider.updateTaskHistory(historyItem)
await controller.updateTaskHistory(historyItem)
progress.report({ increment: 100 / tasksCount })
}
// Update the UI to show the new tasks
await provider.postStateToWebview()
await controller.postStateToWebview()
vscode.window.showInformationMessage(`Created ${tasksCount} test tasks`)
},
+12 -11
View File
@@ -1,27 +1,28 @@
import * as vscode from "vscode"
import { ClineProvider } from "../core/webview/ClineProvider"
import { Controller } from "../core/controller"
import { ClineAPI } from "./cline"
import { getGlobalState } from "../core/state"
export function createClineAPI(outputChannel: vscode.OutputChannel, sidebarProvider: ClineProvider): ClineAPI {
export function createClineAPI(outputChannel: vscode.OutputChannel, sidebarController: Controller): ClineAPI {
const api: ClineAPI = {
setCustomInstructions: async (value: string) => {
await sidebarProvider.updateCustomInstructions(value)
await sidebarController.updateCustomInstructions(value)
outputChannel.appendLine("Custom instructions set")
},
getCustomInstructions: async () => {
return (await sidebarProvider.getGlobalState("customInstructions")) as string | undefined
return (await getGlobalState(sidebarController.context, "customInstructions")) as string | undefined
},
startNewTask: async (task?: string, images?: string[]) => {
outputChannel.appendLine("Starting new task")
await sidebarProvider.clearTask()
await sidebarProvider.postStateToWebview()
await sidebarProvider.postMessageToWebview({
await sidebarController.clearTask()
await sidebarController.postStateToWebview()
await sidebarController.postMessageToWebview({
type: "action",
action: "chatButtonClicked",
})
await sidebarProvider.postMessageToWebview({
await sidebarController.postMessageToWebview({
type: "invoke",
invoke: "sendMessage",
text: task,
@@ -36,7 +37,7 @@ export function createClineAPI(outputChannel: vscode.OutputChannel, sidebarProvi
outputChannel.appendLine(
`Sending message: ${message ? `"${message}"` : "undefined"} with ${images?.length || 0} image(s)`,
)
await sidebarProvider.postMessageToWebview({
await sidebarController.postMessageToWebview({
type: "invoke",
invoke: "sendMessage",
text: message,
@@ -46,7 +47,7 @@ export function createClineAPI(outputChannel: vscode.OutputChannel, sidebarProvi
pressPrimaryButton: async () => {
outputChannel.appendLine("Pressing primary button")
await sidebarProvider.postMessageToWebview({
await sidebarController.postMessageToWebview({
type: "invoke",
invoke: "primaryButtonClick",
})
@@ -54,7 +55,7 @@ export function createClineAPI(outputChannel: vscode.OutputChannel, sidebarProvi
pressSecondaryButton: async () => {
outputChannel.appendLine("Pressing secondary button")
await sidebarProvider.postMessageToWebview({
await sidebarController.postMessageToWebview({
type: "invoke",
invoke: "secondaryButtonClick",
})
+36 -34
View File
@@ -2,13 +2,13 @@
// Import the module and reference it with the alias vscode in your code below
import { setTimeout as setTimeoutPromise } from "node:timers/promises"
import * as vscode from "vscode"
import { ClineProvider } from "./core/webview/ClineProvider"
import { Logger } from "./services/logging/Logger"
import { createClineAPI } from "./exports"
import "./utils/path" // necessary to have access to String.prototype.toPosix
import { DIFF_VIEW_URI_SCHEME } from "./integrations/editor/DiffViewProvider"
import assert from "node:assert"
import { telemetryService } from "./services/telemetry/TelemetryService"
import { WebviewProvider } from "./core/webview"
/*
Built using https://github.com/microsoft/vscode-webview-ui-toolkit
@@ -30,12 +30,12 @@ export function activate(context: vscode.ExtensionContext) {
Logger.initialize(outputChannel)
Logger.log("Cline extension activated")
const sidebarProvider = new ClineProvider(context, outputChannel)
const sidebarWebview = new WebviewProvider(context, outputChannel)
vscode.commands.executeCommand("setContext", "cline.isDevMode", IS_DEV && IS_DEV === "true")
context.subscriptions.push(
vscode.window.registerWebviewViewProvider(ClineProvider.sideBarId, sidebarProvider, {
vscode.window.registerWebviewViewProvider(WebviewProvider.sideBarId, sidebarWebview, {
webviewOptions: { retainContextWhenHidden: true },
}),
)
@@ -43,15 +43,15 @@ export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand("cline.plusButtonClicked", async () => {
Logger.log("Plus button Clicked")
const visibleProvider = ClineProvider.getVisibleInstance()
if (!visibleProvider) {
const visibleWebview = WebviewProvider.getVisibleInstance()
if (!visibleWebview) {
Logger.log("Cannot find any visible Cline instances.")
return
}
await visibleProvider.clearTask()
await visibleProvider.postStateToWebview()
await visibleProvider.postMessageToWebview({
await visibleWebview.controller.clearTask()
await visibleWebview.controller.postStateToWebview()
await visibleWebview.controller.postMessageToWebview({
type: "action",
action: "chatButtonClicked",
})
@@ -60,13 +60,13 @@ export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand("cline.mcpButtonClicked", () => {
const visibleProvider = ClineProvider.getVisibleInstance()
if (!visibleProvider) {
const visibleWebview = WebviewProvider.getVisibleInstance()
if (!visibleWebview) {
Logger.log("Cannot find any visible Cline instances.")
return
}
visibleProvider.postMessageToWebview({
visibleWebview.controller.postMessageToWebview({
type: "action",
action: "mcpButtonClicked",
})
@@ -77,7 +77,7 @@ export function activate(context: vscode.ExtensionContext) {
Logger.log("Opening Cline in new tab")
// (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 tabProvider = new ClineProvider(context, outputChannel)
const tabWebview = new WebviewProvider(context, outputChannel)
//const column = vscode.window.activeTextEditor ? vscode.window.activeTextEditor.viewColumn : undefined
const lastCol = Math.max(...vscode.window.visibleTextEditors.map((editor) => editor.viewColumn || 0))
@@ -88,7 +88,7 @@ export function activate(context: vscode.ExtensionContext) {
}
const targetCol = hasVisibleEditors ? Math.max(lastCol + 1, 1) : vscode.ViewColumn.Two
const panel = vscode.window.createWebviewPanel(ClineProvider.tabPanelId, "Cline", targetCol, {
const panel = vscode.window.createWebviewPanel(WebviewProvider.tabPanelId, "Cline", targetCol, {
enableScripts: true,
retainContextWhenHidden: true,
localResourceRoots: [context.extensionUri],
@@ -99,7 +99,7 @@ export function activate(context: vscode.ExtensionContext) {
light: vscode.Uri.joinPath(context.extensionUri, "assets", "icons", "robot_panel_light.png"),
dark: vscode.Uri.joinPath(context.extensionUri, "assets", "icons", "robot_panel_dark.png"),
}
tabProvider.resolveWebviewView(panel)
tabWebview.resolveWebviewView(panel)
// Lock the editor group so clicking on files doesn't open them over the panel
await setTimeoutPromise(100)
@@ -112,13 +112,13 @@ export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand("cline.settingsButtonClicked", () => {
//vscode.window.showInformationMessage(message)
const visibleClineProvider = ClineProvider.getVisibleInstance()
if (!visibleClineProvider) {
const visibleWebview = WebviewProvider.getVisibleInstance()
if (!visibleWebview) {
Logger.log("Cannot find any visible Cline instances.")
return
}
visibleClineProvider.postMessageToWebview({
visibleWebview.controller.postMessageToWebview({
type: "action",
action: "settingsButtonClicked",
})
@@ -127,13 +127,13 @@ export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand("cline.historyButtonClicked", () => {
const visibleProvider = ClineProvider.getVisibleInstance()
if (!visibleProvider) {
const visibleWebview = WebviewProvider.getVisibleInstance()
if (!visibleWebview) {
Logger.log("Cannot find any visible Cline instances.")
return
}
visibleProvider.postMessageToWebview({
visibleWebview.controller.postMessageToWebview({
type: "action",
action: "historyButtonClicked",
})
@@ -142,13 +142,13 @@ export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand("cline.accountButtonClicked", () => {
const visibleProvider = ClineProvider.getVisibleInstance()
if (!visibleProvider) {
const visibleWebview = WebviewProvider.getVisibleInstance()
if (!visibleWebview) {
Logger.log("Cannot find any visible Cline instances.")
return
}
visibleProvider.postMessageToWebview({
visibleWebview.controller.postMessageToWebview({
type: "action",
action: "accountButtonClicked",
})
@@ -179,15 +179,15 @@ export function activate(context: vscode.ExtensionContext) {
const path = uri.path
const query = new URLSearchParams(uri.query.replace(/\+/g, "%2B"))
const visibleProvider = ClineProvider.getVisibleInstance()
if (!visibleProvider) {
const visibleWebview = WebviewProvider.getVisibleInstance()
if (!visibleWebview) {
return
}
switch (path) {
case "/openrouter": {
const code = query.get("code")
if (code) {
await visibleProvider.handleOpenRouterCallback(code)
await visibleWebview?.controller.handleOpenRouterCallback(code)
}
break
}
@@ -203,13 +203,13 @@ export function activate(context: vscode.ExtensionContext) {
})
// Validate state parameter
if (!(await visibleProvider.validateAuthState(state))) {
if (!(await visibleWebview?.controller.validateAuthState(state))) {
vscode.window.showErrorMessage("Invalid auth state")
return
}
if (token && apiKey) {
await visibleProvider.handleAuthCallback(token, apiKey)
await visibleWebview?.controller.handleAuthCallback(token, apiKey)
}
break
}
@@ -224,7 +224,7 @@ export function activate(context: vscode.ExtensionContext) {
// Use dynamic import to avoid loading the module in production
import("./dev/commands/tasks")
.then((module) => {
const devTaskCommands = module.registerTaskCommands(context, sidebarProvider)
const devTaskCommands = module.registerTaskCommands(context, sidebarWebview.controller)
context.subscriptions.push(...devTaskCommands)
Logger.log("Cline dev task commands registered")
})
@@ -253,8 +253,8 @@ export function activate(context: vscode.ExtensionContext) {
const filePath = editor.document.uri.fsPath
const languageId = editor.document.languageId
// Send to sidebar provider
await sidebarProvider.addSelectedCodeToChat(
const visibleWebview = WebviewProvider.getVisibleInstance()
await visibleWebview?.controller.addSelectedCodeToChat(
selectedText,
filePath,
languageId,
@@ -303,7 +303,8 @@ export function activate(context: vscode.ExtensionContext) {
*/
// Send to sidebar provider
await sidebarProvider.addSelectedTerminalOutputToChat(terminalContents, terminal.name)
const visibleWebview = WebviewProvider.getVisibleInstance()
await visibleWebview?.controller.addSelectedTerminalOutputToChat(terminalContents, terminal.name)
} catch (error) {
// Ensure clipboard is restored even if an error occurs
await vscode.env.clipboard.writeText(tempCopyBuffer)
@@ -374,11 +375,12 @@ export function activate(context: vscode.ExtensionContext) {
const languageId = editor.document.languageId
// Send to sidebar provider with diagnostics
await sidebarProvider.fixWithCline(selectedText, filePath, languageId, diagnostics)
const visibleWebview = WebviewProvider.getVisibleInstance()
await visibleWebview?.controller.fixWithCline(selectedText, filePath, languageId, diagnostics)
}),
)
return createClineAPI(outputChannel, sidebarProvider)
return createClineAPI(outputChannel, sidebarWebview.controller)
}
// This method is called when your extension is deactivated
@@ -3,7 +3,7 @@ import os from "os"
import * as path from "path"
import simpleGit, { SimpleGit } from "simple-git"
import * as vscode from "vscode"
import { ClineProvider } from "../../core/webview/ClineProvider"
import { Controller as ClineProvider } from "../../core/controller"
import { fileExistsAtPath } from "../../utils/fs"
import { globby } from "globby"
@@ -1,18 +1,18 @@
import * as vscode from "vscode"
import * as path from "path"
import { listFiles } from "../../services/glob/list-files"
import { ClineProvider } from "../../core/webview/ClineProvider"
import { Controller } from "../../core/controller"
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
// Note: this is not a drop-in replacement for listFiles at the start of tasks, since that will be done for Desktops when there is no workspace selected
class WorkspaceTracker {
private providerRef: WeakRef<ClineProvider>
private controllerRef: WeakRef<Controller>
private disposables: vscode.Disposable[] = []
private filePaths: Set<string> = new Set()
constructor(provider: ClineProvider) {
this.providerRef = new WeakRef(provider)
constructor(controller: Controller) {
this.controllerRef = new WeakRef(controller)
this.registerListeners()
}
@@ -85,7 +85,7 @@ class WorkspaceTracker {
if (!cwd) {
return
}
this.providerRef.deref()?.postMessageToWebview({
this.controllerRef.deref()?.postMessageToWebview({
type: "workspaceUpdated",
filePaths: Array.from(this.filePaths).map((file) => {
const relativePath = path.relative(cwd, file).toPosix()
+8 -8
View File
@@ -1,20 +1,20 @@
import axios, { AxiosRequestConfig, AxiosResponse } from "axios"
import { ClineProvider } from "../../core/webview/ClineProvider"
import { Controller } from "../../core/controller"
import type { BalanceResponse, PaymentTransaction, UsageTransaction } from "../../shared/ClineAccount"
export class ClineAccountService {
private readonly baseUrl = "https://api.cline.bot/v1"
private providerRef: WeakRef<ClineProvider>
private controllerRef: WeakRef<Controller>
constructor(provider: ClineProvider) {
this.providerRef = new WeakRef(provider)
constructor(controller: Controller) {
this.controllerRef = new WeakRef(controller)
}
/**
* Get the user's Cline Account key from the apiConfiguration
*/
private async getClineApiKey(): Promise<string | undefined> {
const provider = this.providerRef.deref()
const provider = this.controllerRef.deref()
if (!provider) {
return undefined
}
@@ -64,7 +64,7 @@ export class ClineAccountService {
const data = await this.authenticatedRequest<BalanceResponse>("/user/credits/balance")
// Post to webview
await this.providerRef.deref()?.postMessageToWebview({
await this.controllerRef.deref()?.postMessageToWebview({
type: "userCreditsBalance",
userCreditsBalance: data,
})
@@ -84,7 +84,7 @@ export class ClineAccountService {
const data = await this.authenticatedRequest<UsageTransaction[]>("/user/credits/usage")
// Post to webview
await this.providerRef.deref()?.postMessageToWebview({
await this.controllerRef.deref()?.postMessageToWebview({
type: "userCreditsUsage",
userCreditsUsage: data,
})
@@ -104,7 +104,7 @@ export class ClineAccountService {
const data = await this.authenticatedRequest<PaymentTransaction[]>("/user/credits/payments")
// Post to webview
await this.providerRef.deref()?.postMessageToWebview({
await this.controllerRef.deref()?.postMessageToWebview({
type: "userCreditsPayments",
userCreditsPayments: data,
})
+9 -9
View File
@@ -14,7 +14,7 @@ import * as fs from "fs/promises"
import * as path from "path"
import * as vscode from "vscode"
import { z } from "zod"
import { ClineProvider } from "../../core/webview/ClineProvider"
import { Controller } from "../../core/controller"
import {
DEFAULT_MCP_TIMEOUT_SECONDS,
McpMode,
@@ -76,15 +76,15 @@ const McpSettingsSchema = z.object({
})
export class McpHub {
private providerRef: WeakRef<ClineProvider>
private controllerRef: WeakRef<Controller>
private disposables: vscode.Disposable[] = []
private settingsWatcher?: vscode.FileSystemWatcher
private fileWatchers: Map<string, FSWatcher> = new Map()
connections: McpConnection[] = []
isConnecting: boolean = false
constructor(provider: ClineProvider) {
this.providerRef = new WeakRef(provider)
constructor(controller: Controller) {
this.controllerRef = new WeakRef(controller)
this.watchMcpSettingsFile()
this.initializeMcpServers()
}
@@ -99,7 +99,7 @@ export class McpHub {
}
async getMcpServersPath(): Promise<string> {
const provider = this.providerRef.deref()
const provider = this.controllerRef.deref()
if (!provider) {
throw new Error("Provider not available")
}
@@ -108,7 +108,7 @@ export class McpHub {
}
async getMcpSettingsFilePath(): Promise<string> {
const provider = this.providerRef.deref()
const provider = this.controllerRef.deref()
if (!provider) {
throw new Error("Provider not available")
}
@@ -197,7 +197,7 @@ export class McpHub {
const client = new Client(
{
name: "Cline",
version: this.providerRef.deref()?.context.extension?.packageJSON?.version ?? "1.0.0",
version: this.controllerRef.deref()?.context.extension?.packageJSON?.version ?? "1.0.0",
},
{
capabilities: {},
@@ -446,7 +446,7 @@ export class McpHub {
async restartConnection(serverName: string): Promise<void> {
this.isConnecting = true
const provider = this.providerRef.deref()
const provider = this.controllerRef.deref()
if (!provider) {
return
}
@@ -481,7 +481,7 @@ export class McpHub {
const content = await fs.readFile(settingsPath, "utf-8")
const config = JSON.parse(content)
const serverOrder = Object.keys(config.mcpServers || {})
await this.providerRef.deref()?.postMessageToWebview({
await this.controllerRef.deref()?.postMessageToWebview({
type: "mcpServers",
mcpServers: [...this.connections]
.sort((a, b) => {