diff --git a/package.json b/package.json index 520d51cb28..99f58c324f 100644 --- a/package.json +++ b/package.json @@ -235,45 +235,11 @@ "configuration": { "title": "Cline", "properties": { - "cline.vsCodeLmModelSelector": { - "type": "object", - "properties": { - "vendor": { - "type": "string", - "description": "The vendor of the language model (e.g. copilot)" - }, - "family": { - "type": "string", - "description": "The family of the language model (e.g. gpt-4)" - } - }, - "description": "Settings for VSCode Language Model API" - }, "cline.enableCheckpoints": { "type": "boolean", "default": true, "description": "Enables extension to save checkpoints of workspace throughout the task. Uses git under the hood which may not work well with large workspaces." }, - "cline.disableBrowserTool": { - "type": "boolean", - "default": false, - "description": "Disables extension from spawning browser session." - }, - "cline.modelSettings.o3Mini.reasoningEffort": { - "type": "string", - "enum": [ - "low", - "medium", - "high" - ], - "default": "medium", - "description": "Controls the reasoning effort when using an OpenAI reasoning model. Higher values may result in more thorough but slower responses." - }, - "cline.chromeExecutablePath": { - "type": "string", - "default": null, - "description": "Path to Chrome executable for browser use functionality. If not set, the extension will attempt to find or download it automatically." - }, "cline.preferredLanguage": { "type": "string", "enum": [ diff --git a/proto/browser.proto b/proto/browser.proto index b5542f0af3..0f26b19d97 100644 --- a/proto/browser.proto +++ b/proto/browser.proto @@ -40,6 +40,8 @@ message BrowserSettings { Viewport viewport = 1; optional string remote_browser_host = 2; optional bool remote_browser_enabled = 3; + optional string chrome_executable_path = 4; + optional bool disable_tool_use = 5; } message UpdateBrowserSettingsRequest { @@ -47,4 +49,6 @@ message UpdateBrowserSettingsRequest { Viewport viewport = 2; optional string remote_browser_host = 3; optional bool remote_browser_enabled = 4; + optional string chrome_executable_path = 5; + optional bool disable_tool_use = 6; } diff --git a/src/core/controller/browser/updateBrowserSettings.ts b/src/core/controller/browser/updateBrowserSettings.ts index c0db61aef7..1c8bc938ef 100644 --- a/src/core/controller/browser/updateBrowserSettings.ts +++ b/src/core/controller/browser/updateBrowserSettings.ts @@ -1,8 +1,8 @@ import { UpdateBrowserSettingsRequest } from "../../../shared/proto/browser" import { Boolean } from "../../../shared/proto/common" import { Controller } from "../index" -import { updateGlobalState } from "../../storage/state" -import { BrowserSettings as SharedBrowserSettings } from "../../../shared/BrowserSettings" +import { updateGlobalState, getGlobalState } from "../../storage/state" +import { BrowserSettings as SharedBrowserSettings, DEFAULT_BROWSER_SETTINGS } from "../../../shared/BrowserSettings" /** * Update browser settings @@ -12,23 +12,39 @@ import { BrowserSettings as SharedBrowserSettings } from "../../../shared/Browse */ export async function updateBrowserSettings(controller: Controller, request: UpdateBrowserSettingsRequest): Promise { try { - // Convert from protobuf format to shared format - const browserSettings: SharedBrowserSettings = { + // Get current browser settings to preserve fields not in the request + const currentSettings = (await getGlobalState(controller.context, "browserSettings")) as SharedBrowserSettings | undefined + const mergedWithDefaults = { ...DEFAULT_BROWSER_SETTINGS, ...currentSettings } + + // Convert from protobuf format to shared format, merging with existing settings + const newBrowserSettings: SharedBrowserSettings = { + ...mergedWithDefaults, // Start with existing settings (and defaults) viewport: { - width: request.viewport?.width || 900, - height: request.viewport?.height || 600, + // Apply updates from request + width: request.viewport?.width || mergedWithDefaults.viewport.width, + height: request.viewport?.height || mergedWithDefaults.viewport.height, }, - remoteBrowserEnabled: request.remoteBrowserEnabled || false, - remoteBrowserHost: request.remoteBrowserHost || undefined, + // Explicitly handle optional boolean and string fields from the request + remoteBrowserEnabled: + request.remoteBrowserEnabled === undefined + ? mergedWithDefaults.remoteBrowserEnabled + : request.remoteBrowserEnabled, + remoteBrowserHost: + request.remoteBrowserHost === undefined ? mergedWithDefaults.remoteBrowserHost : request.remoteBrowserHost, + chromeExecutablePath: + // If chromeExecutablePath is explicitly in the request (even as ""), use it. + // Otherwise, fall back to mergedWithDefaults. + "chromeExecutablePath" in request ? request.chromeExecutablePath : mergedWithDefaults.chromeExecutablePath, + disableToolUse: request.disableToolUse === undefined ? mergedWithDefaults.disableToolUse : request.disableToolUse, } // Update global state with new settings - await updateGlobalState(controller.context, "browserSettings", browserSettings) + await updateGlobalState(controller.context, "browserSettings", newBrowserSettings) // Update task browser settings if task exists if (controller.task) { - controller.task.browserSettings = browserSettings - controller.task.browserSession.browserSettings = browserSettings + controller.task.browserSettings = newBrowserSettings + controller.task.browserSession.browserSettings = newBrowserSettings } // Post updated state to webview diff --git a/src/core/task/index.ts b/src/core/task/index.ts index a1daa87ad1..53c5b9fcc7 100644 --- a/src/core/task/index.ts +++ b/src/core/task/index.ts @@ -1442,13 +1442,28 @@ export class Task { return statusCode && !message.includes(statusCode.toString()) ? `${statusCode} - ${message}` : message } + /** + * Migrates the disableBrowserTool setting from VSCode configuration to browserSettings + */ + private async migrateDisableBrowserToolSetting(): Promise { + const config = vscode.workspace.getConfiguration("cline") + const disableBrowserTool = vscode.workspace.getConfiguration("cline").get("disableBrowserTool") + + if (disableBrowserTool !== undefined) { + this.browserSettings.disableToolUse = disableBrowserTool + // Remove from VSCode configuration + await config.update("disableBrowserTool", undefined, true) + } + } + async *attemptApiRequest(previousApiReqIndex: number): ApiStream { // Wait for MCP servers to be connected before generating system prompt await pWaitFor(() => this.mcpHub.isConnecting !== true, { timeout: 10_000 }).catch(() => { console.error("MCP servers failed to connect in time") }) - const disableBrowserTool = vscode.workspace.getConfiguration("cline").get("disableBrowserTool") ?? false + await this.migrateDisableBrowserToolSetting() + const disableBrowserTool = this.browserSettings.disableToolUse ?? false // cline browser tool uses image recognition for navigation (requires model image support). const modelSupportsBrowserUse = this.api.getModel().info.supportsImages ?? false diff --git a/src/services/browser/BrowserSession.ts b/src/services/browser/BrowserSession.ts index 4c793d648e..19fb10d52f 100644 --- a/src/services/browser/BrowserSession.ts +++ b/src/services/browser/BrowserSession.ts @@ -67,11 +67,25 @@ export class BrowserSession { } } - async getDetectedChromePath(): Promise<{ path: string; isBundled: boolean }> { - // First check VSCode config + /** + * Migrates the chromeExecutablePath setting from VSCode configuration to browserSettings + */ + private async migrateChromeExecutablePathSetting(): Promise { + const config = vscode.workspace.getConfiguration("cline") const configPath = vscode.workspace.getConfiguration("cline").get("chromeExecutablePath") - if (configPath && (await fileExistsAtPath(configPath))) { - return { path: configPath, isBundled: false } + + if (configPath !== undefined) { + this.browserSettings.chromeExecutablePath = configPath + // Remove from VSCode configuration + await config.update("chromeExecutablePath", undefined, true) + } + } + + async getDetectedChromePath(): Promise<{ path: string; isBundled: boolean }> { + // First check browserSettings (from UI, stored in global state) + await this.migrateChromeExecutablePathSetting() + if (this.browserSettings.chromeExecutablePath && (await fileExistsAtPath(this.browserSettings.chromeExecutablePath))) { + return { path: this.browserSettings.chromeExecutablePath, isBundled: false } } // Then try to find system Chrome diff --git a/src/shared/BrowserSettings.ts b/src/shared/BrowserSettings.ts index db2a1d25be..e22a172f12 100644 --- a/src/shared/BrowserSettings.ts +++ b/src/shared/BrowserSettings.ts @@ -8,6 +8,8 @@ export interface BrowserSettings { // chromeType: "chromium" | "system" remoteBrowserHost?: string remoteBrowserEnabled?: boolean + chromeExecutablePath?: string + disableToolUse?: boolean } export const DEFAULT_BROWSER_SETTINGS: BrowserSettings = { @@ -17,7 +19,9 @@ export const DEFAULT_BROWSER_SETTINGS: BrowserSettings = { }, remoteBrowserEnabled: false, remoteBrowserHost: "http://localhost:9222", + chromeExecutablePath: "", // Changed from undefined to empty string // chromeType: "chromium", + disableToolUse: false, } export const BROWSER_VIEWPORT_PRESETS = { diff --git a/src/shared/proto/browser.ts b/src/shared/proto/browser.ts index 27d24b0ae1..8458d95738 100644 --- a/src/shared/proto/browser.ts +++ b/src/shared/proto/browser.ts @@ -36,6 +36,8 @@ export interface BrowserSettings { viewport?: Viewport | undefined remoteBrowserHost?: string | undefined remoteBrowserEnabled?: boolean | undefined + chromeExecutablePath?: string | undefined + disableToolUse?: boolean | undefined } export interface UpdateBrowserSettingsRequest { @@ -43,6 +45,8 @@ export interface UpdateBrowserSettingsRequest { viewport?: Viewport | undefined remoteBrowserHost?: string | undefined remoteBrowserEnabled?: boolean | undefined + chromeExecutablePath?: string | undefined + disableToolUse?: boolean | undefined } function createBaseBrowserConnectionInfo(): BrowserConnectionInfo { @@ -382,7 +386,13 @@ export const Viewport: MessageFns = { } function createBaseBrowserSettings(): BrowserSettings { - return { viewport: undefined, remoteBrowserHost: undefined, remoteBrowserEnabled: undefined } + return { + viewport: undefined, + remoteBrowserHost: undefined, + remoteBrowserEnabled: undefined, + chromeExecutablePath: undefined, + disableToolUse: undefined, + } } export const BrowserSettings: MessageFns = { @@ -396,6 +406,12 @@ export const BrowserSettings: MessageFns = { if (message.remoteBrowserEnabled !== undefined) { writer.uint32(24).bool(message.remoteBrowserEnabled) } + if (message.chromeExecutablePath !== undefined) { + writer.uint32(34).string(message.chromeExecutablePath) + } + if (message.disableToolUse !== undefined) { + writer.uint32(40).bool(message.disableToolUse) + } return writer }, @@ -430,6 +446,22 @@ export const BrowserSettings: MessageFns = { message.remoteBrowserEnabled = reader.bool() continue } + case 4: { + if (tag !== 34) { + break + } + + message.chromeExecutablePath = reader.string() + continue + } + case 5: { + if (tag !== 40) { + break + } + + message.disableToolUse = reader.bool() + continue + } } if ((tag & 7) === 4 || tag === 0) { break @@ -446,6 +478,8 @@ export const BrowserSettings: MessageFns = { remoteBrowserEnabled: isSet(object.remoteBrowserEnabled) ? globalThis.Boolean(object.remoteBrowserEnabled) : undefined, + chromeExecutablePath: isSet(object.chromeExecutablePath) ? globalThis.String(object.chromeExecutablePath) : undefined, + disableToolUse: isSet(object.disableToolUse) ? globalThis.Boolean(object.disableToolUse) : undefined, } }, @@ -460,6 +494,12 @@ export const BrowserSettings: MessageFns = { if (message.remoteBrowserEnabled !== undefined) { obj.remoteBrowserEnabled = message.remoteBrowserEnabled } + if (message.chromeExecutablePath !== undefined) { + obj.chromeExecutablePath = message.chromeExecutablePath + } + if (message.disableToolUse !== undefined) { + obj.disableToolUse = message.disableToolUse + } return obj }, @@ -472,12 +512,21 @@ export const BrowserSettings: MessageFns = { object.viewport !== undefined && object.viewport !== null ? Viewport.fromPartial(object.viewport) : undefined message.remoteBrowserHost = object.remoteBrowserHost ?? undefined message.remoteBrowserEnabled = object.remoteBrowserEnabled ?? undefined + message.chromeExecutablePath = object.chromeExecutablePath ?? undefined + message.disableToolUse = object.disableToolUse ?? undefined return message }, } function createBaseUpdateBrowserSettingsRequest(): UpdateBrowserSettingsRequest { - return { metadata: undefined, viewport: undefined, remoteBrowserHost: undefined, remoteBrowserEnabled: undefined } + return { + metadata: undefined, + viewport: undefined, + remoteBrowserHost: undefined, + remoteBrowserEnabled: undefined, + chromeExecutablePath: undefined, + disableToolUse: undefined, + } } export const UpdateBrowserSettingsRequest: MessageFns = { @@ -494,6 +543,12 @@ export const UpdateBrowserSettingsRequest: MessageFns { - // Test browser session setting - await vscode.workspace.getConfiguration().update("cline.disableBrowserTool", true, true) - const updatedConfig = vscode.workspace.getConfiguration("cline") - expect(updatedConfig.get("disableBrowserTool")).to.be.true - - // Reset settings - await vscode.workspace.getConfiguration().update("cline.disableBrowserTool", undefined, true) - }) }) diff --git a/webview-ui/src/components/settings/BrowserSettingsSection.tsx b/webview-ui/src/components/settings/BrowserSettingsSection.tsx index 0ddcb10d30..05c3cb5d1e 100644 --- a/webview-ui/src/components/settings/BrowserSettingsSection.tsx +++ b/webview-ui/src/components/settings/BrowserSettingsSection.tsx @@ -37,8 +37,22 @@ const ConnectionStatusIndicator = ({ ) } +const CollapsibleContent = styled.div<{ isOpen: boolean }>` + overflow: hidden; + transition: + max-height 0.3s ease-in-out, + opacity 0.3s ease-in-out, + margin-top 0.3s ease-in-out, + visibility 0.3s ease-in-out; + max-height: ${({ isOpen }) => (isOpen ? "1000px" : "0")}; // Sufficiently large height + opacity: ${({ isOpen }) => (isOpen ? 1 : 0)}; + margin-top: ${({ isOpen }) => (isOpen ? "15px" : "0")}; + visibility: ${({ isOpen }) => (isOpen ? "visible" : "hidden")}; +` + export const BrowserSettingsSection: React.FC = () => { const { browserSettings } = useExtensionState() + const [localChromePath, setLocalChromePath] = useState(browserSettings.chromeExecutablePath || "") const [isCheckingConnection, setIsCheckingConnection] = useState(false) const [connectionStatus, setConnectionStatus] = useState(null) const [relaunchResult, setRelaunchResult] = useState<{ success: boolean; message: string } | null>(null) @@ -91,6 +105,14 @@ export const BrowserSettingsSection: React.FC = () => { }) }, []) + // Sync localChromePath with global state + useEffect(() => { + if (browserSettings.chromeExecutablePath !== localChromePath) { + setLocalChromePath(browserSettings.chromeExecutablePath || "") + } + // Removed sync for local disableToolUse state + }, [browserSettings.chromeExecutablePath, browserSettings.disableToolUse]) + // Debounced connection check function const debouncedCheckConnection = useCallback( debounce(() => { @@ -147,6 +169,8 @@ export const BrowserSettingsSection: React.FC = () => { }, remoteBrowserEnabled: browserSettings.remoteBrowserEnabled, remoteBrowserHost: browserSettings.remoteBrowserHost, + chromeExecutablePath: browserSettings.chromeExecutablePath, + disableToolUse: browserSettings.disableToolUse, }) .then((response) => { if (!response.value) { @@ -169,6 +193,8 @@ export const BrowserSettingsSection: React.FC = () => { remoteBrowserEnabled: enabled, // If disabling, also clear the host remoteBrowserHost: enabled ? browserSettings.remoteBrowserHost : undefined, + chromeExecutablePath: browserSettings.chromeExecutablePath, + disableToolUse: browserSettings.disableToolUse, }) .then((response) => { if (!response.value) { @@ -189,6 +215,55 @@ export const BrowserSettingsSection: React.FC = () => { }, remoteBrowserEnabled: browserSettings.remoteBrowserEnabled, remoteBrowserHost: host, + chromeExecutablePath: browserSettings.chromeExecutablePath, + disableToolUse: browserSettings.disableToolUse, + }) + .then((response) => { + if (!response.value) { + console.error("Failed to update browser settings") + } + }) + .catch((error) => { + console.error("Error updating browser settings:", error) + }) + } + + const debouncedUpdateChromePath = useCallback( + debounce((newPath: string | undefined) => { + BrowserServiceClient.updateBrowserSettings({ + metadata: {}, + viewport: { + width: browserSettings.viewport.width, + height: browserSettings.viewport.height, + }, + remoteBrowserEnabled: browserSettings.remoteBrowserEnabled, + remoteBrowserHost: browserSettings.remoteBrowserHost, + chromeExecutablePath: newPath, + disableToolUse: browserSettings.disableToolUse, + }) + .then((response) => { + if (!response.value) { + console.error("Failed to update browser settings for chromeExecutablePath") + } + }) + .catch((error) => { + console.error("Error updating browser settings for chromeExecutablePath:", error) + }) + }, 500), + [browserSettings], + ) + + const updateChromeExecutablePath = (path: string | undefined) => { + BrowserServiceClient.updateBrowserSettings({ + metadata: {}, + viewport: { + width: browserSettings.viewport.width, + height: browserSettings.viewport.height, + }, + remoteBrowserEnabled: browserSettings.remoteBrowserEnabled, + remoteBrowserHost: browserSettings.remoteBrowserHost, + chromeExecutablePath: path, + disableToolUse: browserSettings.disableToolUse, }) .then((response) => { if (!response.value) { @@ -247,6 +322,28 @@ export const BrowserSettingsSection: React.FC = () => { return () => clearInterval(pollInterval) }, [browserSettings.remoteBrowserEnabled, checkConnectionOnce]) + const updateDisableToolUse = (disabled: boolean) => { + BrowserServiceClient.updateBrowserSettings({ + metadata: {}, + viewport: { + width: browserSettings.viewport.width, + height: browserSettings.viewport.height, + }, + remoteBrowserEnabled: browserSettings.remoteBrowserEnabled, + remoteBrowserHost: browserSettings.remoteBrowserHost, + chromeExecutablePath: browserSettings.chromeExecutablePath, + disableToolUse: disabled, + }) + .then((response) => { + if (!response.value) { + console.error("Failed to update disableToolUse setting") + } + }) + .catch((error) => { + console.error("Error updating disableToolUse setting:", error) + }) + } + const relaunchChromeDebugMode = () => { setDebugMode(true) setRelaunchResult(null) @@ -260,121 +357,169 @@ export const BrowserSettingsSection: React.FC = () => { // Determine if we should show the relaunch button const isRemoteEnabled = Boolean(browserSettings.remoteBrowserEnabled) const shouldShowRelaunchButton = isRemoteEnabled && connectionStatus === false + const isSubSettingsOpen = !(browserSettings.disableToolUse || false) return (

Browser Settings

-
-
- - { - const typedSize = size as { width: number; height: number } - return ( - typedSize.width === browserSettings.viewport.width && - typedSize.height === browserSettings.viewport.height - ) - })?.[0] - } - onChange={(event) => handleViewportChange(event as Event)}> - {Object.entries(BROWSER_VIEWPORT_PRESETS).map(([name]) => ( - - {name} - - ))} - -
+ + {/* Master Toggle */} +
+ updateDisableToolUse((e.target as HTMLInputElement).checked)}> + Disable browser tool usage +

- Set the size of the browser viewport for screenshots and interactions. + Prevent Cline from using browser actions (e.g. launch, click, type).

-
-
- updateRemoteBrowserEnabled((e.target as HTMLInputElement).checked)}> - Use remote browser connection - - + +
+
+ + { + const typedSize = size as { width: number; height: number } + return ( + typedSize.width === browserSettings.viewport.width && + typedSize.height === browserSettings.viewport.height + ) + })?.[0] + } + onChange={(event) => handleViewportChange(event as Event)}> + {Object.entries(BROWSER_VIEWPORT_PRESETS).map(([name]) => ( + + {name} + + ))} + +
+

+ Set the size of the browser viewport for screenshots and interactions. +

-

- Enable Cline to use your Chrome - {isBundled ? "(not detected on your machine)" : detectedChromePath ? ` (${detectedChromePath})` : ""}. This - requires starting Chrome in debug mode - {browserSettings.remoteBrowserEnabled ? ( - <> - {" "} - manually (--remote-debugging-port=9222) or using the button below. Enter the host address - or leave it blank for automatic discovery. - - ) : ( - "." - )} -

- {browserSettings.remoteBrowserEnabled && ( -
- updateRemoteBrowserHost(e.target.value || undefined)} +
+ {" "} + {/* This div now contains Remote Connection & Chrome Path */} +
+ updateRemoteBrowserEnabled((e.target as HTMLInputElement).checked)}> + Use remote browser connection + + - - {shouldShowRelaunchButton && ( -
- - {debugMode ? "Relaunching Browser..." : "Relaunch Browser with Debug Mode"} - -
+
+

+ Enable Cline to use your Chrome + {isBundled ? "(not detected on your machine)" : detectedChromePath ? ` (${detectedChromePath})` : ""}. You + can specify a custom path below. Using a remote browser connection requires starting Chrome in debug mode + {browserSettings.remoteBrowserEnabled ? ( + <> + {" "} + manually (--remote-debugging-port=9222) or using the button below. Enter the host + address or leave it blank for automatic discovery. + + ) : ( + "." )} +

+ {/* Moved remote-specific settings to appear directly after enabling remote connection */} + {browserSettings.remoteBrowserEnabled && ( +
+ updateRemoteBrowserHost(e.target.value || undefined)} + /> - {relaunchResult && ( -
+ + {debugMode ? "Relaunching Browser..." : "Relaunch Browser with Debug Mode"} + +
+ )} + + {relaunchResult && ( +
+ {relaunchResult.message} +
+ )} + +

- {relaunchResult.message} -

- )} - + fontSize: "12px", + color: "var(--vscode-descriptionForeground)", + margin: 0, + }}>

+
+ )} + {/* Chrome Executable Path section now follows remote-specific settings */} +
+ + { + const newValue = e.target.value || "" + setLocalChromePath(newValue) + debouncedUpdateChromePath(newValue) // Send "" if empty, not undefined + }} + />

+ margin: "4px 0 0 0", + }}> + Leave blank to auto-detect. +

- )} -
+
+
) }