From 73fe581868e216c8c687f531200e7ada3157e255 Mon Sep 17 00:00:00 2001 From: Diarmid Mackenzie Date: Mon, 31 Mar 2025 14:47:28 +0100 Subject: [PATCH 01/13] Split webviewMessageHandler out of ClineProvider.ts (#2149) --- src/core/webview/ClineProvider.ts | 1471 +-------------------- src/core/webview/webviewMessageHandler.ts | 1410 ++++++++++++++++++++ 2 files changed, 1444 insertions(+), 1437 deletions(-) create mode 100644 src/core/webview/webviewMessageHandler.ts diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index f4e25f298d..a7fc450b0e 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -9,15 +9,8 @@ import axios from "axios" import pWaitFor from "p-wait-for" import * as vscode from "vscode" -import { - CheckpointStorage, - GlobalState, - Language, - ProviderSettings, - RooCodeSettings, - ApiConfigMeta, -} from "../../schemas" -import { changeLanguage, t } from "../../i18n" +import { GlobalState, ProviderSettings, RooCodeSettings } from "../../schemas" +import { t } from "../../i18n" import { setPanel } from "../../activate/registerCommands" import { ApiConfiguration, @@ -35,51 +28,31 @@ import { supportPrompt } from "../../shared/support-prompt" import { GlobalFileNames } from "../../shared/globalFileNames" import { HistoryItem } from "../../shared/HistoryItem" import { ExtensionMessage } from "../../shared/ExtensionMessage" -import { checkoutDiffPayloadSchema, checkoutRestorePayloadSchema, WebviewMessage } from "../../shared/WebviewMessage" import { Mode, PromptComponent, defaultModeSlug, getModeBySlug, getGroupName } from "../../shared/modes" -import { checkExistKey } from "../../shared/checkExistApiConfig" import { EXPERIMENT_IDS, experiments as Experiments, experimentDefault, ExperimentId } from "../../shared/experiments" import { formatLanguage } from "../../shared/language" import { Terminal, TERMINAL_SHELL_INTEGRATION_TIMEOUT } from "../../integrations/terminal/Terminal" import { downloadTask } from "../../integrations/misc/export-markdown" -import { openFile, openImage } from "../../integrations/misc/open-file" -import { selectImages } from "../../integrations/misc/process-images" 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 { BrowserSession } from "../../services/browser/BrowserSession" -import { discoverChromeHostUrl, tryChromeHostUrl } from "../../services/browser/browserDiscovery" -import { searchWorkspaceFiles } from "../../services/search/file-search" import { fileExistsAtPath } from "../../utils/fs" -import { playSound, setSoundEnabled, setSoundVolume } from "../../utils/sound" -import { playTts, setTtsEnabled, setTtsSpeed, stopTts } from "../../utils/tts" -import { singleCompletionHandler } from "../../utils/single-completion-handler" -import { searchCommits } from "../../utils/git" -import { getDiffStrategy } from "../diff/DiffStrategy" -import { SYSTEM_PROMPT } from "../prompts/system" +import { setSoundEnabled } from "../../utils/sound" +import { setTtsEnabled, setTtsSpeed } from "../../utils/tts" import { ContextProxy } from "../config/ContextProxy" import { ProviderSettingsManager } from "../config/ProviderSettingsManager" -import { exportSettings, importSettings } from "../config/importExport" import { CustomModesManager } from "../config/CustomModesManager" import { buildApiHandler } from "../../api" -import { getOpenRouterModels } from "../../api/providers/openrouter" -import { getGlamaModels } from "../../api/providers/glama" -import { getUnboundModels } from "../../api/providers/unbound" -import { getRequestyModels } from "../../api/providers/requesty" -import { getOpenAiModels } from "../../api/providers/openai" -import { getOllamaModels } from "../../api/providers/ollama" -import { getVsCodeLmModels } from "../../api/providers/vscode-lm" -import { getLmStudioModels } from "../../api/providers/lmstudio" import { ACTION_NAMES } from "../CodeActionProvider" import { Cline, ClineOptions } from "../Cline" -import { openMention } from "../mentions" import { getNonce } from "./getNonce" import { getUri } from "./getUri" import { telemetryService } from "../../services/telemetry/TelemetryService" -import { TelemetrySetting } from "../../shared/TelemetrySetting" import { getWorkspacePath } from "../../utils/path" +import { webviewMessageHandler } from "./webviewMessageHandler" +import { WebviewMessage } from "../../shared/WebviewMessage" /** * https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts @@ -95,20 +68,29 @@ export class ClineProvider extends EventEmitter implements public static readonly tabPanelId = "roo-cline.TabPanelProvider" private static activeInstances: Set = new Set() private disposables: vscode.Disposable[] = [] - private view?: vscode.WebviewView | vscode.WebviewPanel - private isViewLaunched = false + // not private, so it can be accessed from webviewMessageHandler + view?: vscode.WebviewView | vscode.WebviewPanel + // not private, so it can be accessed from webviewMessageHandler + // callers could update to get viewLaunched() getter function + isViewLaunched = false private clineStack: Cline[] = [] - private workspaceTracker?: WorkspaceTracker - protected mcpHub?: McpHub // Change from private to protected - private latestAnnouncementId = "mar-30-2025-3-11" // update for v3.11.0 announcement - private settingsImportedAt?: number + // not private, so it can be accessed from webviewMessageHandler + workspaceTracker?: WorkspaceTracker + // not protected, so it can be accessed from webviewMessageHandler. + // Could modify code to use getMcpHub() instead. + mcpHub?: McpHub // Change from private to protected + // not private, so it can be accessed from webviewMessageHandler + latestAnnouncementId = "mar-30-2025-3-11" // update for v3.11.0 announcement + // not private, so it can be accessed from webviewMessageHandler + settingsImportedAt?: number public readonly contextProxy: ContextProxy public readonly providerSettingsManager: ProviderSettingsManager public readonly customModesManager: CustomModesManager constructor( readonly context: vscode.ExtensionContext, - private readonly outputChannel: vscode.OutputChannel, + // not private, so it can be accessed from webviewMessageHandler + readonly outputChannel: vscode.OutputChannel, private readonly renderContext: "sidebar" | "editor" = "sidebar", ) { super() @@ -747,1399 +729,9 @@ export class ClineProvider extends EventEmitter implements * @param webview A reference to the extension webview */ private setWebviewMessageListener(webview: vscode.Webview) { - webview.onDidReceiveMessage( - async (message: WebviewMessage) => { - switch (message.type) { - case "webviewDidLaunch": - // Load custom modes first - const customModes = await this.customModesManager.getCustomModes() - await this.updateGlobalState("customModes", customModes) + const onReceiveMessage = async (message: WebviewMessage) => webviewMessageHandler(this, message) - this.postStateToWebview() - this.workspaceTracker?.initializeFilePaths() // don't await - - getTheme().then((theme) => - this.postMessageToWebview({ type: "theme", text: JSON.stringify(theme) }), - ) - - // If MCP Hub is already initialized, update the webview with current server list - if (this.mcpHub) { - this.postMessageToWebview({ - type: "mcpServers", - mcpServers: this.mcpHub.getAllServers(), - }) - } - - const cacheDir = await this.ensureCacheDirectoryExists() - - // Post last cached models in case the call to endpoint fails. - this.readModelsFromCache(GlobalFileNames.openRouterModels).then((openRouterModels) => { - if (openRouterModels) { - this.postMessageToWebview({ type: "openRouterModels", openRouterModels }) - } - }) - - // GUI relies on model info to be up-to-date to provide - // the most accurate pricing, so we need to fetch the - // latest details on launch. - // We do this for all users since many users switch - // between api providers and if they were to switch back - // to OpenRouter it would be showing outdated model info - // if we hadn't retrieved the latest at this point - // (see normalizeApiConfiguration > openrouter). - const { apiConfiguration: currentApiConfig } = await this.getState() - getOpenRouterModels(currentApiConfig).then(async (openRouterModels) => { - if (Object.keys(openRouterModels).length > 0) { - await fs.writeFile( - path.join(cacheDir, GlobalFileNames.openRouterModels), - JSON.stringify(openRouterModels), - ) - await this.postMessageToWebview({ type: "openRouterModels", openRouterModels }) - - // Update model info in state (this needs to be - // done here since we don't want to update state - // while settings is open, and we may refresh - // models there). - const { apiConfiguration } = await this.getState() - - if (apiConfiguration.openRouterModelId) { - await this.updateGlobalState( - "openRouterModelInfo", - openRouterModels[apiConfiguration.openRouterModelId], - ) - await this.postStateToWebview() - } - } - }) - - this.readModelsFromCache(GlobalFileNames.glamaModels).then((glamaModels) => { - if (glamaModels) { - this.postMessageToWebview({ type: "glamaModels", glamaModels }) - } - }) - - getGlamaModels().then(async (glamaModels) => { - if (Object.keys(glamaModels).length > 0) { - await fs.writeFile( - path.join(cacheDir, GlobalFileNames.glamaModels), - JSON.stringify(glamaModels), - ) - await this.postMessageToWebview({ type: "glamaModels", glamaModels }) - - const { apiConfiguration } = await this.getState() - - if (apiConfiguration.glamaModelId) { - await this.updateGlobalState( - "glamaModelInfo", - glamaModels[apiConfiguration.glamaModelId], - ) - await this.postStateToWebview() - } - } - }) - - this.readModelsFromCache(GlobalFileNames.unboundModels).then((unboundModels) => { - if (unboundModels) { - this.postMessageToWebview({ type: "unboundModels", unboundModels }) - } - }) - - getUnboundModels().then(async (unboundModels) => { - if (Object.keys(unboundModels).length > 0) { - await fs.writeFile( - path.join(cacheDir, GlobalFileNames.unboundModels), - JSON.stringify(unboundModels), - ) - await this.postMessageToWebview({ type: "unboundModels", unboundModels }) - - const { apiConfiguration } = await this.getState() - - if (apiConfiguration?.unboundModelId) { - await this.updateGlobalState( - "unboundModelInfo", - unboundModels[apiConfiguration.unboundModelId], - ) - await this.postStateToWebview() - } - } - }) - - this.readModelsFromCache(GlobalFileNames.requestyModels).then((requestyModels) => { - if (requestyModels) { - this.postMessageToWebview({ type: "requestyModels", requestyModels }) - } - }) - - getRequestyModels().then(async (requestyModels) => { - if (Object.keys(requestyModels).length > 0) { - await fs.writeFile( - path.join(cacheDir, GlobalFileNames.requestyModels), - JSON.stringify(requestyModels), - ) - await this.postMessageToWebview({ type: "requestyModels", requestyModels }) - - const { apiConfiguration } = await this.getState() - - if (apiConfiguration.requestyModelId) { - await this.updateGlobalState( - "requestyModelInfo", - requestyModels[apiConfiguration.requestyModelId], - ) - await this.postStateToWebview() - } - } - }) - - this.providerSettingsManager - .listConfig() - .then(async (listApiConfig) => { - if (!listApiConfig) { - return - } - - if (listApiConfig.length === 1) { - // Check if first time init then sync with exist config. - if (!checkExistKey(listApiConfig[0])) { - const { apiConfiguration } = await this.getState() - - await this.providerSettingsManager.saveConfig( - listApiConfig[0].name ?? "default", - apiConfiguration, - ) - - listApiConfig[0].apiProvider = apiConfiguration.apiProvider - } - } - - const currentConfigName = this.getGlobalState("currentApiConfigName") - - if (currentConfigName) { - if (!(await this.providerSettingsManager.hasConfig(currentConfigName))) { - // current config name not valid, get first config in list - await this.updateGlobalState("currentApiConfigName", listApiConfig?.[0]?.name) - if (listApiConfig?.[0]?.name) { - const apiConfig = await this.providerSettingsManager.loadConfig( - listApiConfig?.[0]?.name, - ) - - await Promise.all([ - this.updateGlobalState("listApiConfigMeta", listApiConfig), - this.postMessageToWebview({ type: "listApiConfig", listApiConfig }), - this.updateApiConfiguration(apiConfig), - ]) - await this.postStateToWebview() - return - } - } - } - - await Promise.all([ - await this.updateGlobalState("listApiConfigMeta", listApiConfig), - await this.postMessageToWebview({ type: "listApiConfig", listApiConfig }), - ]) - }) - .catch((error) => - this.outputChannel.appendLine( - `Error list api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, - ), - ) - - // If user already opted in to telemetry, enable telemetry service - this.getStateToPostToWebview().then((state) => { - const { telemetrySetting } = state - const isOptedIn = telemetrySetting === "enabled" - telemetryService.updateTelemetryState(isOptedIn) - }) - - this.isViewLaunched = true - break - case "newTask": - // Code that should run in response to the hello message command - //vscode.window.showInformationMessage(message.text!) - - // Send a message to our webview. - // You can send any JSON serializable data. - // Could also do this in extension .ts - //this.postMessageToWebview({ type: "text", text: `Extension: ${Date.now()}` }) - // initializing new instance of Cline will make sure that any agentically running promises in old instance don't affect our new task. this essentially creates a fresh slate for the new task - await this.initClineWithTask(message.text, message.images) - break - case "apiConfiguration": - if (message.apiConfiguration) { - await this.updateApiConfiguration(message.apiConfiguration) - } - await this.postStateToWebview() - break - case "customInstructions": - await this.updateCustomInstructions(message.text) - break - case "alwaysAllowReadOnly": - await this.updateGlobalState("alwaysAllowReadOnly", message.bool ?? undefined) - await this.postStateToWebview() - break - case "alwaysAllowReadOnlyOutsideWorkspace": - await this.updateGlobalState("alwaysAllowReadOnlyOutsideWorkspace", message.bool ?? undefined) - await this.postStateToWebview() - break - case "alwaysAllowWrite": - await this.updateGlobalState("alwaysAllowWrite", message.bool ?? undefined) - await this.postStateToWebview() - break - case "alwaysAllowWriteOutsideWorkspace": - await this.updateGlobalState("alwaysAllowWriteOutsideWorkspace", message.bool ?? undefined) - await this.postStateToWebview() - break - case "alwaysAllowExecute": - await this.updateGlobalState("alwaysAllowExecute", message.bool ?? undefined) - await this.postStateToWebview() - break - case "alwaysAllowBrowser": - await this.updateGlobalState("alwaysAllowBrowser", message.bool ?? undefined) - await this.postStateToWebview() - break - case "alwaysAllowMcp": - await this.updateGlobalState("alwaysAllowMcp", message.bool) - await this.postStateToWebview() - break - case "alwaysAllowModeSwitch": - await this.updateGlobalState("alwaysAllowModeSwitch", message.bool) - await this.postStateToWebview() - break - case "alwaysAllowSubtasks": - await this.updateGlobalState("alwaysAllowSubtasks", message.bool) - await this.postStateToWebview() - break - case "askResponse": - this.getCurrentCline()?.handleWebviewAskResponse( - message.askResponse!, - message.text, - message.images, - ) - break - case "clearTask": - // clear task resets the current session and allows for a new task to be started, if this session is a subtask - it allows the parent task to be resumed - await this.finishSubTask(t("common:tasks.canceled")) - await this.postStateToWebview() - break - case "didShowAnnouncement": - await this.updateGlobalState("lastShownAnnouncementId", this.latestAnnouncementId) - await this.postStateToWebview() - break - case "selectImages": - const images = await selectImages() - await this.postMessageToWebview({ type: "selectedImages", images }) - break - case "exportCurrentTask": - const currentTaskId = this.getCurrentCline()?.taskId - if (currentTaskId) { - this.exportTaskWithId(currentTaskId) - } - break - case "showTaskWithId": - this.showTaskWithId(message.text!) - break - case "deleteTaskWithId": - this.deleteTaskWithId(message.text!) - break - case "deleteMultipleTasksWithIds": { - const ids = message.ids - - if (Array.isArray(ids)) { - // Process in batches of 20 (or another reasonable number) - const batchSize = 20 - const results = [] - - // Only log start and end of the operation - console.log(`Batch deletion started: ${ids.length} tasks total`) - - for (let i = 0; i < ids.length; i += batchSize) { - const batch = ids.slice(i, i + batchSize) - - const batchPromises = batch.map(async (id) => { - try { - await this.deleteTaskWithId(id) - return { id, success: true } - } catch (error) { - // Keep error logging for debugging purposes - console.log( - `Failed to delete task ${id}: ${error instanceof Error ? error.message : String(error)}`, - ) - return { id, success: false } - } - }) - - // Process each batch in parallel but wait for completion before starting the next batch - const batchResults = await Promise.all(batchPromises) - results.push(...batchResults) - - // Update the UI after each batch to show progress - await this.postStateToWebview() - } - - // Log final results - const successCount = results.filter((r) => r.success).length - const failCount = results.length - successCount - console.log( - `Batch deletion completed: ${successCount}/${ids.length} tasks successful, ${failCount} tasks failed`, - ) - } - break - } - case "exportTaskWithId": - this.exportTaskWithId(message.text!) - break - case "importSettings": - const { success } = await importSettings({ - providerSettingsManager: this.providerSettingsManager, - contextProxy: this.contextProxy, - }) - - if (success) { - this.settingsImportedAt = Date.now() - await this.postStateToWebview() - await vscode.window.showInformationMessage(t("common:info.settings_imported")) - } - - break - case "exportSettings": - await exportSettings({ - providerSettingsManager: this.providerSettingsManager, - contextProxy: this.contextProxy, - }) - - break - case "resetState": - await this.resetState() - break - case "refreshOpenRouterModels": { - const { apiConfiguration: configForRefresh } = await this.getState() - const openRouterModels = await getOpenRouterModels(configForRefresh) - - if (Object.keys(openRouterModels).length > 0) { - const cacheDir = await this.ensureCacheDirectoryExists() - await fs.writeFile( - path.join(cacheDir, GlobalFileNames.openRouterModels), - JSON.stringify(openRouterModels), - ) - await this.postMessageToWebview({ type: "openRouterModels", openRouterModels }) - } - - break - } - case "refreshGlamaModels": - const glamaModels = await getGlamaModels() - - if (Object.keys(glamaModels).length > 0) { - const cacheDir = await this.ensureCacheDirectoryExists() - await fs.writeFile( - path.join(cacheDir, GlobalFileNames.glamaModels), - JSON.stringify(glamaModels), - ) - await this.postMessageToWebview({ type: "glamaModels", glamaModels }) - } - - break - case "refreshUnboundModels": - const unboundModels = await getUnboundModels() - - if (Object.keys(unboundModels).length > 0) { - const cacheDir = await this.ensureCacheDirectoryExists() - await fs.writeFile( - path.join(cacheDir, GlobalFileNames.unboundModels), - JSON.stringify(unboundModels), - ) - await this.postMessageToWebview({ type: "unboundModels", unboundModels }) - } - - break - case "refreshRequestyModels": - const requestyModels = await getRequestyModels() - - if (Object.keys(requestyModels).length > 0) { - const cacheDir = await this.ensureCacheDirectoryExists() - await fs.writeFile( - path.join(cacheDir, GlobalFileNames.requestyModels), - JSON.stringify(requestyModels), - ) - await this.postMessageToWebview({ type: "requestyModels", requestyModels }) - } - - break - case "refreshOpenAiModels": - if (message?.values?.baseUrl && message?.values?.apiKey) { - const openAiModels = await getOpenAiModels( - message?.values?.baseUrl, - message?.values?.apiKey, - ) - this.postMessageToWebview({ type: "openAiModels", openAiModels }) - } - - break - case "requestOllamaModels": - const ollamaModels = await getOllamaModels(message.text) - // TODO: Cache like we do for OpenRouter, etc? - this.postMessageToWebview({ type: "ollamaModels", ollamaModels }) - break - case "requestLmStudioModels": - const lmStudioModels = await getLmStudioModels(message.text) - // TODO: Cache like we do for OpenRouter, etc? - this.postMessageToWebview({ type: "lmStudioModels", lmStudioModels }) - break - case "requestVsCodeLmModels": - const vsCodeLmModels = await getVsCodeLmModels() - // TODO: Cache like we do for OpenRouter, etc? - this.postMessageToWebview({ type: "vsCodeLmModels", vsCodeLmModels }) - break - case "openImage": - openImage(message.text!) - break - case "openFile": - openFile(message.text!, message.values as { create?: boolean; content?: string }) - break - case "openMention": - { - const { osInfo } = (await this.getState()) || {} - openMention(message.text, osInfo) - } - break - case "checkpointDiff": - const result = checkoutDiffPayloadSchema.safeParse(message.payload) - - if (result.success) { - await this.getCurrentCline()?.checkpointDiff(result.data) - } - - break - case "checkpointRestore": { - const result = checkoutRestorePayloadSchema.safeParse(message.payload) - - if (result.success) { - await this.cancelTask() - - try { - await pWaitFor(() => this.getCurrentCline()?.isInitialized === true, { timeout: 3_000 }) - } catch (error) { - vscode.window.showErrorMessage(t("common:errors.checkpoint_timeout")) - } - - try { - await this.getCurrentCline()?.checkpointRestore(result.data) - } catch (error) { - vscode.window.showErrorMessage(t("common:errors.checkpoint_failed")) - } - } - - break - } - case "cancelTask": - await this.cancelTask() - break - case "allowedCommands": - await this.context.globalState.update("allowedCommands", message.commands) - // Also update workspace settings - await vscode.workspace - .getConfiguration("roo-cline") - .update("allowedCommands", message.commands, vscode.ConfigurationTarget.Global) - break - case "openMcpSettings": { - const mcpSettingsFilePath = await this.mcpHub?.getMcpSettingsFilePath() - if (mcpSettingsFilePath) { - openFile(mcpSettingsFilePath) - } - break - } - case "openProjectMcpSettings": { - if (!vscode.workspace.workspaceFolders?.length) { - vscode.window.showErrorMessage(t("common:errors.no_workspace")) - return - } - - const workspaceFolder = vscode.workspace.workspaceFolders[0] - const rooDir = path.join(workspaceFolder.uri.fsPath, ".roo") - const mcpPath = path.join(rooDir, "mcp.json") - - try { - await fs.mkdir(rooDir, { recursive: true }) - const exists = await fileExistsAtPath(mcpPath) - if (!exists) { - await fs.writeFile(mcpPath, JSON.stringify({ mcpServers: {} }, null, 2)) - } - await openFile(mcpPath) - } catch (error) { - vscode.window.showErrorMessage(t("common:errors.create_mcp_json", { error: `${error}` })) - } - break - } - case "openCustomModesSettings": { - const customModesFilePath = await this.customModesManager.getCustomModesFilePath() - if (customModesFilePath) { - openFile(customModesFilePath) - } - break - } - case "deleteMcpServer": { - if (!message.serverName) { - break - } - - try { - this.outputChannel.appendLine(`Attempting to delete MCP server: ${message.serverName}`) - await this.mcpHub?.deleteServer(message.serverName, message.source as "global" | "project") - this.outputChannel.appendLine(`Successfully deleted MCP server: ${message.serverName}`) - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error) - this.outputChannel.appendLine(`Failed to delete MCP server: ${errorMessage}`) - // Error messages are already handled by McpHub.deleteServer - } - break - } - case "restartMcpServer": { - try { - await this.mcpHub?.restartConnection(message.text!, message.source as "global" | "project") - } catch (error) { - this.outputChannel.appendLine( - `Failed to retry connection for ${message.text}: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, - ) - } - break - } - case "toggleToolAlwaysAllow": { - try { - if (this.mcpHub) { - await this.mcpHub.toggleToolAlwaysAllow( - message.serverName!, - message.source as "global" | "project", - message.toolName!, - Boolean(message.alwaysAllow), - ) - } - } catch (error) { - this.outputChannel.appendLine( - `Failed to toggle auto-approve for tool ${message.toolName}: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, - ) - } - break - } - case "toggleMcpServer": { - try { - await this.mcpHub?.toggleServerDisabled( - message.serverName!, - message.disabled!, - message.source as "global" | "project", - ) - } catch (error) { - this.outputChannel.appendLine( - `Failed to toggle MCP server ${message.serverName}: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, - ) - } - break - } - case "mcpEnabled": - const mcpEnabled = message.bool ?? true - await this.updateGlobalState("mcpEnabled", mcpEnabled) - await this.postStateToWebview() - break - case "enableMcpServerCreation": - await this.updateGlobalState("enableMcpServerCreation", message.bool ?? true) - await this.postStateToWebview() - break - case "playSound": - if (message.audioType) { - const soundPath = path.join(this.context.extensionPath, "audio", `${message.audioType}.wav`) - playSound(soundPath) - } - break - case "soundEnabled": - const soundEnabled = message.bool ?? true - await this.updateGlobalState("soundEnabled", soundEnabled) - setSoundEnabled(soundEnabled) // Add this line to update the sound utility - await this.postStateToWebview() - break - case "soundVolume": - const soundVolume = message.value ?? 0.5 - await this.updateGlobalState("soundVolume", soundVolume) - setSoundVolume(soundVolume) - await this.postStateToWebview() - break - case "ttsEnabled": - const ttsEnabled = message.bool ?? true - await this.updateGlobalState("ttsEnabled", ttsEnabled) - setTtsEnabled(ttsEnabled) // Add this line to update the tts utility - await this.postStateToWebview() - break - case "ttsSpeed": - const ttsSpeed = message.value ?? 1.0 - await this.updateGlobalState("ttsSpeed", ttsSpeed) - setTtsSpeed(ttsSpeed) - await this.postStateToWebview() - break - case "playTts": - if (message.text) { - playTts(message.text, { - onStart: () => this.postMessageToWebview({ type: "ttsStart", text: message.text }), - onStop: () => this.postMessageToWebview({ type: "ttsStop", text: message.text }), - }) - } - break - case "stopTts": - stopTts() - break - case "diffEnabled": - const diffEnabled = message.bool ?? true - await this.updateGlobalState("diffEnabled", diffEnabled) - await this.postStateToWebview() - break - case "enableCheckpoints": - const enableCheckpoints = message.bool ?? true - await this.updateGlobalState("enableCheckpoints", enableCheckpoints) - await this.postStateToWebview() - break - case "checkpointStorage": - console.log(`[ClineProvider] checkpointStorage: ${message.text}`) - const checkpointStorage = message.text ?? "task" - await this.updateGlobalState("checkpointStorage", checkpointStorage as CheckpointStorage) - await this.postStateToWebview() - break - case "browserViewportSize": - const browserViewportSize = message.text ?? "900x600" - await this.updateGlobalState("browserViewportSize", browserViewportSize) - await this.postStateToWebview() - break - case "remoteBrowserHost": - await this.updateGlobalState("remoteBrowserHost", message.text) - await this.postStateToWebview() - break - case "remoteBrowserEnabled": - // Store the preference in global state - // remoteBrowserEnabled now means "enable remote browser connection" - await this.updateGlobalState("remoteBrowserEnabled", message.bool ?? false) - // If disabling remote browser connection, clear the remoteBrowserHost - if (!message.bool) { - await this.updateGlobalState("remoteBrowserHost", undefined) - } - await this.postStateToWebview() - break - case "testBrowserConnection": - // If no text is provided, try auto-discovery - if (!message.text) { - // Use testBrowserConnection for auto-discovery - const chromeHostUrl = await discoverChromeHostUrl() - if (chromeHostUrl) { - // Send the result back to the webview - await this.postMessageToWebview({ - type: "browserConnectionResult", - success: !!chromeHostUrl, - text: `Auto-discovered and tested connection to Chrome: ${chromeHostUrl}`, - values: { endpoint: chromeHostUrl }, - }) - } else { - await this.postMessageToWebview({ - type: "browserConnectionResult", - success: false, - text: "No Chrome instances found on the network. Make sure Chrome is running with remote debugging enabled (--remote-debugging-port=9222).", - }) - } - } else { - // Test the provided URL - const customHostUrl = message.text - const hostIsValid = await tryChromeHostUrl(message.text) - // Send the result back to the webview - await this.postMessageToWebview({ - type: "browserConnectionResult", - success: hostIsValid, - text: hostIsValid - ? `Successfully connected to Chrome: ${customHostUrl}` - : "Failed to connect to Chrome", - }) - } - break - case "fuzzyMatchThreshold": - await this.updateGlobalState("fuzzyMatchThreshold", message.value) - await this.postStateToWebview() - break - case "alwaysApproveResubmit": - await this.updateGlobalState("alwaysApproveResubmit", message.bool ?? false) - await this.postStateToWebview() - break - case "requestDelaySeconds": - await this.updateGlobalState("requestDelaySeconds", message.value ?? 5) - await this.postStateToWebview() - break - case "rateLimitSeconds": - await this.updateGlobalState("rateLimitSeconds", message.value ?? 0) - await this.postStateToWebview() - break - case "writeDelayMs": - await this.updateGlobalState("writeDelayMs", message.value) - await this.postStateToWebview() - break - case "terminalOutputLineLimit": - await this.updateGlobalState("terminalOutputLineLimit", message.value) - await this.postStateToWebview() - break - case "terminalShellIntegrationTimeout": - await this.updateGlobalState("terminalShellIntegrationTimeout", message.value) - await this.postStateToWebview() - if (message.value !== undefined) { - Terminal.setShellIntegrationTimeout(message.value) - } - break - case "mode": - await this.handleModeSwitch(message.text as Mode) - break - case "updateSupportPrompt": - try { - if (Object.keys(message?.values ?? {}).length === 0) { - return - } - - const existingPrompts = this.getGlobalState("customSupportPrompts") ?? {} - const updatedPrompts = { ...existingPrompts, ...message.values } - await this.updateGlobalState("customSupportPrompts", updatedPrompts) - await this.postStateToWebview() - } catch (error) { - this.outputChannel.appendLine( - `Error update support prompt: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, - ) - vscode.window.showErrorMessage(t("common:errors.update_support_prompt")) - } - break - case "resetSupportPrompt": - try { - if (!message?.text) { - return - } - - const existingPrompts = this.getGlobalState("customSupportPrompts") ?? {} - const updatedPrompts = { ...existingPrompts } - updatedPrompts[message.text] = undefined - await this.updateGlobalState("customSupportPrompts", updatedPrompts) - await this.postStateToWebview() - } catch (error) { - this.outputChannel.appendLine( - `Error reset support prompt: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, - ) - vscode.window.showErrorMessage(t("common:errors.reset_support_prompt")) - } - break - case "updatePrompt": - if (message.promptMode && message.customPrompt !== undefined) { - const existingPrompts = this.getGlobalState("customModePrompts") ?? {} - const updatedPrompts = { ...existingPrompts, [message.promptMode]: message.customPrompt } - await this.updateGlobalState("customModePrompts", updatedPrompts) - const currentState = await this.getState() - const stateWithPrompts = { ...currentState, customModePrompts: updatedPrompts } - this.view?.webview.postMessage({ type: "state", state: stateWithPrompts }) - } - break - case "deleteMessage": { - const answer = await vscode.window.showInformationMessage( - t("common:confirmation.delete_message"), - { modal: true }, - t("common:confirmation.just_this_message"), - t("common:confirmation.this_and_subsequent"), - ) - - if ( - (answer === t("common:confirmation.just_this_message") || - answer === t("common:confirmation.this_and_subsequent")) && - this.getCurrentCline() && - typeof message.value === "number" && - message.value - ) { - const timeCutoff = message.value - 1000 // 1 second buffer before the message to delete - - const messageIndex = this.getCurrentCline()!.clineMessages.findIndex( - (msg) => msg.ts && msg.ts >= timeCutoff, - ) - - const apiConversationHistoryIndex = - this.getCurrentCline()?.apiConversationHistory.findIndex( - (msg) => msg.ts && msg.ts >= timeCutoff, - ) - - if (messageIndex !== -1) { - const { historyItem } = await this.getTaskWithId(this.getCurrentCline()!.taskId) - - if (answer === t("common:confirmation.just_this_message")) { - // Find the next user message first - const nextUserMessage = this.getCurrentCline()! - .clineMessages.slice(messageIndex + 1) - .find((msg) => msg.type === "say" && msg.say === "user_feedback") - - // Handle UI messages - if (nextUserMessage) { - // Find absolute index of next user message - const nextUserMessageIndex = this.getCurrentCline()!.clineMessages.findIndex( - (msg) => msg === nextUserMessage, - ) - - // Keep messages before current message and after next user message - await this.getCurrentCline()!.overwriteClineMessages([ - ...this.getCurrentCline()!.clineMessages.slice(0, messageIndex), - ...this.getCurrentCline()!.clineMessages.slice(nextUserMessageIndex), - ]) - } else { - // If no next user message, keep only messages before current message - await this.getCurrentCline()!.overwriteClineMessages( - this.getCurrentCline()!.clineMessages.slice(0, messageIndex), - ) - } - - // Handle API messages - if (apiConversationHistoryIndex !== -1) { - if (nextUserMessage && nextUserMessage.ts) { - // Keep messages before current API message and after next user message - await this.getCurrentCline()!.overwriteApiConversationHistory([ - ...this.getCurrentCline()!.apiConversationHistory.slice( - 0, - apiConversationHistoryIndex, - ), - ...this.getCurrentCline()!.apiConversationHistory.filter( - (msg) => msg.ts && msg.ts >= nextUserMessage.ts, - ), - ]) - } else { - // If no next user message, keep only messages before current API message - await this.getCurrentCline()!.overwriteApiConversationHistory( - this.getCurrentCline()!.apiConversationHistory.slice( - 0, - apiConversationHistoryIndex, - ), - ) - } - } - } else if (answer === t("common:confirmation.this_and_subsequent")) { - // Delete this message and all that follow - await this.getCurrentCline()!.overwriteClineMessages( - this.getCurrentCline()!.clineMessages.slice(0, messageIndex), - ) - if (apiConversationHistoryIndex !== -1) { - await this.getCurrentCline()!.overwriteApiConversationHistory( - this.getCurrentCline()!.apiConversationHistory.slice( - 0, - apiConversationHistoryIndex, - ), - ) - } - } - - await this.initClineWithHistoryItem(historyItem) - } - } - break - } - case "screenshotQuality": - await this.updateGlobalState("screenshotQuality", message.value) - await this.postStateToWebview() - break - case "maxOpenTabsContext": - const tabCount = Math.min(Math.max(0, message.value ?? 20), 500) - await this.updateGlobalState("maxOpenTabsContext", tabCount) - await this.postStateToWebview() - break - case "maxWorkspaceFiles": - const fileCount = Math.min(Math.max(0, message.value ?? 200), 500) - await this.updateGlobalState("maxWorkspaceFiles", fileCount) - await this.postStateToWebview() - break - case "browserToolEnabled": - await this.updateGlobalState("browserToolEnabled", message.bool ?? true) - await this.postStateToWebview() - break - case "language": - changeLanguage(message.text ?? "en") - await this.updateGlobalState("language", message.text as Language) - await this.postStateToWebview() - break - case "showRooIgnoredFiles": - await this.updateGlobalState("showRooIgnoredFiles", message.bool ?? true) - await this.postStateToWebview() - break - case "maxReadFileLine": - await this.updateGlobalState("maxReadFileLine", message.value) - await this.postStateToWebview() - break - case "toggleApiConfigPin": - if (message.text) { - const currentPinned = this.getGlobalState("pinnedApiConfigs") ?? {} - const updatedPinned: Record = { ...currentPinned } - - if (currentPinned[message.text]) { - delete updatedPinned[message.text] - } else { - updatedPinned[message.text] = true - } - - await this.updateGlobalState("pinnedApiConfigs", updatedPinned) - await this.postStateToWebview() - } - break - case "enhancementApiConfigId": - await this.updateGlobalState("enhancementApiConfigId", message.text) - await this.postStateToWebview() - break - case "autoApprovalEnabled": - await this.updateGlobalState("autoApprovalEnabled", message.bool ?? false) - await this.postStateToWebview() - break - case "enhancePrompt": - if (message.text) { - try { - const { - apiConfiguration, - customSupportPrompts, - listApiConfigMeta, - enhancementApiConfigId, - } = await this.getState() - - // Try to get enhancement config first, fall back to current config - let configToUse: ApiConfiguration = apiConfiguration - if (enhancementApiConfigId) { - const config = listApiConfigMeta?.find( - (c: ApiConfigMeta) => c.id === enhancementApiConfigId, - ) - if (config?.name) { - const loadedConfig = await this.providerSettingsManager.loadConfig(config.name) - if (loadedConfig.apiProvider) { - configToUse = loadedConfig - } - } - } - - const enhancedPrompt = await singleCompletionHandler( - configToUse, - supportPrompt.create( - "ENHANCE", - { - userInput: message.text, - }, - customSupportPrompts, - ), - ) - - await this.postMessageToWebview({ - type: "enhancedPrompt", - text: enhancedPrompt, - }) - } catch (error) { - this.outputChannel.appendLine( - `Error enhancing prompt: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, - ) - vscode.window.showErrorMessage(t("common:errors.enhance_prompt")) - await this.postMessageToWebview({ - type: "enhancedPrompt", - }) - } - } - break - case "getSystemPrompt": - try { - const systemPrompt = await generateSystemPrompt(message) - - await this.postMessageToWebview({ - type: "systemPrompt", - text: systemPrompt, - mode: message.mode, - }) - } catch (error) { - this.outputChannel.appendLine( - `Error getting system prompt: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, - ) - vscode.window.showErrorMessage(t("common:errors.get_system_prompt")) - } - break - case "copySystemPrompt": - try { - const systemPrompt = await generateSystemPrompt(message) - - await vscode.env.clipboard.writeText(systemPrompt) - await vscode.window.showInformationMessage(t("common:info.clipboard_copy")) - } catch (error) { - this.outputChannel.appendLine( - `Error getting system prompt: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, - ) - vscode.window.showErrorMessage(t("common:errors.get_system_prompt")) - } - break - case "searchCommits": { - const cwd = this.cwd - if (cwd) { - try { - const commits = await searchCommits(message.query || "", cwd) - await this.postMessageToWebview({ - type: "commitSearchResults", - commits, - }) - } catch (error) { - this.outputChannel.appendLine( - `Error searching commits: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, - ) - vscode.window.showErrorMessage(t("common:errors.search_commits")) - } - } - break - } - case "searchFiles": { - const workspacePath = getWorkspacePath() - - if (!workspacePath) { - // Handle case where workspace path is not available - await this.postMessageToWebview({ - type: "fileSearchResults", - results: [], - requestId: message.requestId, - error: "No workspace path available", - }) - break - } - try { - // Call file search service with query from message - const results = await searchWorkspaceFiles( - message.query || "", - workspacePath, - 20, // Use default limit, as filtering is now done in the backend - ) - - // Send results back to webview - await this.postMessageToWebview({ - type: "fileSearchResults", - results, - requestId: message.requestId, - }) - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error) - - // Send error response to webview - await this.postMessageToWebview({ - type: "fileSearchResults", - results: [], - error: errorMessage, - requestId: message.requestId, - }) - } - break - } - case "saveApiConfiguration": - if (message.text && message.apiConfiguration) { - try { - await this.providerSettingsManager.saveConfig(message.text, message.apiConfiguration) - const listApiConfig = await this.providerSettingsManager.listConfig() - await this.updateGlobalState("listApiConfigMeta", listApiConfig) - } catch (error) { - this.outputChannel.appendLine( - `Error save api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, - ) - vscode.window.showErrorMessage(t("common:errors.save_api_config")) - } - } - break - case "upsertApiConfiguration": - if (message.text && message.apiConfiguration) { - await this.upsertApiConfiguration(message.text, message.apiConfiguration) - } - break - case "renameApiConfiguration": - if (message.values && message.apiConfiguration) { - try { - const { oldName, newName } = message.values - - if (oldName === newName) { - break - } - - // Load the old configuration to get its ID - const oldConfig = await this.providerSettingsManager.loadConfig(oldName) - - // Create a new configuration with the same ID - const newConfig = { - ...message.apiConfiguration, - id: oldConfig.id, // Preserve the ID - } - - // Save with the new name but same ID - await this.providerSettingsManager.saveConfig(newName, newConfig) - await this.providerSettingsManager.deleteConfig(oldName) - - const listApiConfig = await this.providerSettingsManager.listConfig() - - // Update listApiConfigMeta first to ensure UI has latest data - await this.updateGlobalState("listApiConfigMeta", listApiConfig) - await this.updateGlobalState("currentApiConfigName", newName) - - await this.postStateToWebview() - } catch (error) { - this.outputChannel.appendLine( - `Error rename api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, - ) - vscode.window.showErrorMessage(t("common:errors.rename_api_config")) - } - } - break - case "loadApiConfiguration": - if (message.text) { - try { - const apiConfig = await this.providerSettingsManager.loadConfig(message.text) - const listApiConfig = await this.providerSettingsManager.listConfig() - - await Promise.all([ - this.updateGlobalState("listApiConfigMeta", listApiConfig), - this.updateGlobalState("currentApiConfigName", message.text), - this.updateApiConfiguration(apiConfig), - ]) - - await this.postStateToWebview() - } catch (error) { - this.outputChannel.appendLine( - `Error load api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, - ) - vscode.window.showErrorMessage(t("common:errors.load_api_config")) - } - } - break - case "loadApiConfigurationById": - if (message.text) { - try { - const { config: apiConfig, name } = await this.providerSettingsManager.loadConfigById( - message.text, - ) - const listApiConfig = await this.providerSettingsManager.listConfig() - - await Promise.all([ - this.updateGlobalState("listApiConfigMeta", listApiConfig), - this.updateGlobalState("currentApiConfigName", name), - this.updateApiConfiguration(apiConfig), - ]) - - await this.postStateToWebview() - } catch (error) { - this.outputChannel.appendLine( - `Error load api configuration by ID: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, - ) - vscode.window.showErrorMessage(t("common:errors.load_api_config")) - } - } - break - case "deleteApiConfiguration": - if (message.text) { - const answer = await vscode.window.showInformationMessage( - t("common:confirmation.delete_config_profile"), - { modal: true }, - t("common:answers.yes"), - ) - - if (answer !== t("common:answers.yes")) { - break - } - - try { - await this.providerSettingsManager.deleteConfig(message.text) - const listApiConfig = await this.providerSettingsManager.listConfig() - - // Update listApiConfigMeta first to ensure UI has latest data - await this.updateGlobalState("listApiConfigMeta", listApiConfig) - - // If this was the current config, switch to first available - const currentApiConfigName = this.getGlobalState("currentApiConfigName") - - if (message.text === currentApiConfigName && listApiConfig?.[0]?.name) { - const apiConfig = await this.providerSettingsManager.loadConfig( - listApiConfig[0].name, - ) - await Promise.all([ - this.updateGlobalState("currentApiConfigName", listApiConfig[0].name), - this.updateApiConfiguration(apiConfig), - ]) - } - - await this.postStateToWebview() - } catch (error) { - this.outputChannel.appendLine( - `Error delete api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, - ) - vscode.window.showErrorMessage(t("common:errors.delete_api_config")) - } - } - break - case "getListApiConfiguration": - try { - const listApiConfig = await this.providerSettingsManager.listConfig() - await this.updateGlobalState("listApiConfigMeta", listApiConfig) - this.postMessageToWebview({ type: "listApiConfig", listApiConfig }) - } catch (error) { - this.outputChannel.appendLine( - `Error get list api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, - ) - vscode.window.showErrorMessage(t("common:errors.list_api_config")) - } - break - case "updateExperimental": { - if (!message.values) { - break - } - - const updatedExperiments = { - ...(this.getGlobalState("experiments") ?? experimentDefault), - ...message.values, - } - - await this.updateGlobalState("experiments", updatedExperiments) - - const currentCline = this.getCurrentCline() - - // Update diffStrategy in current Cline instance if it exists. - if (message.values[EXPERIMENT_IDS.DIFF_STRATEGY_UNIFIED] !== undefined && currentCline) { - await currentCline.updateDiffStrategy(updatedExperiments) - } - - await this.postStateToWebview() - break - } - case "updateMcpTimeout": - if (message.serverName && typeof message.timeout === "number") { - try { - await this.mcpHub?.updateServerTimeout( - message.serverName, - message.timeout, - message.source as "global" | "project", - ) - } catch (error) { - this.outputChannel.appendLine( - `Failed to update timeout for ${message.serverName}: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, - ) - vscode.window.showErrorMessage(t("common:errors.update_server_timeout")) - } - } - break - case "updateCustomMode": - if (message.modeConfig) { - await this.customModesManager.updateCustomMode(message.modeConfig.slug, message.modeConfig) - // Update state after saving the mode - const customModes = await this.customModesManager.getCustomModes() - await this.updateGlobalState("customModes", customModes) - await this.updateGlobalState("mode", message.modeConfig.slug) - await this.postStateToWebview() - } - break - case "deleteCustomMode": - if (message.slug) { - const answer = await vscode.window.showInformationMessage( - t("common:confirmation.delete_custom_mode"), - { modal: true }, - t("common:answers.yes"), - ) - - if (answer !== t("common:answers.yes")) { - break - } - - await this.customModesManager.deleteCustomMode(message.slug) - // Switch back to default mode after deletion - await this.updateGlobalState("mode", defaultModeSlug) - await this.postStateToWebview() - } - break - case "humanRelayResponse": - if (message.requestId && message.text) { - vscode.commands.executeCommand("roo-cline.handleHumanRelayResponse", { - requestId: message.requestId, - text: message.text, - cancelled: false, - }) - } - break - - case "humanRelayCancel": - if (message.requestId) { - vscode.commands.executeCommand("roo-cline.handleHumanRelayResponse", { - requestId: message.requestId, - cancelled: true, - }) - } - break - - case "telemetrySetting": { - const telemetrySetting = message.text as TelemetrySetting - await this.updateGlobalState("telemetrySetting", telemetrySetting) - const isOptedIn = telemetrySetting === "enabled" - telemetryService.updateTelemetryState(isOptedIn) - await this.postStateToWebview() - break - } - } - }, - null, - this.disposables, - ) - - const generateSystemPrompt = async (message: WebviewMessage) => { - const { - apiConfiguration, - customModePrompts, - customInstructions, - browserViewportSize, - diffEnabled, - mcpEnabled, - fuzzyMatchThreshold, - experiments, - enableMcpServerCreation, - browserToolEnabled, - language, - } = await this.getState() - - // Create diffStrategy based on current model and settings. - const diffStrategy = getDiffStrategy({ - model: apiConfiguration.apiModelId || apiConfiguration.openRouterModelId || "", - experiments, - fuzzyMatchThreshold, - }) - - const cwd = this.cwd - - const mode = message.mode ?? defaultModeSlug - const customModes = await this.customModesManager.getCustomModes() - - const rooIgnoreInstructions = this.getCurrentCline()?.rooIgnoreController?.getInstructions() - - // Determine if browser tools can be used based on model support, mode, and user settings - let modelSupportsComputerUse = false - - // Create a temporary API handler to check if the model supports computer use - // This avoids relying on an active Cline instance which might not exist during preview - try { - const tempApiHandler = buildApiHandler(apiConfiguration) - modelSupportsComputerUse = tempApiHandler.getModel().info.supportsComputerUse ?? false - } catch (error) { - console.error("Error checking if model supports computer use:", error) - } - - // Check if the current mode includes the browser tool group - const modeConfig = getModeBySlug(mode, customModes) - const modeSupportsBrowser = modeConfig?.groups.some((group) => getGroupName(group) === "browser") ?? false - - // Only enable browser tools if the model supports it, the mode includes browser tools, - // and browser tools are enabled in settings - const canUseBrowserTool = modelSupportsComputerUse && modeSupportsBrowser && (browserToolEnabled ?? true) - - const systemPrompt = await SYSTEM_PROMPT( - this.context, - cwd, - canUseBrowserTool, - mcpEnabled ? this.mcpHub : undefined, - diffStrategy, - browserViewportSize ?? "900x600", - mode, - customModePrompts, - customModes, - customInstructions, - diffEnabled, - experiments, - enableMcpServerCreation, - language, - rooIgnoreInstructions, - ) - return systemPrompt - } + webview.onDidReceiveMessage(onReceiveMessage, null, this.disposables) } /** @@ -2191,7 +783,8 @@ export class ClineProvider extends EventEmitter implements await this.postStateToWebview() } - private async updateApiConfiguration(providerSettings: ProviderSettings) { + // not private, so it can be accessed from webviewMessageHandler + async updateApiConfiguration(providerSettings: ProviderSettings) { // Update mode's default config. const { mode } = await this.getState() @@ -2297,13 +890,15 @@ export class ClineProvider extends EventEmitter implements return getSettingsDirectoryPath(globalStoragePath) } - private async ensureCacheDirectoryExists() { + // not private, so it can be accessed from webviewMessageHandler + async ensureCacheDirectoryExists() { const { getCacheDirectoryPath } = await import("../../shared/storagePathManager") const globalStoragePath = this.contextProxy.globalStorageUri.fsPath return getCacheDirectoryPath(globalStoragePath) } - private async readModelsFromCache(filename: string): Promise | undefined> { + // not private, so it can be accessed from webviewMessageHandler + async readModelsFromCache(filename: string): Promise | undefined> { const filePath = path.join(await this.ensureCacheDirectoryExists(), filename) const fileExists = await fileExistsAtPath(filePath) @@ -2763,12 +1358,14 @@ export class ClineProvider extends EventEmitter implements // ContextProxy // @deprecated - Use `ContextProxy#setValue` instead. - private async updateGlobalState(key: K, value: GlobalState[K]) { + // not private, so it can be accessed from webviewMessageHandler + async updateGlobalState(key: K, value: GlobalState[K]) { await this.contextProxy.setValue(key, value) } // @deprecated - Use `ContextProxy#getValue` instead. - private getGlobalState(key: K) { + // not private, so it can be accessed from webviewMessageHandler + getGlobalState(key: K) { return this.contextProxy.getValue(key) } diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts new file mode 100644 index 0000000000..68ff56a79e --- /dev/null +++ b/src/core/webview/webviewMessageHandler.ts @@ -0,0 +1,1410 @@ +import * as path from "path" +import fs from "fs/promises" +import pWaitFor from "p-wait-for" +import * as vscode from "vscode" + +import { ClineProvider } from "./ClineProvider" +import { CheckpointStorage, Language, ApiConfigMeta } from "../../schemas" +import { changeLanguage, t } from "../../i18n" +import { ApiConfiguration } from "../../shared/api" +import { supportPrompt } from "../../shared/support-prompt" +import { GlobalFileNames } from "../../shared/globalFileNames" + +import { checkoutDiffPayloadSchema, checkoutRestorePayloadSchema, WebviewMessage } from "../../shared/WebviewMessage" +import { checkExistKey } from "../../shared/checkExistApiConfig" +import { EXPERIMENT_IDS, experiments as Experiments, experimentDefault, ExperimentId } from "../../shared/experiments" +import { Terminal } from "../../integrations/terminal/Terminal" +import { openFile, openImage } from "../../integrations/misc/open-file" +import { selectImages } from "../../integrations/misc/process-images" +import { getTheme } from "../../integrations/theme/getTheme" +import { discoverChromeHostUrl, tryChromeHostUrl } from "../../services/browser/browserDiscovery" +import { searchWorkspaceFiles } from "../../services/search/file-search" +import { fileExistsAtPath } from "../../utils/fs" +import { playSound, setSoundEnabled, setSoundVolume } from "../../utils/sound" +import { playTts, setTtsEnabled, setTtsSpeed, stopTts } from "../../utils/tts" +import { singleCompletionHandler } from "../../utils/single-completion-handler" +import { searchCommits } from "../../utils/git" +import { exportSettings, importSettings } from "../config/importExport" +import { getOpenRouterModels } from "../../api/providers/openrouter" +import { getGlamaModels } from "../../api/providers/glama" +import { getUnboundModels } from "../../api/providers/unbound" +import { getRequestyModels } from "../../api/providers/requesty" +import { getOpenAiModels } from "../../api/providers/openai" +import { getOllamaModels } from "../../api/providers/ollama" +import { getVsCodeLmModels } from "../../api/providers/vscode-lm" +import { getLmStudioModels } from "../../api/providers/lmstudio" +import { openMention } from "../mentions" +import { telemetryService } from "../../services/telemetry/TelemetryService" +import { TelemetrySetting } from "../../shared/TelemetrySetting" +import { getWorkspacePath } from "../../utils/path" +import { Mode, PromptComponent, defaultModeSlug, getModeBySlug, getGroupName } from "../../shared/modes" +import { getDiffStrategy } from "../diff/DiffStrategy" +import { SYSTEM_PROMPT } from "../prompts/system" +import { buildApiHandler } from "../../api" + +export const webviewMessageHandler = async (provider: ClineProvider, message: WebviewMessage) => { + switch (message.type) { + case "webviewDidLaunch": + // Load custom modes first + const customModes = await provider.customModesManager.getCustomModes() + await provider.updateGlobalState("customModes", customModes) + + provider.postStateToWebview() + provider.workspaceTracker?.initializeFilePaths() // don't await + + getTheme().then((theme) => provider.postMessageToWebview({ type: "theme", text: JSON.stringify(theme) })) + + // If MCP Hub is already initialized, update the webview with current server list + if (provider.mcpHub) { + provider.postMessageToWebview({ + type: "mcpServers", + mcpServers: provider.mcpHub.getAllServers(), + }) + } + + const cacheDir = await provider.ensureCacheDirectoryExists() + + // Post last cached models in case the call to endpoint fails. + provider.readModelsFromCache(GlobalFileNames.openRouterModels).then((openRouterModels) => { + if (openRouterModels) { + provider.postMessageToWebview({ type: "openRouterModels", openRouterModels }) + } + }) + + // GUI relies on model info to be up-to-date to provide + // the most accurate pricing, so we need to fetch the + // latest details on launch. + // We do this for all users since many users switch + // between api providers and if they were to switch back + // to OpenRouter it would be showing outdated model info + // if we hadn't retrieved the latest at this point + // (see normalizeApiConfiguration > openrouter). + const { apiConfiguration: currentApiConfig } = await provider.getState() + getOpenRouterModels(currentApiConfig).then(async (openRouterModels) => { + if (Object.keys(openRouterModels).length > 0) { + await fs.writeFile( + path.join(cacheDir, GlobalFileNames.openRouterModels), + JSON.stringify(openRouterModels), + ) + await provider.postMessageToWebview({ type: "openRouterModels", openRouterModels }) + + // Update model info in state (this needs to be + // done here since we don't want to update state + // while settings is open, and we may refresh + // models there). + const { apiConfiguration } = await provider.getState() + + if (apiConfiguration.openRouterModelId) { + await provider.updateGlobalState( + "openRouterModelInfo", + openRouterModels[apiConfiguration.openRouterModelId], + ) + await provider.postStateToWebview() + } + } + }) + + provider.readModelsFromCache(GlobalFileNames.glamaModels).then((glamaModels) => { + if (glamaModels) { + provider.postMessageToWebview({ type: "glamaModels", glamaModels }) + } + }) + + getGlamaModels().then(async (glamaModels) => { + if (Object.keys(glamaModels).length > 0) { + await fs.writeFile(path.join(cacheDir, GlobalFileNames.glamaModels), JSON.stringify(glamaModels)) + await provider.postMessageToWebview({ type: "glamaModels", glamaModels }) + + const { apiConfiguration } = await provider.getState() + + if (apiConfiguration.glamaModelId) { + await provider.updateGlobalState("glamaModelInfo", glamaModels[apiConfiguration.glamaModelId]) + await provider.postStateToWebview() + } + } + }) + + provider.readModelsFromCache(GlobalFileNames.unboundModels).then((unboundModels) => { + if (unboundModels) { + provider.postMessageToWebview({ type: "unboundModels", unboundModels }) + } + }) + + getUnboundModels().then(async (unboundModels) => { + if (Object.keys(unboundModels).length > 0) { + await fs.writeFile( + path.join(cacheDir, GlobalFileNames.unboundModels), + JSON.stringify(unboundModels), + ) + await provider.postMessageToWebview({ type: "unboundModels", unboundModels }) + + const { apiConfiguration } = await provider.getState() + + if (apiConfiguration?.unboundModelId) { + await provider.updateGlobalState( + "unboundModelInfo", + unboundModels[apiConfiguration.unboundModelId], + ) + await provider.postStateToWebview() + } + } + }) + + provider.readModelsFromCache(GlobalFileNames.requestyModels).then((requestyModels) => { + if (requestyModels) { + provider.postMessageToWebview({ type: "requestyModels", requestyModels }) + } + }) + + getRequestyModels().then(async (requestyModels) => { + if (Object.keys(requestyModels).length > 0) { + await fs.writeFile( + path.join(cacheDir, GlobalFileNames.requestyModels), + JSON.stringify(requestyModels), + ) + await provider.postMessageToWebview({ type: "requestyModels", requestyModels }) + + const { apiConfiguration } = await provider.getState() + + if (apiConfiguration.requestyModelId) { + await provider.updateGlobalState( + "requestyModelInfo", + requestyModels[apiConfiguration.requestyModelId], + ) + await provider.postStateToWebview() + } + } + }) + + provider.providerSettingsManager + .listConfig() + .then(async (listApiConfig) => { + if (!listApiConfig) { + return + } + + if (listApiConfig.length === 1) { + // Check if first time init then sync with exist config. + if (!checkExistKey(listApiConfig[0])) { + const { apiConfiguration } = await provider.getState() + + await provider.providerSettingsManager.saveConfig( + listApiConfig[0].name ?? "default", + apiConfiguration, + ) + + listApiConfig[0].apiProvider = apiConfiguration.apiProvider + } + } + + const currentConfigName = provider.getGlobalState("currentApiConfigName") + + if (currentConfigName) { + if (!(await provider.providerSettingsManager.hasConfig(currentConfigName))) { + // current config name not valid, get first config in list + await provider.updateGlobalState("currentApiConfigName", listApiConfig?.[0]?.name) + if (listApiConfig?.[0]?.name) { + const apiConfig = await provider.providerSettingsManager.loadConfig( + listApiConfig?.[0]?.name, + ) + + await Promise.all([ + provider.updateGlobalState("listApiConfigMeta", listApiConfig), + provider.postMessageToWebview({ type: "listApiConfig", listApiConfig }), + provider.updateApiConfiguration(apiConfig), + ]) + await provider.postStateToWebview() + return + } + } + } + + await Promise.all([ + await provider.updateGlobalState("listApiConfigMeta", listApiConfig), + await provider.postMessageToWebview({ type: "listApiConfig", listApiConfig }), + ]) + }) + .catch((error) => + provider.outputChannel.appendLine( + `Error list api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, + ), + ) + + // If user already opted in to telemetry, enable telemetry service + provider.getStateToPostToWebview().then((state) => { + const { telemetrySetting } = state + const isOptedIn = telemetrySetting === "enabled" + telemetryService.updateTelemetryState(isOptedIn) + }) + + provider.isViewLaunched = true + break + case "newTask": + // Code that should run in response to the hello message command + //vscode.window.showInformationMessage(message.text!) + + // Send a message to our webview. + // You can send any JSON serializable data. + // Could also do this in extension .ts + //provider.postMessageToWebview({ type: "text", text: `Extension: ${Date.now()}` }) + // initializing new instance of Cline will make sure that any agentically running promises in old instance don't affect our new task. this essentially creates a fresh slate for the new task + await provider.initClineWithTask(message.text, message.images) + break + case "apiConfiguration": + if (message.apiConfiguration) { + await provider.updateApiConfiguration(message.apiConfiguration) + } + await provider.postStateToWebview() + break + case "customInstructions": + await provider.updateCustomInstructions(message.text) + break + case "alwaysAllowReadOnly": + await provider.updateGlobalState("alwaysAllowReadOnly", message.bool ?? undefined) + await provider.postStateToWebview() + break + case "alwaysAllowReadOnlyOutsideWorkspace": + await provider.updateGlobalState("alwaysAllowReadOnlyOutsideWorkspace", message.bool ?? undefined) + await provider.postStateToWebview() + break + case "alwaysAllowWrite": + await provider.updateGlobalState("alwaysAllowWrite", message.bool ?? undefined) + await provider.postStateToWebview() + break + case "alwaysAllowWriteOutsideWorkspace": + await provider.updateGlobalState("alwaysAllowWriteOutsideWorkspace", message.bool ?? undefined) + await provider.postStateToWebview() + break + case "alwaysAllowExecute": + await provider.updateGlobalState("alwaysAllowExecute", message.bool ?? undefined) + await provider.postStateToWebview() + break + case "alwaysAllowBrowser": + await provider.updateGlobalState("alwaysAllowBrowser", message.bool ?? undefined) + await provider.postStateToWebview() + break + case "alwaysAllowMcp": + await provider.updateGlobalState("alwaysAllowMcp", message.bool) + await provider.postStateToWebview() + break + case "alwaysAllowModeSwitch": + await provider.updateGlobalState("alwaysAllowModeSwitch", message.bool) + await provider.postStateToWebview() + break + case "alwaysAllowSubtasks": + await provider.updateGlobalState("alwaysAllowSubtasks", message.bool) + await provider.postStateToWebview() + break + case "askResponse": + provider.getCurrentCline()?.handleWebviewAskResponse(message.askResponse!, message.text, message.images) + break + case "clearTask": + // clear task resets the current session and allows for a new task to be started, if this session is a subtask - it allows the parent task to be resumed + await provider.finishSubTask(t("common:tasks.canceled")) + await provider.postStateToWebview() + break + case "didShowAnnouncement": + await provider.updateGlobalState("lastShownAnnouncementId", provider.latestAnnouncementId) + await provider.postStateToWebview() + break + case "selectImages": + const images = await selectImages() + await provider.postMessageToWebview({ type: "selectedImages", images }) + break + case "exportCurrentTask": + const currentTaskId = provider.getCurrentCline()?.taskId + if (currentTaskId) { + provider.exportTaskWithId(currentTaskId) + } + break + case "showTaskWithId": + provider.showTaskWithId(message.text!) + break + case "deleteTaskWithId": + provider.deleteTaskWithId(message.text!) + break + case "deleteMultipleTasksWithIds": { + const ids = message.ids + + if (Array.isArray(ids)) { + // Process in batches of 20 (or another reasonable number) + const batchSize = 20 + const results = [] + + // Only log start and end of the operation + console.log(`Batch deletion started: ${ids.length} tasks total`) + + for (let i = 0; i < ids.length; i += batchSize) { + const batch = ids.slice(i, i + batchSize) + + const batchPromises = batch.map(async (id) => { + try { + await provider.deleteTaskWithId(id) + return { id, success: true } + } catch (error) { + // Keep error logging for debugging purposes + console.log( + `Failed to delete task ${id}: ${error instanceof Error ? error.message : String(error)}`, + ) + return { id, success: false } + } + }) + + // Process each batch in parallel but wait for completion before starting the next batch + const batchResults = await Promise.all(batchPromises) + results.push(...batchResults) + + // Update the UI after each batch to show progress + await provider.postStateToWebview() + } + + // Log final results + const successCount = results.filter((r) => r.success).length + const failCount = results.length - successCount + console.log( + `Batch deletion completed: ${successCount}/${ids.length} tasks successful, ${failCount} tasks failed`, + ) + } + break + } + case "exportTaskWithId": + provider.exportTaskWithId(message.text!) + break + case "importSettings": + const { success } = await importSettings({ + providerSettingsManager: provider.providerSettingsManager, + contextProxy: provider.contextProxy, + }) + + if (success) { + provider.settingsImportedAt = Date.now() + await provider.postStateToWebview() + await vscode.window.showInformationMessage(t("common:info.settings_imported")) + } + + break + case "exportSettings": + await exportSettings({ + providerSettingsManager: provider.providerSettingsManager, + contextProxy: provider.contextProxy, + }) + + break + case "resetState": + await provider.resetState() + break + case "refreshOpenRouterModels": { + const { apiConfiguration: configForRefresh } = await provider.getState() + const openRouterModels = await getOpenRouterModels(configForRefresh) + + if (Object.keys(openRouterModels).length > 0) { + const cacheDir = await provider.ensureCacheDirectoryExists() + await fs.writeFile( + path.join(cacheDir, GlobalFileNames.openRouterModels), + JSON.stringify(openRouterModels), + ) + await provider.postMessageToWebview({ type: "openRouterModels", openRouterModels }) + } + + break + } + case "refreshGlamaModels": + const glamaModels = await getGlamaModels() + + if (Object.keys(glamaModels).length > 0) { + const cacheDir = await provider.ensureCacheDirectoryExists() + await fs.writeFile(path.join(cacheDir, GlobalFileNames.glamaModels), JSON.stringify(glamaModels)) + await provider.postMessageToWebview({ type: "glamaModels", glamaModels }) + } + + break + case "refreshUnboundModels": + const unboundModels = await getUnboundModels() + + if (Object.keys(unboundModels).length > 0) { + const cacheDir = await provider.ensureCacheDirectoryExists() + await fs.writeFile(path.join(cacheDir, GlobalFileNames.unboundModels), JSON.stringify(unboundModels)) + await provider.postMessageToWebview({ type: "unboundModels", unboundModels }) + } + + break + case "refreshRequestyModels": + const requestyModels = await getRequestyModels() + + if (Object.keys(requestyModels).length > 0) { + const cacheDir = await provider.ensureCacheDirectoryExists() + await fs.writeFile(path.join(cacheDir, GlobalFileNames.requestyModels), JSON.stringify(requestyModels)) + await provider.postMessageToWebview({ type: "requestyModels", requestyModels }) + } + + break + case "refreshOpenAiModels": + if (message?.values?.baseUrl && message?.values?.apiKey) { + const openAiModels = await getOpenAiModels(message?.values?.baseUrl, message?.values?.apiKey) + provider.postMessageToWebview({ type: "openAiModels", openAiModels }) + } + + break + case "requestOllamaModels": + const ollamaModels = await getOllamaModels(message.text) + // TODO: Cache like we do for OpenRouter, etc? + provider.postMessageToWebview({ type: "ollamaModels", ollamaModels }) + break + case "requestLmStudioModels": + const lmStudioModels = await getLmStudioModels(message.text) + // TODO: Cache like we do for OpenRouter, etc? + provider.postMessageToWebview({ type: "lmStudioModels", lmStudioModels }) + break + case "requestVsCodeLmModels": + const vsCodeLmModels = await getVsCodeLmModels() + // TODO: Cache like we do for OpenRouter, etc? + provider.postMessageToWebview({ type: "vsCodeLmModels", vsCodeLmModels }) + break + case "openImage": + openImage(message.text!) + break + case "openFile": + openFile(message.text!, message.values as { create?: boolean; content?: string }) + break + case "openMention": + { + const { osInfo } = (await provider.getState()) || {} + openMention(message.text, osInfo) + } + break + case "checkpointDiff": + const result = checkoutDiffPayloadSchema.safeParse(message.payload) + + if (result.success) { + await provider.getCurrentCline()?.checkpointDiff(result.data) + } + + break + case "checkpointRestore": { + const result = checkoutRestorePayloadSchema.safeParse(message.payload) + + if (result.success) { + await provider.cancelTask() + + try { + await pWaitFor(() => provider.getCurrentCline()?.isInitialized === true, { timeout: 3_000 }) + } catch (error) { + vscode.window.showErrorMessage(t("common:errors.checkpoint_timeout")) + } + + try { + await provider.getCurrentCline()?.checkpointRestore(result.data) + } catch (error) { + vscode.window.showErrorMessage(t("common:errors.checkpoint_failed")) + } + } + + break + } + case "cancelTask": + await provider.cancelTask() + break + case "allowedCommands": + await provider.context.globalState.update("allowedCommands", message.commands) + // Also update workspace settings + await vscode.workspace + .getConfiguration("roo-cline") + .update("allowedCommands", message.commands, vscode.ConfigurationTarget.Global) + break + case "openMcpSettings": { + const mcpSettingsFilePath = await provider.mcpHub?.getMcpSettingsFilePath() + if (mcpSettingsFilePath) { + openFile(mcpSettingsFilePath) + } + break + } + case "openProjectMcpSettings": { + if (!vscode.workspace.workspaceFolders?.length) { + vscode.window.showErrorMessage(t("common:errors.no_workspace")) + return + } + + const workspaceFolder = vscode.workspace.workspaceFolders[0] + const rooDir = path.join(workspaceFolder.uri.fsPath, ".roo") + const mcpPath = path.join(rooDir, "mcp.json") + + try { + await fs.mkdir(rooDir, { recursive: true }) + const exists = await fileExistsAtPath(mcpPath) + if (!exists) { + await fs.writeFile(mcpPath, JSON.stringify({ mcpServers: {} }, null, 2)) + } + await openFile(mcpPath) + } catch (error) { + vscode.window.showErrorMessage(t("common:errors.create_mcp_json", { error: `${error}` })) + } + break + } + case "openCustomModesSettings": { + const customModesFilePath = await provider.customModesManager.getCustomModesFilePath() + if (customModesFilePath) { + openFile(customModesFilePath) + } + break + } + case "deleteMcpServer": { + if (!message.serverName) { + break + } + + try { + provider.outputChannel.appendLine(`Attempting to delete MCP server: ${message.serverName}`) + await provider.mcpHub?.deleteServer(message.serverName, message.source as "global" | "project") + provider.outputChannel.appendLine(`Successfully deleted MCP server: ${message.serverName}`) + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + provider.outputChannel.appendLine(`Failed to delete MCP server: ${errorMessage}`) + // Error messages are already handled by McpHub.deleteServer + } + break + } + case "restartMcpServer": { + try { + await provider.mcpHub?.restartConnection(message.text!, message.source as "global" | "project") + } catch (error) { + provider.outputChannel.appendLine( + `Failed to retry connection for ${message.text}: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, + ) + } + break + } + case "toggleToolAlwaysAllow": { + try { + if (provider.mcpHub) { + await provider.mcpHub.toggleToolAlwaysAllow( + message.serverName!, + message.source as "global" | "project", + message.toolName!, + Boolean(message.alwaysAllow), + ) + } + } catch (error) { + provider.outputChannel.appendLine( + `Failed to toggle auto-approve for tool ${message.toolName}: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, + ) + } + break + } + case "toggleMcpServer": { + try { + await provider.mcpHub?.toggleServerDisabled( + message.serverName!, + message.disabled!, + message.source as "global" | "project", + ) + } catch (error) { + provider.outputChannel.appendLine( + `Failed to toggle MCP server ${message.serverName}: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, + ) + } + break + } + case "mcpEnabled": + const mcpEnabled = message.bool ?? true + await provider.updateGlobalState("mcpEnabled", mcpEnabled) + await provider.postStateToWebview() + break + case "enableMcpServerCreation": + await provider.updateGlobalState("enableMcpServerCreation", message.bool ?? true) + await provider.postStateToWebview() + break + case "playSound": + if (message.audioType) { + const soundPath = path.join(provider.context.extensionPath, "audio", `${message.audioType}.wav`) + playSound(soundPath) + } + break + case "soundEnabled": + const soundEnabled = message.bool ?? true + await provider.updateGlobalState("soundEnabled", soundEnabled) + setSoundEnabled(soundEnabled) // Add this line to update the sound utility + await provider.postStateToWebview() + break + case "soundVolume": + const soundVolume = message.value ?? 0.5 + await provider.updateGlobalState("soundVolume", soundVolume) + setSoundVolume(soundVolume) + await provider.postStateToWebview() + break + case "ttsEnabled": + const ttsEnabled = message.bool ?? true + await provider.updateGlobalState("ttsEnabled", ttsEnabled) + setTtsEnabled(ttsEnabled) // Add this line to update the tts utility + await provider.postStateToWebview() + break + case "ttsSpeed": + const ttsSpeed = message.value ?? 1.0 + await provider.updateGlobalState("ttsSpeed", ttsSpeed) + setTtsSpeed(ttsSpeed) + await provider.postStateToWebview() + break + case "playTts": + if (message.text) { + playTts(message.text, { + onStart: () => provider.postMessageToWebview({ type: "ttsStart", text: message.text }), + onStop: () => provider.postMessageToWebview({ type: "ttsStop", text: message.text }), + }) + } + break + case "stopTts": + stopTts() + break + case "diffEnabled": + const diffEnabled = message.bool ?? true + await provider.updateGlobalState("diffEnabled", diffEnabled) + await provider.postStateToWebview() + break + case "enableCheckpoints": + const enableCheckpoints = message.bool ?? true + await provider.updateGlobalState("enableCheckpoints", enableCheckpoints) + await provider.postStateToWebview() + break + case "checkpointStorage": + console.log(`[ClineProvider] checkpointStorage: ${message.text}`) + const checkpointStorage = message.text ?? "task" + await provider.updateGlobalState("checkpointStorage", checkpointStorage as CheckpointStorage) + await provider.postStateToWebview() + break + case "browserViewportSize": + const browserViewportSize = message.text ?? "900x600" + await provider.updateGlobalState("browserViewportSize", browserViewportSize) + await provider.postStateToWebview() + break + case "remoteBrowserHost": + await provider.updateGlobalState("remoteBrowserHost", message.text) + await provider.postStateToWebview() + break + case "remoteBrowserEnabled": + // Store the preference in global state + // remoteBrowserEnabled now means "enable remote browser connection" + await provider.updateGlobalState("remoteBrowserEnabled", message.bool ?? false) + // If disabling remote browser connection, clear the remoteBrowserHost + if (!message.bool) { + await provider.updateGlobalState("remoteBrowserHost", undefined) + } + await provider.postStateToWebview() + break + case "testBrowserConnection": + // If no text is provided, try auto-discovery + if (!message.text) { + // Use testBrowserConnection for auto-discovery + const chromeHostUrl = await discoverChromeHostUrl() + if (chromeHostUrl) { + // Send the result back to the webview + await provider.postMessageToWebview({ + type: "browserConnectionResult", + success: !!chromeHostUrl, + text: `Auto-discovered and tested connection to Chrome: ${chromeHostUrl}`, + values: { endpoint: chromeHostUrl }, + }) + } else { + await provider.postMessageToWebview({ + type: "browserConnectionResult", + success: false, + text: "No Chrome instances found on the network. Make sure Chrome is running with remote debugging enabled (--remote-debugging-port=9222).", + }) + } + } else { + // Test the provided URL + const customHostUrl = message.text + const hostIsValid = await tryChromeHostUrl(message.text) + // Send the result back to the webview + await provider.postMessageToWebview({ + type: "browserConnectionResult", + success: hostIsValid, + text: hostIsValid + ? `Successfully connected to Chrome: ${customHostUrl}` + : "Failed to connect to Chrome", + }) + } + break + case "fuzzyMatchThreshold": + await provider.updateGlobalState("fuzzyMatchThreshold", message.value) + await provider.postStateToWebview() + break + case "alwaysApproveResubmit": + await provider.updateGlobalState("alwaysApproveResubmit", message.bool ?? false) + await provider.postStateToWebview() + break + case "requestDelaySeconds": + await provider.updateGlobalState("requestDelaySeconds", message.value ?? 5) + await provider.postStateToWebview() + break + case "rateLimitSeconds": + await provider.updateGlobalState("rateLimitSeconds", message.value ?? 0) + await provider.postStateToWebview() + break + case "writeDelayMs": + await provider.updateGlobalState("writeDelayMs", message.value) + await provider.postStateToWebview() + break + case "terminalOutputLineLimit": + await provider.updateGlobalState("terminalOutputLineLimit", message.value) + await provider.postStateToWebview() + break + case "terminalShellIntegrationTimeout": + await provider.updateGlobalState("terminalShellIntegrationTimeout", message.value) + await provider.postStateToWebview() + if (message.value !== undefined) { + Terminal.setShellIntegrationTimeout(message.value) + } + break + case "mode": + await provider.handleModeSwitch(message.text as Mode) + break + case "updateSupportPrompt": + try { + if (Object.keys(message?.values ?? {}).length === 0) { + return + } + + const existingPrompts = provider.getGlobalState("customSupportPrompts") ?? {} + const updatedPrompts = { ...existingPrompts, ...message.values } + await provider.updateGlobalState("customSupportPrompts", updatedPrompts) + await provider.postStateToWebview() + } catch (error) { + provider.outputChannel.appendLine( + `Error update support prompt: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, + ) + vscode.window.showErrorMessage(t("common:errors.update_support_prompt")) + } + break + case "resetSupportPrompt": + try { + if (!message?.text) { + return + } + + const existingPrompts = provider.getGlobalState("customSupportPrompts") ?? {} + const updatedPrompts = { ...existingPrompts } + updatedPrompts[message.text] = undefined + await provider.updateGlobalState("customSupportPrompts", updatedPrompts) + await provider.postStateToWebview() + } catch (error) { + provider.outputChannel.appendLine( + `Error reset support prompt: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, + ) + vscode.window.showErrorMessage(t("common:errors.reset_support_prompt")) + } + break + case "updatePrompt": + if (message.promptMode && message.customPrompt !== undefined) { + const existingPrompts = provider.getGlobalState("customModePrompts") ?? {} + const updatedPrompts = { ...existingPrompts, [message.promptMode]: message.customPrompt } + await provider.updateGlobalState("customModePrompts", updatedPrompts) + const currentState = await provider.getState() + const stateWithPrompts = { ...currentState, customModePrompts: updatedPrompts } + provider.view?.webview.postMessage({ type: "state", state: stateWithPrompts }) + } + break + case "deleteMessage": { + const answer = await vscode.window.showInformationMessage( + t("common:confirmation.delete_message"), + { modal: true }, + t("common:confirmation.just_this_message"), + t("common:confirmation.this_and_subsequent"), + ) + + if ( + (answer === t("common:confirmation.just_this_message") || + answer === t("common:confirmation.this_and_subsequent")) && + provider.getCurrentCline() && + typeof message.value === "number" && + message.value + ) { + const timeCutoff = message.value - 1000 // 1 second buffer before the message to delete + + const messageIndex = provider + .getCurrentCline()! + .clineMessages.findIndex((msg) => msg.ts && msg.ts >= timeCutoff) + + const apiConversationHistoryIndex = provider + .getCurrentCline() + ?.apiConversationHistory.findIndex((msg) => msg.ts && msg.ts >= timeCutoff) + + if (messageIndex !== -1) { + const { historyItem } = await provider.getTaskWithId(provider.getCurrentCline()!.taskId) + + if (answer === t("common:confirmation.just_this_message")) { + // Find the next user message first + const nextUserMessage = provider + .getCurrentCline()! + .clineMessages.slice(messageIndex + 1) + .find((msg) => msg.type === "say" && msg.say === "user_feedback") + + // Handle UI messages + if (nextUserMessage) { + // Find absolute index of next user message + const nextUserMessageIndex = provider + .getCurrentCline()! + .clineMessages.findIndex((msg) => msg === nextUserMessage) + + // Keep messages before current message and after next user message + await provider + .getCurrentCline()! + .overwriteClineMessages([ + ...provider.getCurrentCline()!.clineMessages.slice(0, messageIndex), + ...provider.getCurrentCline()!.clineMessages.slice(nextUserMessageIndex), + ]) + } else { + // If no next user message, keep only messages before current message + await provider + .getCurrentCline()! + .overwriteClineMessages( + provider.getCurrentCline()!.clineMessages.slice(0, messageIndex), + ) + } + + // Handle API messages + if (apiConversationHistoryIndex !== -1) { + if (nextUserMessage && nextUserMessage.ts) { + // Keep messages before current API message and after next user message + await provider + .getCurrentCline()! + .overwriteApiConversationHistory([ + ...provider + .getCurrentCline()! + .apiConversationHistory.slice(0, apiConversationHistoryIndex), + ...provider + .getCurrentCline()! + .apiConversationHistory.filter( + (msg) => msg.ts && msg.ts >= nextUserMessage.ts, + ), + ]) + } else { + // If no next user message, keep only messages before current API message + await provider + .getCurrentCline()! + .overwriteApiConversationHistory( + provider + .getCurrentCline()! + .apiConversationHistory.slice(0, apiConversationHistoryIndex), + ) + } + } + } else if (answer === t("common:confirmation.this_and_subsequent")) { + // Delete this message and all that follow + await provider + .getCurrentCline()! + .overwriteClineMessages(provider.getCurrentCline()!.clineMessages.slice(0, messageIndex)) + if (apiConversationHistoryIndex !== -1) { + await provider + .getCurrentCline()! + .overwriteApiConversationHistory( + provider + .getCurrentCline()! + .apiConversationHistory.slice(0, apiConversationHistoryIndex), + ) + } + } + + await provider.initClineWithHistoryItem(historyItem) + } + } + break + } + case "screenshotQuality": + await provider.updateGlobalState("screenshotQuality", message.value) + await provider.postStateToWebview() + break + case "maxOpenTabsContext": + const tabCount = Math.min(Math.max(0, message.value ?? 20), 500) + await provider.updateGlobalState("maxOpenTabsContext", tabCount) + await provider.postStateToWebview() + break + case "maxWorkspaceFiles": + const fileCount = Math.min(Math.max(0, message.value ?? 200), 500) + await provider.updateGlobalState("maxWorkspaceFiles", fileCount) + await provider.postStateToWebview() + break + case "browserToolEnabled": + await provider.updateGlobalState("browserToolEnabled", message.bool ?? true) + await provider.postStateToWebview() + break + case "language": + changeLanguage(message.text ?? "en") + await provider.updateGlobalState("language", message.text as Language) + await provider.postStateToWebview() + break + case "showRooIgnoredFiles": + await provider.updateGlobalState("showRooIgnoredFiles", message.bool ?? true) + await provider.postStateToWebview() + break + case "maxReadFileLine": + await provider.updateGlobalState("maxReadFileLine", message.value) + await provider.postStateToWebview() + break + case "toggleApiConfigPin": + if (message.text) { + const currentPinned = provider.getGlobalState("pinnedApiConfigs") ?? {} + const updatedPinned: Record = { ...currentPinned } + + if (currentPinned[message.text]) { + delete updatedPinned[message.text] + } else { + updatedPinned[message.text] = true + } + + await provider.updateGlobalState("pinnedApiConfigs", updatedPinned) + await provider.postStateToWebview() + } + break + case "enhancementApiConfigId": + await provider.updateGlobalState("enhancementApiConfigId", message.text) + await provider.postStateToWebview() + break + case "autoApprovalEnabled": + await provider.updateGlobalState("autoApprovalEnabled", message.bool ?? false) + await provider.postStateToWebview() + break + case "enhancePrompt": + if (message.text) { + try { + const { apiConfiguration, customSupportPrompts, listApiConfigMeta, enhancementApiConfigId } = + await provider.getState() + + // Try to get enhancement config first, fall back to current config + let configToUse: ApiConfiguration = apiConfiguration + if (enhancementApiConfigId) { + const config = listApiConfigMeta?.find((c: ApiConfigMeta) => c.id === enhancementApiConfigId) + if (config?.name) { + const loadedConfig = await provider.providerSettingsManager.loadConfig(config.name) + if (loadedConfig.apiProvider) { + configToUse = loadedConfig + } + } + } + + const enhancedPrompt = await singleCompletionHandler( + configToUse, + supportPrompt.create( + "ENHANCE", + { + userInput: message.text, + }, + customSupportPrompts, + ), + ) + + await provider.postMessageToWebview({ + type: "enhancedPrompt", + text: enhancedPrompt, + }) + } catch (error) { + provider.outputChannel.appendLine( + `Error enhancing prompt: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, + ) + vscode.window.showErrorMessage(t("common:errors.enhance_prompt")) + await provider.postMessageToWebview({ + type: "enhancedPrompt", + }) + } + } + break + case "getSystemPrompt": + try { + const systemPrompt = await generateSystemPrompt(provider, message) + + await provider.postMessageToWebview({ + type: "systemPrompt", + text: systemPrompt, + mode: message.mode, + }) + } catch (error) { + provider.outputChannel.appendLine( + `Error getting system prompt: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, + ) + vscode.window.showErrorMessage(t("common:errors.get_system_prompt")) + } + break + case "copySystemPrompt": + try { + const systemPrompt = await generateSystemPrompt(provider, message) + + await vscode.env.clipboard.writeText(systemPrompt) + await vscode.window.showInformationMessage(t("common:info.clipboard_copy")) + } catch (error) { + provider.outputChannel.appendLine( + `Error getting system prompt: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, + ) + vscode.window.showErrorMessage(t("common:errors.get_system_prompt")) + } + break + case "searchCommits": { + const cwd = provider.cwd + if (cwd) { + try { + const commits = await searchCommits(message.query || "", cwd) + await provider.postMessageToWebview({ + type: "commitSearchResults", + commits, + }) + } catch (error) { + provider.outputChannel.appendLine( + `Error searching commits: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, + ) + vscode.window.showErrorMessage(t("common:errors.search_commits")) + } + } + break + } + case "searchFiles": { + const workspacePath = getWorkspacePath() + + if (!workspacePath) { + // Handle case where workspace path is not available + await provider.postMessageToWebview({ + type: "fileSearchResults", + results: [], + requestId: message.requestId, + error: "No workspace path available", + }) + break + } + try { + // Call file search service with query from message + const results = await searchWorkspaceFiles( + message.query || "", + workspacePath, + 20, // Use default limit, as filtering is now done in the backend + ) + + // Send results back to webview + await provider.postMessageToWebview({ + type: "fileSearchResults", + results, + requestId: message.requestId, + }) + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + + // Send error response to webview + await provider.postMessageToWebview({ + type: "fileSearchResults", + results: [], + error: errorMessage, + requestId: message.requestId, + }) + } + break + } + case "saveApiConfiguration": + if (message.text && message.apiConfiguration) { + try { + await provider.providerSettingsManager.saveConfig(message.text, message.apiConfiguration) + const listApiConfig = await provider.providerSettingsManager.listConfig() + await provider.updateGlobalState("listApiConfigMeta", listApiConfig) + } catch (error) { + provider.outputChannel.appendLine( + `Error save api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, + ) + vscode.window.showErrorMessage(t("common:errors.save_api_config")) + } + } + break + case "upsertApiConfiguration": + if (message.text && message.apiConfiguration) { + await provider.upsertApiConfiguration(message.text, message.apiConfiguration) + } + break + case "renameApiConfiguration": + if (message.values && message.apiConfiguration) { + try { + const { oldName, newName } = message.values + + if (oldName === newName) { + break + } + + // Load the old configuration to get its ID + const oldConfig = await provider.providerSettingsManager.loadConfig(oldName) + + // Create a new configuration with the same ID + const newConfig = { + ...message.apiConfiguration, + id: oldConfig.id, // Preserve the ID + } + + // Save with the new name but same ID + await provider.providerSettingsManager.saveConfig(newName, newConfig) + await provider.providerSettingsManager.deleteConfig(oldName) + + const listApiConfig = await provider.providerSettingsManager.listConfig() + + // Update listApiConfigMeta first to ensure UI has latest data + await provider.updateGlobalState("listApiConfigMeta", listApiConfig) + await provider.updateGlobalState("currentApiConfigName", newName) + + await provider.postStateToWebview() + } catch (error) { + provider.outputChannel.appendLine( + `Error rename api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, + ) + vscode.window.showErrorMessage(t("common:errors.rename_api_config")) + } + } + break + case "loadApiConfiguration": + if (message.text) { + try { + const apiConfig = await provider.providerSettingsManager.loadConfig(message.text) + const listApiConfig = await provider.providerSettingsManager.listConfig() + + await Promise.all([ + provider.updateGlobalState("listApiConfigMeta", listApiConfig), + provider.updateGlobalState("currentApiConfigName", message.text), + provider.updateApiConfiguration(apiConfig), + ]) + + await provider.postStateToWebview() + } catch (error) { + provider.outputChannel.appendLine( + `Error load api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, + ) + vscode.window.showErrorMessage(t("common:errors.load_api_config")) + } + } + break + case "loadApiConfigurationById": + if (message.text) { + try { + const { config: apiConfig, name } = await provider.providerSettingsManager.loadConfigById( + message.text, + ) + const listApiConfig = await provider.providerSettingsManager.listConfig() + + await Promise.all([ + provider.updateGlobalState("listApiConfigMeta", listApiConfig), + provider.updateGlobalState("currentApiConfigName", name), + provider.updateApiConfiguration(apiConfig), + ]) + + await provider.postStateToWebview() + } catch (error) { + provider.outputChannel.appendLine( + `Error load api configuration by ID: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, + ) + vscode.window.showErrorMessage(t("common:errors.load_api_config")) + } + } + break + case "deleteApiConfiguration": + if (message.text) { + const answer = await vscode.window.showInformationMessage( + t("common:confirmation.delete_config_profile"), + { modal: true }, + t("common:answers.yes"), + ) + + if (answer !== t("common:answers.yes")) { + break + } + + try { + await provider.providerSettingsManager.deleteConfig(message.text) + const listApiConfig = await provider.providerSettingsManager.listConfig() + + // Update listApiConfigMeta first to ensure UI has latest data + await provider.updateGlobalState("listApiConfigMeta", listApiConfig) + + // If this was the current config, switch to first available + const currentApiConfigName = provider.getGlobalState("currentApiConfigName") + + if (message.text === currentApiConfigName && listApiConfig?.[0]?.name) { + const apiConfig = await provider.providerSettingsManager.loadConfig(listApiConfig[0].name) + await Promise.all([ + provider.updateGlobalState("currentApiConfigName", listApiConfig[0].name), + provider.updateApiConfiguration(apiConfig), + ]) + } + + await provider.postStateToWebview() + } catch (error) { + provider.outputChannel.appendLine( + `Error delete api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, + ) + vscode.window.showErrorMessage(t("common:errors.delete_api_config")) + } + } + break + case "getListApiConfiguration": + try { + const listApiConfig = await provider.providerSettingsManager.listConfig() + await provider.updateGlobalState("listApiConfigMeta", listApiConfig) + provider.postMessageToWebview({ type: "listApiConfig", listApiConfig }) + } catch (error) { + provider.outputChannel.appendLine( + `Error get list api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, + ) + vscode.window.showErrorMessage(t("common:errors.list_api_config")) + } + break + case "updateExperimental": { + if (!message.values) { + break + } + + const updatedExperiments = { + ...(provider.getGlobalState("experiments") ?? experimentDefault), + ...message.values, + } + + await provider.updateGlobalState("experiments", updatedExperiments) + + const currentCline = provider.getCurrentCline() + + // Update diffStrategy in current Cline instance if it exists. + if (message.values[EXPERIMENT_IDS.DIFF_STRATEGY_UNIFIED] !== undefined && currentCline) { + await currentCline.updateDiffStrategy(updatedExperiments) + } + + await provider.postStateToWebview() + break + } + case "updateMcpTimeout": + if (message.serverName && typeof message.timeout === "number") { + try { + await provider.mcpHub?.updateServerTimeout( + message.serverName, + message.timeout, + message.source as "global" | "project", + ) + } catch (error) { + provider.outputChannel.appendLine( + `Failed to update timeout for ${message.serverName}: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, + ) + vscode.window.showErrorMessage(t("common:errors.update_server_timeout")) + } + } + break + case "updateCustomMode": + if (message.modeConfig) { + await provider.customModesManager.updateCustomMode(message.modeConfig.slug, message.modeConfig) + // Update state after saving the mode + const customModes = await provider.customModesManager.getCustomModes() + await provider.updateGlobalState("customModes", customModes) + await provider.updateGlobalState("mode", message.modeConfig.slug) + await provider.postStateToWebview() + } + break + case "deleteCustomMode": + if (message.slug) { + const answer = await vscode.window.showInformationMessage( + t("common:confirmation.delete_custom_mode"), + { modal: true }, + t("common:answers.yes"), + ) + + if (answer !== t("common:answers.yes")) { + break + } + + await provider.customModesManager.deleteCustomMode(message.slug) + // Switch back to default mode after deletion + await provider.updateGlobalState("mode", defaultModeSlug) + await provider.postStateToWebview() + } + break + case "humanRelayResponse": + if (message.requestId && message.text) { + vscode.commands.executeCommand("roo-cline.handleHumanRelayResponse", { + requestId: message.requestId, + text: message.text, + cancelled: false, + }) + } + break + + case "humanRelayCancel": + if (message.requestId) { + vscode.commands.executeCommand("roo-cline.handleHumanRelayResponse", { + requestId: message.requestId, + cancelled: true, + }) + } + break + + case "telemetrySetting": { + const telemetrySetting = message.text as TelemetrySetting + await provider.updateGlobalState("telemetrySetting", telemetrySetting) + const isOptedIn = telemetrySetting === "enabled" + telemetryService.updateTelemetryState(isOptedIn) + await provider.postStateToWebview() + break + } + } +} + +const generateSystemPrompt = async (provider: ClineProvider, message: WebviewMessage) => { + const { + apiConfiguration, + customModePrompts, + customInstructions, + browserViewportSize, + diffEnabled, + mcpEnabled, + fuzzyMatchThreshold, + experiments, + enableMcpServerCreation, + browserToolEnabled, + language, + } = await provider.getState() + + // Create diffStrategy based on current model and settings. + const diffStrategy = getDiffStrategy({ + model: apiConfiguration.apiModelId || apiConfiguration.openRouterModelId || "", + experiments, + fuzzyMatchThreshold, + }) + + const cwd = provider.cwd + + const mode = message.mode ?? defaultModeSlug + const customModes = await provider.customModesManager.getCustomModes() + + const rooIgnoreInstructions = provider.getCurrentCline()?.rooIgnoreController?.getInstructions() + + // Determine if browser tools can be used based on model support, mode, and user settings + let modelSupportsComputerUse = false + + // Create a temporary API handler to check if the model supports computer use + // This avoids relying on an active Cline instance which might not exist during preview + try { + const tempApiHandler = buildApiHandler(apiConfiguration) + modelSupportsComputerUse = tempApiHandler.getModel().info.supportsComputerUse ?? false + } catch (error) { + console.error("Error checking if model supports computer use:", error) + } + + // Check if the current mode includes the browser tool group + const modeConfig = getModeBySlug(mode, customModes) + const modeSupportsBrowser = modeConfig?.groups.some((group) => getGroupName(group) === "browser") ?? false + + // Only enable browser tools if the model supports it, the mode includes browser tools, + // and browser tools are enabled in settings + const canUseBrowserTool = modelSupportsComputerUse && modeSupportsBrowser && (browserToolEnabled ?? true) + + const systemPrompt = await SYSTEM_PROMPT( + provider.context, + cwd, + canUseBrowserTool, + mcpEnabled ? provider.mcpHub : undefined, + diffStrategy, + browserViewportSize ?? "900x600", + mode, + customModePrompts, + customModes, + customInstructions, + diffEnabled, + experiments, + enableMcpServerCreation, + language, + rooIgnoreInstructions, + ) + return systemPrompt +} From a6ed5ce0ea5852805aee714b0455d2d9ee6fa109 Mon Sep 17 00:00:00 2001 From: Chris Estreich Date: Mon, 31 Mar 2025 10:40:27 -0700 Subject: [PATCH 02/13] Update the webview when changing settings via the API (#2143) * Update the webview when changing settings via the API * Update src/exports/api.ts Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> --------- Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> --- .changeset/grumpy-pets-run.md | 5 +++++ src/exports/api.ts | 9 +-------- src/exports/interface.ts | 14 -------------- src/exports/roo-code.d.ts | 12 ------------ 4 files changed, 6 insertions(+), 34 deletions(-) create mode 100644 .changeset/grumpy-pets-run.md diff --git a/.changeset/grumpy-pets-run.md b/.changeset/grumpy-pets-run.md new file mode 100644 index 0000000000..271f580fc6 --- /dev/null +++ b/.changeset/grumpy-pets-run.md @@ -0,0 +1,5 @@ +--- +"roo-cline": patch +--- + +Update the webview when changing settings via the API diff --git a/src/exports/api.ts b/src/exports/api.ts index 9794d89028..d37d0880eb 100644 --- a/src/exports/api.ts +++ b/src/exports/api.ts @@ -82,16 +82,9 @@ export class API extends EventEmitter implements RooCodeAPI { return this.provider.getValues() } - public getConfigurationValue(key: K) { - return this.provider.getValue(key) - } - public async setConfiguration(values: RooCodeSettings) { await this.provider.setValues(values) - } - - public async setConfigurationValue(key: K, value: RooCodeSettings[K]) { - await this.provider.setValue(key, value) + await this.provider.postStateToWebview() } public isReady() { diff --git a/src/exports/interface.ts b/src/exports/interface.ts index de1f7ea15c..33eb39aa4e 100644 --- a/src/exports/interface.ts +++ b/src/exports/interface.ts @@ -67,26 +67,12 @@ export interface RooCodeAPI extends EventEmitter { */ getConfiguration(): RooCodeSettings - /** - * Returns the value of a configuration key. - * @param key The key of the configuration value to return. - * @returns The value of the configuration key. - */ - getConfigurationValue(key: K): RooCodeSettings[K] - /** * Sets the configuration for the current task. * @param values An object containing key-value pairs to set. */ setConfiguration(values: RooCodeSettings): Promise - /** - * Sets the value of a configuration key. - * @param key The key of the configuration value to set. - * @param value The value to set. - */ - setConfigurationValue(key: K, value: RooCodeSettings[K]): Promise - /** * Returns true if the API is ready to use. */ diff --git a/src/exports/roo-code.d.ts b/src/exports/roo-code.d.ts index 2804ab47f0..dac6d1728e 100644 --- a/src/exports/roo-code.d.ts +++ b/src/exports/roo-code.d.ts @@ -457,23 +457,11 @@ interface RooCodeAPI extends EventEmitter { * @returns The current configuration. */ getConfiguration(): RooCodeSettings - /** - * Returns the value of a configuration key. - * @param key The key of the configuration value to return. - * @returns The value of the configuration key. - */ - getConfigurationValue(key: K): RooCodeSettings[K] /** * Sets the configuration for the current task. * @param values An object containing key-value pairs to set. */ setConfiguration(values: RooCodeSettings): Promise - /** - * Sets the value of a configuration key. - * @param key The key of the configuration value to set. - * @param value The value to set. - */ - setConfigurationValue(key: K, value: RooCodeSettings[K]): Promise /** * Returns true if the API is ready to use. */ From 136cf998855e3646d44c71d00e468286345a8d6f Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Mon, 31 Mar 2025 13:51:21 -0400 Subject: [PATCH 03/13] Add requesty to connect-src (#2155) --- .changeset/hot-garlics-join.md | 5 +++++ src/core/webview/ClineProvider.ts | 2 +- src/core/webview/__tests__/ClineProvider.test.ts | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 .changeset/hot-garlics-join.md diff --git a/.changeset/hot-garlics-join.md b/.changeset/hot-garlics-join.md new file mode 100644 index 0000000000..18e007a43f --- /dev/null +++ b/.changeset/hot-garlics-join.md @@ -0,0 +1,5 @@ +--- +"roo-cline": patch +--- + +Fix bug in loading Requesty key balance diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index a7fc450b0e..ff223250b0 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -705,7 +705,7 @@ export class ClineProvider extends EventEmitter implements - +