Browse Source

Add support for remote browser connection and settings

Afshawn Lotfi 11 months ago
parent
commit
22f51304a2

+ 7 - 0
src/core/webview/ClineProvider.ts

@@ -1262,6 +1262,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 						await this.updateGlobalState("browserViewportSize", browserViewportSize)
 						await this.updateGlobalState("browserViewportSize", browserViewportSize)
 						await this.postStateToWebview()
 						await this.postStateToWebview()
 						break
 						break
+					case "remoteBrowserHost":
+						await this.updateGlobalState("remoteBrowserHost", message.text)
+						await this.postStateToWebview()
+						break
 					case "fuzzyMatchThreshold":
 					case "fuzzyMatchThreshold":
 						await this.updateGlobalState("fuzzyMatchThreshold", message.value)
 						await this.updateGlobalState("fuzzyMatchThreshold", message.value)
 						await this.postStateToWebview()
 						await this.postStateToWebview()
@@ -2188,6 +2192,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			soundVolume,
 			soundVolume,
 			browserViewportSize,
 			browserViewportSize,
 			screenshotQuality,
 			screenshotQuality,
+			remoteBrowserHost,
 			preferredLanguage,
 			preferredLanguage,
 			writeDelayMs,
 			writeDelayMs,
 			terminalOutputLimit,
 			terminalOutputLimit,
@@ -2246,6 +2251,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			soundVolume: soundVolume ?? 0.5,
 			soundVolume: soundVolume ?? 0.5,
 			browserViewportSize: browserViewportSize ?? "900x600",
 			browserViewportSize: browserViewportSize ?? "900x600",
 			screenshotQuality: screenshotQuality ?? 75,
 			screenshotQuality: screenshotQuality ?? 75,
+			remoteBrowserHost,
 			preferredLanguage: preferredLanguage ?? "English",
 			preferredLanguage: preferredLanguage ?? "English",
 			writeDelayMs: writeDelayMs ?? 1000,
 			writeDelayMs: writeDelayMs ?? 1000,
 			terminalOutputLimit: terminalOutputLimit ?? TERMINAL_OUTPUT_LIMIT,
 			terminalOutputLimit: terminalOutputLimit ?? TERMINAL_OUTPUT_LIMIT,
@@ -2399,6 +2405,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			soundVolume: stateValues.soundVolume,
 			soundVolume: stateValues.soundVolume,
 			browserViewportSize: stateValues.browserViewportSize ?? "900x600",
 			browserViewportSize: stateValues.browserViewportSize ?? "900x600",
 			screenshotQuality: stateValues.screenshotQuality ?? 75,
 			screenshotQuality: stateValues.screenshotQuality ?? 75,
+			remoteBrowserHost: stateValues.remoteBrowserHost,
 			fuzzyMatchThreshold: stateValues.fuzzyMatchThreshold ?? 1.0,
 			fuzzyMatchThreshold: stateValues.fuzzyMatchThreshold ?? 1.0,
 			writeDelayMs: stateValues.writeDelayMs ?? 1000,
 			writeDelayMs: stateValues.writeDelayMs ?? 1000,
 			terminalOutputLimit: stateValues.terminalOutputLimit ?? TERMINAL_OUTPUT_LIMIT,
 			terminalOutputLimit: stateValues.terminalOutputLimit ?? TERMINAL_OUTPUT_LIMIT,

+ 45 - 2
src/services/browser/BrowserSession.ts

@@ -1,11 +1,12 @@
 import * as vscode from "vscode"
 import * as vscode from "vscode"
 import * as fs from "fs/promises"
 import * as fs from "fs/promises"
 import * as path from "path"
 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
 // @ts-ignore
 import PCR from "puppeteer-chromium-resolver"
 import PCR from "puppeteer-chromium-resolver"
 import pWaitFor from "p-wait-for"
 import pWaitFor from "p-wait-for"
 import delay from "delay"
 import delay from "delay"
