Browse Source

Split webviewMessageHandler out of ClineProvider.ts (#2149)

Diarmid Mackenzie 9 months ago
parent
commit
73fe581868
2 changed files with 1434 additions and 1432 deletions
  1. 24 1432
      src/core/webview/ClineProvider.ts
  2. 1410 0
      src/core/webview/webviewMessageHandler.ts

File diff suppressed because it is too large
+ 24 - 1432
src/core/webview/ClineProvider.ts


+ 1410 - 0
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<string, boolean> = { ...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
+}

Some files were not shown because too many files changed in this diff