diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 657e4a9ab6..3e8ed51668 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -1262,6 +1262,10 @@ export class ClineProvider implements vscode.WebviewViewProvider { await this.updateGlobalState("browserViewportSize", browserViewportSize) await this.postStateToWebview() break + case "remoteBrowserHost": + await this.updateGlobalState("remoteBrowserHost", message.text) + await this.postStateToWebview() + break case "fuzzyMatchThreshold": await this.updateGlobalState("fuzzyMatchThreshold", message.value) await this.postStateToWebview() @@ -2188,6 +2192,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { soundVolume, browserViewportSize, screenshotQuality, + remoteBrowserHost, preferredLanguage, writeDelayMs, terminalOutputLimit, @@ -2246,6 +2251,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { soundVolume: soundVolume ?? 0.5, browserViewportSize: browserViewportSize ?? "900x600", screenshotQuality: screenshotQuality ?? 75, + remoteBrowserHost, preferredLanguage: preferredLanguage ?? "English", writeDelayMs: writeDelayMs ?? 1000, terminalOutputLimit: terminalOutputLimit ?? TERMINAL_OUTPUT_LIMIT, @@ -2399,6 +2405,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { soundVolume: stateValues.soundVolume, browserViewportSize: stateValues.browserViewportSize ?? "900x600", screenshotQuality: stateValues.screenshotQuality ?? 75, + remoteBrowserHost: stateValues.remoteBrowserHost, fuzzyMatchThreshold: stateValues.fuzzyMatchThreshold ?? 1.0, writeDelayMs: stateValues.writeDelayMs ?? 1000, terminalOutputLimit: stateValues.terminalOutputLimit ?? TERMINAL_OUTPUT_LIMIT, diff --git a/src/services/browser/BrowserSession.ts b/src/services/browser/BrowserSession.ts index bed0332244..6fe25ddf5b 100644 --- a/src/services/browser/BrowserSession.ts +++ b/src/services/browser/BrowserSession.ts @@ -1,11 +1,12 @@ import * as vscode from "vscode" import * as fs from "fs/promises" import * as path from "path" -import { Browser, Page, ScreenshotOptions, TimeoutError, launch } from "puppeteer-core" +import { Browser, Page, ScreenshotOptions, TimeoutError, launch, connect } from "puppeteer-core" // @ts-ignore import PCR from "puppeteer-chromium-resolver" import pWaitFor from "p-wait-for" import delay from "delay" +import axios from "axios" import { fileExistsAtPath } from "../../utils/fs" import { BrowserActionResult } from "../../shared/ExtensionMessage" @@ -52,6 +53,41 @@ export class BrowserSession { await this.closeBrowser() // this may happen when the model launches a browser again after having used it already before } + const remoteBrowserHost = this.context.globalState.get("remoteBrowserHost") as string | undefined + + if (remoteBrowserHost) { + console.log(`Attempting to connect to remote browser at ${remoteBrowserHost}`) + try { + // Fetch the WebSocket endpoint from the Chrome DevTools Protocol + const versionUrl = `${remoteBrowserHost.replace(/\/$/, "")}/json/version` + console.log(`Fetching WebSocket endpoint from ${versionUrl}`) + + const response = await axios.get(versionUrl) + const browserWSEndpoint = response.data.webSocketDebuggerUrl + + if (!browserWSEndpoint) { + throw new Error("Could not find webSocketDebuggerUrl in the response") + } + + console.log(`Found WebSocket endpoint: ${browserWSEndpoint}`) + + this.browser = await connect({ + browserWSEndpoint, + defaultViewport: (() => { + const size = + (this.context.globalState.get("browserViewportSize") as string | undefined) || "900x600" + const [width, height] = size.split("x").map(Number) + return { width, height } + })(), + }) + this.page = await this.browser?.newPage() + return + } catch (error) { + console.error(`Failed to connect to remote browser: ${error}`) + // Fall back to local browser if remote connection fails + } + } + const stats = await this.ensureChromiumExists() this.browser = await stats.puppeteer.launch({ args: [ @@ -72,7 +108,14 @@ export class BrowserSession { async closeBrowser(): Promise { if (this.browser || this.page) { console.log("closing browser...") - await this.browser?.close().catch(() => {}) + + const remoteBrowserHost = this.context.globalState.get("remoteBrowserHost") as string | undefined + if (remoteBrowserHost && this.browser) { + await this.browser.disconnect().catch(() => {}) + } else { + await this.browser?.close().catch(() => {}) + } + this.browser = undefined this.page = undefined this.currentMousePosition = undefined diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index b7e3d850cf..5b638e6534 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -123,6 +123,7 @@ export interface ExtensionState { checkpointStorage: CheckpointStorage browserViewportSize?: string screenshotQuality?: number + remoteBrowserHost?: string fuzzyMatchThreshold?: number preferredLanguage: string writeDelayMs: number diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 216c7588d7..df5c7d29f9 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -57,6 +57,7 @@ export interface WebviewMessage { | "checkpointStorage" | "browserViewportSize" | "screenshotQuality" + | "remoteBrowserHost" | "openMcpSettings" | "restartMcpServer" | "toggleToolAlwaysAllow" diff --git a/src/shared/globalState.ts b/src/shared/globalState.ts index 35e53bbe9c..05f54bfb8b 100644 --- a/src/shared/globalState.ts +++ b/src/shared/globalState.ts @@ -66,6 +66,7 @@ export const GLOBAL_STATE_KEYS = [ "checkpointStorage", "browserViewportSize", "screenshotQuality", + "remoteBrowserHost", "fuzzyMatchThreshold", "preferredLanguage", // Language setting for Cline's communication "writeDelayMs", diff --git a/webview-ui/src/components/settings/BrowserSettings.tsx b/webview-ui/src/components/settings/BrowserSettings.tsx index 1ff0a36b9c..ab4a88113a 100644 --- a/webview-ui/src/components/settings/BrowserSettings.tsx +++ b/webview-ui/src/components/settings/BrowserSettings.tsx @@ -12,13 +12,17 @@ type BrowserSettingsProps = HTMLAttributes & { browserToolEnabled?: boolean browserViewportSize?: string screenshotQuality?: number - setCachedStateField: SetCachedStateField<"browserToolEnabled" | "browserViewportSize" | "screenshotQuality"> + remoteBrowserHost?: string + setCachedStateField: SetCachedStateField< + "browserToolEnabled" | "browserViewportSize" | "screenshotQuality" | "remoteBrowserHost" + > } export const BrowserSettings = ({ browserToolEnabled, browserViewportSize, screenshotQuality, + remoteBrowserHost, setCachedStateField, ...props }: BrowserSettingsProps) => { @@ -96,6 +100,33 @@ export const BrowserSettings = ({ screenshots but increase token usage.

+
+ + + setCachedStateField("remoteBrowserHost", e.target.value || undefined) + } + /> +

+ Connect to a remote Chrome browser by providing the DevTools Protocol host address. + Roo will automatically fetch the WebSocket endpoint from this address. If provided, + Roo will use this browser instead of launching a local one. Leave empty to use the + built-in browser. +

+
)} diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index 7cafbd6663..0b8cce9b87 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -77,6 +77,7 @@ const SettingsView = forwardRef(({ onDone }, mcpEnabled, rateLimitSeconds, requestDelaySeconds, + remoteBrowserHost, screenshotQuality, soundEnabled, soundVolume, @@ -172,6 +173,7 @@ const SettingsView = forwardRef(({ onDone }, vscode.postMessage({ type: "enableCheckpoints", bool: enableCheckpoints }) vscode.postMessage({ type: "checkpointStorage", text: checkpointStorage }) vscode.postMessage({ type: "browserViewportSize", text: browserViewportSize }) + vscode.postMessage({ type: "remoteBrowserHost", text: remoteBrowserHost }) vscode.postMessage({ type: "fuzzyMatchThreshold", value: fuzzyMatchThreshold ?? 1.0 }) vscode.postMessage({ type: "writeDelayMs", value: writeDelayMs }) vscode.postMessage({ type: "screenshotQuality", value: screenshotQuality ?? 75 }) @@ -378,6 +380,7 @@ const SettingsView = forwardRef(({ onDone }, browserToolEnabled={browserToolEnabled} browserViewportSize={browserViewportSize} screenshotQuality={screenshotQuality} + remoteBrowserHost={remoteBrowserHost} setCachedStateField={setCachedStateField} />