+import axios from "axios"
 import { fileExistsAtPath } from "../../utils/fs"
 import { fileExistsAtPath } from "../../utils/fs"
 import { BrowserActionResult } from "../../shared/ExtensionMessage"
 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
 			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()
 		const stats = await this.ensureChromiumExists()
 		this.browser = await stats.puppeteer.launch({
 		this.browser = await stats.puppeteer.launch({
 			args: [
 			args: [
@@ -72,7 +108,14 @@ export class BrowserSession {
 	async closeBrowser(): Promise<BrowserActionResult> {
 	async closeBrowser(): Promise<BrowserActionResult> {
 		if (this.browser || this.page) {
 		if (this.browser || this.page) {
 			console.log("closing browser...")
 			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.browser = undefined
 			this.page = undefined
 			this.page = undefined
 			this.currentMousePosition = undefined
 			this.currentMousePosition = undefined

+ 1 - 0
src/shared/ExtensionMessage.ts

@@ -123,6 +123,7 @@ export interface ExtensionState {
 	checkpointStorage: CheckpointStorage
 	checkpointStorage: CheckpointStorage
 	browserViewportSize?: string
 	browserViewportSize?: string
 	screenshotQuality?: number
 	screenshotQuality?: number
+	remoteBrowserHost?: string
 	fuzzyMatchThreshold?: number
 	fuzzyMatchThreshold?: number
 	preferredLanguage: string
 	preferredLanguage: string
 	writeDelayMs: number
 	writeDelayMs: number

+ 1 - 0
src/shared/WebviewMessage.ts

@@ -57,6 +57,7 @@ export interface WebviewMessage {
 		| "checkpointStorage"
 		| "checkpointStorage"
 		| "browserViewportSize"
 		| "browserViewportSize"
 		| "screenshotQuality"
 		| "screenshotQuality"
+		| "remoteBrowserHost"
 		| "openMcpSettings"
 		| "openMcpSettings"
 		| "restartMcpServer"
 		| "restartMcpServer"
 		| "toggleToolAlwaysAllow"
 		| "toggleToolAlwaysAllow"

+ 1 - 0
src/shared/globalState.ts

@@ -66,6 +66,7 @@ export const GLOBAL_STATE_KEYS = [
 	"checkpointStorage",
 	"checkpointStorage",
 	"browserViewportSize",
 	"browserViewportSize",
 	"screenshotQuality",
 	"screenshotQuality",
+	"remoteBrowserHost",
 	"fuzzyMatchThreshold",
 	"fuzzyMatchThreshold",
 	"preferredLanguage", // Language setting for Cline's communication
 	"preferredLanguage", // Language setting for Cline's communication
 	"writeDelayMs",
 	"writeDelayMs",

+ 32 - 1
webview-ui/src/components/settings/BrowserSettings.tsx

@@ -12,13 +12,17 @@ type BrowserSettingsProps = HTMLAttributes<HTMLDivElement> & {
 	browserToolEnabled?: boolean
 	browserToolEnabled?: boolean
 	browserViewportSize?: string
 	browserViewportSize?: string
 	screenshotQuality?: number
 	screenshotQuality?: number
-	setCachedStateField: SetCachedStateField<"browserToolEnabled" | "browserViewportSize" | "screenshotQuality">
+	remoteBrowserHost?: string
+	setCachedStateField: SetCachedStateField<
+		"browserToolEnabled" | "browserViewportSize" | "screenshotQuality" | "remoteBrowserHost"
+	>
 }
 }
 
 
 export const BrowserSettings = ({
 export const BrowserSettings = ({
 	browserToolEnabled,
 	browserToolEnabled,
 	browserViewportSize,
 	browserViewportSize,
 	screenshotQuality,
 	screenshotQuality,
+	remoteBrowserHost,
 	setCachedStateField,
 	setCachedStateField,
 	...props
 	...props
 }: BrowserSettingsProps) => {
 }: BrowserSettingsProps) => {
@@ -96,6 +100,33 @@ export const BrowserSettings = ({
 									screenshots but increase token usage.
 									screenshots but increase token usage.
 								</p>
 								</p>
 							</div>
 							</div>
+							<div className="mt-4">
+								<label style={{ fontWeight: "500", display: "block", marginBottom: 5 }}>
+									Remote Chrome DevTools Host (optional)
+								</label>
+								<input
+									type="text"
+									value={remoteBrowserHost ?? ""}
+									placeholder="http://localhost:9222"
+									style={{
+										width: "100%",
+										padding: "4px 8px",
+										backgroundColor: "var(--vscode-input-background)",
+										color: "var(--vscode-input-foreground)",
+										border: "1px solid var(--vscode-input-border)",
+										borderRadius: "2px",
+									}}
+									onChange={(e) =>
+										setCachedStateField("remoteBrowserHost", e.target.value || undefined)
+									}
+								/>
+								<p className="text-vscode-descriptionForeground text-sm mt-0">
+									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.
+								</p>
+							</div>
 						</div>
 						</div>
 					)}
 					)}
 				</div>
 				</div>

+ 3 - 0
webview-ui/src/components/settings/SettingsView.tsx

@@ -77,6 +77,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
 		mcpEnabled,
 		mcpEnabled,
 		rateLimitSeconds,
 		rateLimitSeconds,
 		requestDelaySeconds,
 		requestDelaySeconds,
+		remoteBrowserHost,
 		screenshotQuality,
 		screenshotQuality,
 		soundEnabled,
 		soundEnabled,
 		soundVolume,
 		soundVolume,
@@ -172,6 +173,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
 			vscode.postMessage({ type: "enableCheckpoints", bool: enableCheckpoints })
 			vscode.postMessage({ type: "enableCheckpoints", bool: enableCheckpoints })
 			vscode.postMessage({ type: "checkpointStorage", text: checkpointStorage })
 			vscode.postMessage({ type: "checkpointStorage", text: checkpointStorage })
 			vscode.postMessage({ type: "browserViewportSize", text: browserViewportSize })
 			vscode.postMessage({ type: "browserViewportSize", text: browserViewportSize })
+			vscode.postMessage({ type: "remoteBrowserHost", text: remoteBrowserHost })
 			vscode.postMessage({ type: "fuzzyMatchThreshold", value: fuzzyMatchThreshold ?? 1.0 })
 			vscode.postMessage({ type: "fuzzyMatchThreshold", value: fuzzyMatchThreshold ?? 1.0 })
 			vscode.postMessage({ type: "writeDelayMs", value: writeDelayMs })
 			vscode.postMessage({ type: "writeDelayMs", value: writeDelayMs })
 			vscode.postMessage({ type: "screenshotQuality", value: screenshotQuality ?? 75 })
 			vscode.postMessage({ type: "screenshotQuality", value: screenshotQuality ?? 75 })
@@ -378,6 +380,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
 						browserToolEnabled={browserToolEnabled}
 						browserToolEnabled={browserToolEnabled}
 						browserViewportSize={browserViewportSize}
 						browserViewportSize={browserViewportSize}
 						screenshotQuality={screenshotQuality}
 						screenshotQuality={screenshotQuality}
+						remoteBrowserHost={remoteBrowserHost}
 						setCachedStateField={setCachedStateField}
 						setCachedStateField={setCachedStateField}
 					/>
 					/>
 				</div>
 				</div>