Compare commits

...
Author SHA1 Message Date
Andrei Edell f6ab672efb info styling, close browser tab, hide headless info 2025-03-27 09:26:46 -10:00
Andrei Edell 5dbeaab8ae Revert "remove duplicated message & prettier fix"
This reverts commit dcefef35aa.
2025-03-27 07:14:10 -10:00
Andrei Edell dcefef35aa remove duplicated message & prettier fix 2025-03-26 11:54:29 -10:00
Andrei Edell 5d4611f1e7 start of info panel popover 2025-03-26 11:46:21 -10:00
canvrno aa3a6157d0 Disable notifications in browser 2025-03-26 11:00:12 -07:00
canvrno 67c8e636e6 headless browser fix (#2451) 2025-03-25 21:33:29 -10:00
Andrei Edell b2098c9a26 respect viewport size in remote host 2025-03-25 20:25:31 -10:00
Andrei Edell d51ef68597 Remove sketchy network scanning code 2025-03-25 19:13:21 -10:00
Andrei Edell 4afb302e5b clarify settings descriptions 2025-03-25 18:50:01 -10:00
Andrei Edell c37a27c546 fix linter issue 2025-03-25 16:39:05 -10:00
Andrei Edell d3f66bf293 fix a merge conflict resolution error 2025-03-25 16:16:35 -10:00
54d145a57d Features to relaunch browser in debug, test connection (#2440)
* Features to Relaunch browser in debug, test connection

* Update src/services/browser/BrowserSession.ts

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* Update webview-ui/src/components/browser/BrowserSettingsMenu.tsx

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

---------

Co-authored-by: Andrei Edell <garoth@gmail.com>
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-03-25 16:08:50 -10:00
canvrnoandAndrei Edell 714c528dd8 added feature to detect and display chrome path as placeholder in browser settings (#2442)
Co-authored-by: Andrei Edell <garoth@gmail.com>
2025-03-25 16:03:44 -10:00
Andrei EdellandAndrei Edell 84913554dd Sub-PR of hugelung/remote_browser: clicking browser widget's gear opens basic settings & scrolls down with a highlight (#2439)
* first version of scrolling to browser settings

* really nice generic scroll to settings & highlight

* formatting & changeset

---------

Co-authored-by: Andrei Edell <andrei@nugbase.com>
2025-03-25 16:02:03 -10:00
Andrei Edell 0160b18a56 rename incorrect headless mode to 'local mode' 2025-03-25 10:25:42 -10:00
Andrei Edell 173020f2cd correct chrome path description, remove some old comments, and rename headless mode to local mode 2025-03-24 18:58:21 -10:00
Andrei Edell 2f37207e2b changeset & prettier 2025-03-24 18:46:02 -10:00
Andrei Edell 8dbd25cf6e move the browser settings into regular settings 2025-03-24 18:46:02 -10:00
Andrei Edell b6c590d0c4 clean up auto-detect vs specified path 2025-03-24 18:46:02 -10:00
Andrei Edell e51b39e7f2 successfully open remote chrome 2025-03-24 18:46:02 -10:00
Andrei Edell 78bb82ace8 manual port 2025-03-24 18:45:54 -10:00
18 changed files with 1129 additions and 316 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Feature to open basic settings & scroll a section into view with a highlight animation
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Disable notifications in browser
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Features to Relaunch browser in debug, test connection
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Remote browser control via devtools protocol
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Added feature to detect installed versions of chromium and display them as a placeholder if not already explicitly configured by the user
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Fix for headless browser mode
+11
View File
@@ -232,6 +232,16 @@
"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.remoteBrowserEnabled": {
"type": "boolean",
"default": false,
"description": "Enable connection to a remote Chrome browser with remote debugging enabled (--remote-debugging-port=9222)."
},
"cline.remoteBrowserHost": {
"type": "string",
"default": "http://localhost:9222",
"description": "URL of the remote Chrome browser's DevTools Protocol endpoint. Leave empty for auto-discovery."
},
"cline.preferredLanguage": {
"type": "string",
"enum": [
@@ -336,6 +346,7 @@
"axios": "^1.8.2",
"cheerio": "^1.0.0",
"chokidar": "^4.0.1",
"chrome-launcher": "^1.1.2",
"clone-deep": "^4.0.1",
"default-shell": "^2.2.0",
"diff": "^5.2.0",
+216 -6
View File
@@ -41,6 +41,8 @@ import { getTotalTasksSize } from "../../utils/storage"
import { ConversationTelemetryService } from "../../services/telemetry/ConversationTelemetryService"
import { GlobalFileNames } from "../../global-constants"
import { setTimeout as setTimeoutPromise } from "node:timers/promises"
import { BrowserSession } from "../../services/browser/BrowserSession"
import { discoverChromeInstances } from "../../services/browser/BrowserDiscovery"
/*
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
@@ -115,6 +117,8 @@ type GlobalStateKey =
| "asksageApiUrl"
| "thinkingBudgetTokens"
| "planActSeparateModelsSetting"
| "remoteBrowserHost"
| "remoteBrowserEnabled"
export class ClineProvider implements vscode.WebviewViewProvider {
public static readonly sideBarId = "claude-dev.SidebarProvider" // used in package.json as the view's id. This value cannot be changed due to how vscode caches views based on their id, and updating the id would break existing instances of the extension.
@@ -574,6 +578,140 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await this.postStateToWebview()
}
break
case "remoteBrowserHost":
await this.updateGlobalState("remoteBrowserHost", message.text)
await this.postStateToWebview()
break
case "remoteBrowserEnabled":
// Store the preference in global state
// remoteBrowserEnabled now means "enable remote browser connection"
await this.updateGlobalState("remoteBrowserEnabled", message.bool ?? false)
// If disabling remote browser connection, clear the remoteBrowserHost
if (!message.bool) {
await this.updateGlobalState("remoteBrowserHost", undefined)
}
await this.postStateToWebview()
break
case "getBrowserConnectionInfo":
try {
// Get the current browser session from Cline if it exists
if (this.cline?.browserSession) {
const connectionInfo = this.cline.browserSession.getConnectionInfo();
await this.postMessageToWebview({
type: "browserConnectionInfo",
isConnected: connectionInfo.isConnected,
isRemote: connectionInfo.isRemote,
host: connectionInfo.host,
isHeadless: connectionInfo.isHeadless
});
} else {
// If no active browser session, just return the settings
const { browserSettings } = await this.getState();
await this.postMessageToWebview({
type: "browserConnectionInfo",
isConnected: false,
isRemote: !!browserSettings.remoteBrowserEnabled,
host: browserSettings.remoteBrowserHost,
isHeadless: !!browserSettings.headless
});
}
} catch (error) {
console.error("Error getting browser connection info:", error);
await this.postMessageToWebview({
type: "browserConnectionInfo",
isConnected: false,
isRemote: false,
isHeadless: true
});
}
break;
case "testBrowserConnection":
try {
const { browserSettings } = await this.getState()
const browserSession = new BrowserSession(this.context, browserSettings)
// If no text is provided, try auto-discovery
if (!message.text) {
try {
const discoveredHost = await discoverChromeInstances()
if (discoveredHost) {
// Test the connection to the discovered host
const result = await browserSession.testConnection(discoveredHost)
// Send the result back to the webview
await this.postMessageToWebview({
type: "browserConnectionResult",
success: result.success,
text: `Auto-discovered and tested connection to Chrome at ${discoveredHost}: ${result.message}`,
values: { endpoint: result.endpoint },
})
} else {
await this.postMessageToWebview({
type: "browserConnectionResult",
success: false,
text: "No Chrome instances found on the network. Make sure Chrome is running with remote debugging enabled (--remote-debugging-port=9222).",
})
}
} catch (error) {
await this.postMessageToWebview({
type: "browserConnectionResult",
success: false,
text: `Error during auto-discovery: ${error instanceof Error ? error.message : String(error)}`,
})
}
} else {
// Test the provided URL
const result = await browserSession.testConnection(message.text)
// Send the result back to the webview
await this.postMessageToWebview({
type: "browserConnectionResult",
success: result.success,
text: result.message,
values: { endpoint: result.endpoint },
})
}
} catch (error) {
await this.postMessageToWebview({
type: "browserConnectionResult",
success: false,
text: `Error testing connection: ${error instanceof Error ? error.message : String(error)}`,
})
}
break
case "discoverBrowser":
try {
const discoveredHost = await discoverChromeInstances()
if (discoveredHost) {
// Don't update the remoteBrowserHost state when auto-discovering
// This way we don't override the user's preference
// Test the connection to get the endpoint
const { browserSettings } = await this.getState()
const browserSession = new BrowserSession(this.context, browserSettings)
const result = await browserSession.testConnection(discoveredHost)
// Send the result back to the webview
await this.postMessageToWebview({
type: "browserConnectionResult",
success: true,
text: `Successfully discovered and connected to Chrome at ${discoveredHost}`,
values: { endpoint: result.endpoint },
})
} else {
await this.postMessageToWebview({
type: "browserConnectionResult",
success: false,
text: "No Chrome instances found on the network. Make sure Chrome is running with remote debugging enabled (--remote-debugging-port=9222).",
})
}
} catch (error) {
await this.postMessageToWebview({
type: "browserConnectionResult",
success: false,
text: `Error discovering browser: ${error instanceof Error ? error.message : String(error)}`,
})
}
break
case "togglePlanActMode":
if (message.chatSettings) {
await this.togglePlanActModeWithChatSettings(message.chatSettings, message.chatContent)
@@ -586,11 +724,11 @@ export class ClineProvider implements vscode.WebviewViewProvider {
text: message.text,
})
break
// case "relaunchChromeDebugMode":
// if (this.cline) {
// this.cline.browserSession.relaunchChromeDebugMode()
// }
// break
case "relaunchChromeDebugMode":
const { browserSettings } = await this.getState()
const browserSession = new BrowserSession(this.context, browserSettings)
await browserSession.relaunchChromeDebugMode(webview)
break
case "askResponse":
this.cline?.handleWebviewAskResponse(message.askResponse!, message.text, message.images)
break
@@ -901,6 +1039,13 @@ export class ClineProvider implements vscode.WebviewViewProvider {
})
break
}
case "scrollToSettings": {
await this.postMessageToWebview({
type: "scrollToSettings",
text: message.text,
})
break
}
case "telemetrySetting": {
if (message.telemetrySetting) {
await this.updateTelemetrySetting(message.telemetrySetting)
@@ -938,6 +1083,55 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.postMessageToWebview({ type: "relinquishControl" })
break
}
case "getBrowserConnectionInfo": {
try {
// Get the current browser session from Cline if it exists
if (this.cline?.browserSession) {
const connectionInfo = this.cline.browserSession.getConnectionInfo();
await this.postMessageToWebview({
type: "browserConnectionInfo",
isConnected: connectionInfo.isConnected,
isRemote: connectionInfo.isRemote,
host: connectionInfo.host,
isHeadless: connectionInfo.isHeadless
});
} else {
// If no active browser session, just return the settings
const { browserSettings } = await this.getState();
await this.postMessageToWebview({
type: "browserConnectionInfo",
isConnected: false,
isRemote: !!browserSettings.remoteBrowserEnabled,
host: browserSettings.remoteBrowserHost,
isHeadless: !!browserSettings.headless
});
}
} catch (error) {
console.error("Error getting browser connection info:", error);
await this.postMessageToWebview({
type: "browserConnectionInfo",
isConnected: false,
isRemote: false,
isHeadless: true
});
}
break;
}
case "getDetectedChromePath": {
try {
const { browserSettings } = await this.getState()
const browserSession = new BrowserSession(this.context, browserSettings)
const { path, isBundled } = await browserSession.getDetectedChromePath()
await this.postMessageToWebview({
type: "detectedChromePath",
text: path,
isBundled,
})
} catch (error) {
console.error("Error getting detected Chrome path:", error)
}
break
}
// Add more switch case statements here as more webview message commands
// are created within the webview context (i.e. inside media/main.js)
}
@@ -2083,6 +2277,11 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
*/
async getState() {
// Read settings from VSCode configuration
const config = vscode.workspace.getConfiguration("cline")
const configRemoteBrowserEnabled = config.get<boolean>("remoteBrowserEnabled")
const configRemoteBrowserHost = config.get<string>("remoteBrowserHost")
const [
storedApiProvider,
apiModelId,
@@ -2147,6 +2346,8 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
thinkingBudgetTokens,
sambanovaApiKey,
planActSeparateModelsSettingRaw,
remoteBrowserEnabled,
remoteBrowserHost,
] = await Promise.all([
this.getGlobalState("apiProvider") as Promise<ApiProvider | undefined>,
this.getGlobalState("apiModelId") as Promise<string | undefined>,
@@ -2211,6 +2412,8 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
this.getGlobalState("thinkingBudgetTokens") as Promise<number | undefined>,
this.getSecret("sambanovaApiKey") as Promise<string | undefined>,
this.getGlobalState("planActSeparateModelsSetting") as Promise<boolean | undefined>,
this.getGlobalState("remoteBrowserEnabled") as Promise<boolean | undefined>,
this.getGlobalState("remoteBrowserHost") as Promise<string | undefined>,
])
let apiProvider: ApiProvider
@@ -2251,6 +2454,13 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
await this.updateGlobalState("planActSeparateModelsSetting", planActSeparateModelsSetting)
}
// Merge browser settings with configuration values
const mergedBrowserSettings = {
...(browserSettings || DEFAULT_BROWSER_SETTINGS),
remoteBrowserEnabled: remoteBrowserEnabled ?? configRemoteBrowserEnabled ?? false,
remoteBrowserHost: remoteBrowserHost ?? configRemoteBrowserHost ?? "http://localhost:9222",
}
return {
apiConfiguration: {
apiProvider,
@@ -2308,7 +2518,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
customInstructions,
taskHistory,
autoApprovalSettings: autoApprovalSettings || DEFAULT_AUTO_APPROVAL_SETTINGS, // default value can be 0 or empty string
browserSettings: browserSettings || DEFAULT_BROWSER_SETTINGS,
browserSettings: mergedBrowserSettings,
chatSettings: chatSettings || DEFAULT_CHAT_SETTINGS,
userInfo,
previousModeApiProvider,
+108
View File
@@ -0,0 +1,108 @@
import * as net from "net"
import axios from "axios"
/**
* Check if a port is open on a given host
*/
export async function isPortOpen(host: string, port: number, timeout = 1000): Promise<boolean> {
return new Promise((resolve) => {
const socket = new net.Socket()
let status = false
// Set timeout
socket.setTimeout(timeout)
// Handle successful connection
socket.on("connect", () => {
status = true
socket.destroy()
})
// Handle any errors
socket.on("error", () => {
socket.destroy()
})
// Handle timeout
socket.on("timeout", () => {
socket.destroy()
})
// Handle close
socket.on("close", () => {
resolve(status)
})
// Attempt to connect
socket.connect(port, host)
})
}
/**
* Try to connect to Chrome at a specific IP address
*/
export async function tryConnect(ipAddress: string): Promise<{ endpoint: string; ip: string } | null> {
try {
console.log(`Trying to connect to Chrome at: http://${ipAddress}:9222/json/version`)
const response = await axios.get(`http://${ipAddress}:9222/json/version`, { timeout: 1000 })
const data = response.data
return { endpoint: data.webSocketDebuggerUrl, ip: ipAddress }
} catch (error) {
return null
}
}
/**
* Discover Chrome instances on the network
* Simplified to only check localhost
*/
export async function discoverChromeInstances(): Promise<string | null> {
// Only try localhost
const ipAddresses = ["localhost", "127.0.0.1"]
console.log("Checking for Chrome on localhost")
// Try connecting to each IP address
for (const ip of ipAddresses) {
const connection = await tryConnect(ip)
if (connection) {
console.log(`Successfully connected to Chrome at: ${connection.ip}`)
console.log(`✅ Found Chrome at ${connection.ip}`)
return `http://${connection.ip}:9222`
}
}
return null
}
/**
* Test connection to a remote browser
*/
export async function testBrowserConnection(host: string): Promise<{ success: boolean; message: string; endpoint?: string }> {
try {
// Fetch the WebSocket endpoint from the Chrome DevTools Protocol
const versionUrl = `${host.replace(/\/$/, "")}/json/version`
console.log(`Testing connection to ${versionUrl}`)
const response = await axios.get(versionUrl, { timeout: 3000 })
const browserWSEndpoint = response.data.webSocketDebuggerUrl
if (!browserWSEndpoint) {
return {
success: false,
message: "Could not find webSocketDebuggerUrl in the response",
}
}
return {
success: true,
message: "Successfully connected to Chrome browser",
endpoint: browserWSEndpoint,
}
} catch (error) {
console.error(`Failed to connect to remote browser: ${error}`)
return {
success: false,
message: `Failed to connect: ${error instanceof Error ? error.message : String(error)}`,
}
}
}
+229 -98
View File
@@ -1,36 +1,89 @@
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 { setTimeout as setTimeoutPromise } from "node:timers/promises"
import axios from "axios"
import { fileExistsAtPath } from "../../utils/fs"
import { BrowserActionResult } from "../../shared/ExtensionMessage"
import { BrowserSettings } from "../../shared/BrowserSettings"
// import * as chromeLauncher from "chrome-launcher"
import { discoverChromeInstances, testBrowserConnection } from "./BrowserDiscovery"
import * as chromeLauncher from "chrome-launcher"
interface PCRStats {
puppeteer: { launch: typeof launch }
executablePath: string
}
// const DEBUG_PORT = 9222 // Chrome's default debugging port
// Define browser connection info interface
export interface BrowserConnectionInfo {
isConnected: boolean
isRemote: boolean
host?: string
isHeadless: boolean
}
const DEBUG_PORT = 9222 // Chrome's default debugging port
export class BrowserSession {
private context: vscode.ExtensionContext
private browser?: Browser
private page?: Page
private currentMousePosition?: string
private cachedWebSocketEndpoint?: string
private lastConnectionAttempt: number = 0
browserSettings: BrowserSettings
private isConnectedToRemote: boolean = false
constructor(context: vscode.ExtensionContext, browserSettings: BrowserSettings) {
this.context = context
this.browserSettings = browserSettings
}
private async ensureChromiumExists(): Promise<PCRStats> {
// Tests remote browser connection
async testConnection(host: string): Promise<{ success: boolean; message: string; endpoint?: string }> {
return testBrowserConnection(host)
}
/**
* Get current browser connection information
*/
getConnectionInfo(): BrowserConnectionInfo {
return {
isConnected: !!this.browser,
isRemote: this.isConnectedToRemote,
host: this.isConnectedToRemote ? this.browserSettings.remoteBrowserHost : undefined,
isHeadless: this.browserSettings.headless
}
}
async getDetectedChromePath(): Promise<{ path: string; isBundled: boolean }> {
// First check VSCode config
const configPath = vscode.workspace.getConfiguration("cline").get<string>("chromeExecutablePath")
if (configPath && (await fileExistsAtPath(configPath))) {
return { path: configPath, isBundled: false }
}
// Then try to find system Chrome
try {
const systemPath = chromeLauncher.Launcher.getFirstInstallation()
// Add validation to ensure path is not in Trash - This can happen on Mac OS due to the way the chrome-launcher library works
if (systemPath && !systemPath.includes(".Trash") && (await fileExistsAtPath(systemPath))) {
return { path: systemPath, isBundled: false }
}
} catch (error) {
console.log("Could not find system Chrome:", error)
}
// Finally fall back to PCR's bundled version
const stats = await this.ensureChromiumExists()
return { path: stats.executablePath, isBundled: true }
}
async ensureChromiumExists(): Promise<PCRStats> {
const globalStoragePath = this.context?.globalStorageUri?.fsPath
if (!globalStoragePath) {
throw new Error("Global storage uri is invalid")
@@ -42,130 +95,208 @@ export class BrowserSession {
await fs.mkdir(puppeteerDir, { recursive: true })
}
const chromeExecutablePath = vscode.workspace.getConfiguration("cline").get<string>("chromeExecutablePath")
if (chromeExecutablePath && !(await fileExistsAtPath(chromeExecutablePath))) {
throw new Error(`Chrome executable not found at path: ${chromeExecutablePath}`)
}
const stats: PCRStats = chromeExecutablePath
? { puppeteer: require("puppeteer-core"), executablePath: chromeExecutablePath }
: // if chromium doesn't exist, this will download it to path.join(puppeteerDir, ".chromium-browser-snapshots")
// if it does exist it will return the path to existing chromium
await PCR({ downloadPath: puppeteerDir })
// if chromium doesn't exist, this will download it to path.join(puppeteerDir, ".chromium-browser-snapshots")
// if it does exist it will return the path to existing chromium
const stats = await PCR({ downloadPath: puppeteerDir })
return stats
}
// private async checkExistingChromeDebugger(): Promise<boolean> {
// try {
// // Try to connect to existing debugger
// const response = await fetch(`http://localhost:${DEBUG_PORT}/json/version`)
// return response.ok
// } catch {
// return false
// }
// }
async relaunchChromeDebugMode(webview?: vscode.Webview) {
const result = await vscode.window.showWarningMessage(
"This will close your existing Chrome tabs and relaunch Chrome in debug mode. Are you sure?",
{ modal: true },
"Yes",
)
// async relaunchChromeDebugMode() {
// const result = await vscode.window.showWarningMessage(
// "This will close your existing Chrome tabs and relaunch Chrome in debug mode. Are you sure?",
// { modal: true },
// "Yes",
// )
if (result !== "Yes") {
webview?.postMessage({ type: "browserRelaunchResult", success: false, text: "Operation cancelled by user" })
return
}
// if (result !== "Yes") {
// return
// }
try {
// Kill any existing Chrome instances
await chromeLauncher.killAll()
// // // Kill any existing Chrome instances
// // await chromeLauncher.killAll()
const chromeFlags = [
"--remote-debugging-port=" + DEBUG_PORT,
"--no-first-run",
"--no-default-browser-check",
"--disable-notifications",
]
// // // Launch Chrome with debug port
// // const launcher = new chromeLauncher.Launcher({
// // port: DEBUG_PORT,
// // chromeFlags: ["--remote-debugging-port=" + DEBUG_PORT, "--no-first-run", "--no-default-browser-check"],
// // })
if (this.browserSettings.headless) {
chromeFlags.push("--headless")
}
// // await launcher.launch()
// const installation = chromeLauncher.Launcher.getFirstInstallation()
// if (!installation) {
// throw new Error("Could not find Chrome installation on this system")
// }
// console.log("chrome installation", installation)
// }
// Launch Chrome with debug port
const launcher = new chromeLauncher.Launcher({
port: DEBUG_PORT,
chromeFlags: chromeFlags,
})
// private async getSystemChromeExecutablePath(): Promise<string> {
// // Find installed Chrome
// const installation = chromeLauncher.Launcher.getFirstInstallation()
// if (!installation) {
// throw new Error("Could not find Chrome installation on this system")
// }
// console.log("chrome installation", installation)
// return installation
// }
await launcher.launch()
const installation = chromeLauncher.Launcher.getFirstInstallation()
if (!installation) {
throw new Error("Could not find Chrome installation on this system")
}
console.log("chrome installation", installation)
// /**
// * Helper to detect users default Chrome data dir.
// * Adjust for OS if needed.
// */
// private getDefaultChromeUserDataDir(): string {
// const homedir = require("os").homedir()
// switch (process.platform) {
// case "win32":
// return path.join(homedir, "AppData", "Local", "Google", "Chrome", "User Data")
// case "darwin":
// return path.join(homedir, "Library", "Application Support", "Google", "Chrome")
// default:
// return path.join(homedir, ".config", "google-chrome")
// }
// }
webview?.postMessage({
type: "browserRelaunchResult",
success: true,
text: `Browser successfully launched in debug mode${this.browserSettings.headless ? " (headless)" : ""}`,
})
} catch (error) {
webview?.postMessage({
type: "browserRelaunchResult",
success: false,
text: `Failed to relaunch Chrome: ${error instanceof Error ? error.message : String(error)}`,
})
}
}
async launchBrowser() {
console.log("launch browser called")
if (this.browser) {
// throw new Error("Browser already launched")
await this.closeBrowser() // this may happen when the model launches a browser again after having used it already before
}
const stats = await this.ensureChromiumExists()
this.browser = await stats.puppeteer.launch({
// Reset remote connection status
this.isConnectedToRemote = false
if (this.browserSettings.remoteBrowserEnabled) {
console.log(`launch browser called -- remote host mode (headless: ${this.browserSettings.headless})`)
try {
await this.launchRemoteBrowser()
// Don't create a new page here, as we'll create it in launchRemoteBrowser
return
} catch (error) {
console.error("Failed to launch remote browser, falling back to local mode:", error)
await this.launchLocalBrowser()
}
} else {
console.log(`launch browser called -- local mode (headless: ${this.browserSettings.headless})`)
await this.launchLocalBrowser()
}
this.page = await this.browser?.newPage()
}
async launchLocalBrowser() {
const { path } = await this.getDetectedChromePath()
this.browser = await require("puppeteer-core").launch({
args: [
"--user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36",
],
executablePath: stats.executablePath,
executablePath: path,
defaultViewport: this.browserSettings.viewport,
headless: this.browserSettings.headless,
headless: this.browserSettings.headless ? "shell" : false,
})
this.isConnectedToRemote = false
}
// if (this.browserSettings.chromeType === "system") {
// const userDataDir = this.getDefaultChromeUserDataDir()
// this.browser = await stats.puppeteer.launch({
// args: [`--user-data-dir=${userDataDir}`, "--profile-directory=Default"],
// executablePath: await this.getSystemChromeExecutablePath(),
// defaultViewport: this.browserSettings.viewport,
// headless: this.browserSettings.headless,
// })
// } else {
// this.browser = await stats.puppeteer.launch({
// args: [
// "--user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36",
// ],
// executablePath: stats.executablePath,
// defaultViewport: this.browserSettings.viewport,
// headless: this.browserSettings.headless,
// })
// }
async launchRemoteBrowser() {
let remoteBrowserHost = this.browserSettings.remoteBrowserHost
let browserWSEndpoint: string | undefined = this.cachedWebSocketEndpoint
let reconnectionAttempted = false
// (latest version of puppeteer does not add headless to user agent)
this.page = await this.browser?.newPage()
const getViewport = () => {
return this.browserSettings.viewport
}
// First try auto-discovery if no host is provided
if (!remoteBrowserHost) {
try {
console.log("No remote browser host provided, trying auto-discovery")
const discoveredHost = await discoverChromeInstances()
if (discoveredHost) {
console.log(`Auto-discovered Chrome at ${discoveredHost}`)
remoteBrowserHost = discoveredHost
}
} catch (error) {
console.log(`Auto-discovery failed: ${error}`)
}
}
// Try to connect with cached endpoint first if it exists and is recent (less than 1 hour old)
if (browserWSEndpoint && Date.now() - this.lastConnectionAttempt < 3600000) {
try {
console.log(`Attempting to connect using cached WebSocket endpoint: ${browserWSEndpoint}`)
this.browser = await connect({
browserWSEndpoint,
defaultViewport: getViewport(),
})
this.page = await this.browser?.newPage()
this.isConnectedToRemote = true
return
} catch (error) {
console.log(`Failed to connect using cached endpoint: ${error}`)
// Clear the cached endpoint since it's no longer valid
this.cachedWebSocketEndpoint = undefined
// User wants to give up after one reconnection attempt
if (remoteBrowserHost) {
reconnectionAttempted = true
}
}
}
// Try to connect with host (either user-provided or auto-discovered)
if (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)
browserWSEndpoint = response.data.webSocketDebuggerUrl
if (!browserWSEndpoint) {
throw new Error("Could not find webSocketDebuggerUrl in the response")
}
console.log(`Found WebSocket browser endpoint: ${browserWSEndpoint}`)
// Cache the successful endpoint
this.cachedWebSocketEndpoint = browserWSEndpoint
this.lastConnectionAttempt = Date.now()
this.browser = await connect({
browserWSEndpoint,
defaultViewport: getViewport(),
})
this.page = await this.browser?.newPage()
this.isConnectedToRemote = true
return
} catch (error) {
console.log(`Failed to connect to remote browser: ${error}`)
}
}
// If we get here, all connection attempts failed
throw new Error(
"Failed to connect to remote browser. Make sure Chrome is running with remote debugging enabled (--remote-debugging-port=9222).",
)
}
async closeBrowser(): Promise<BrowserActionResult> {
if (this.browser || this.page) {
console.log("closing browser...")
await this.browser?.close().catch(() => {})
if (this.isConnectedToRemote && this.browser) {
// Close the page/tab first if it exists
if (this.page) {
await this.page.close().catch(() => {})
console.log("closed remote browser tab...")
}
await this.browser.disconnect().catch(() => {})
console.log("disconnected from remote browser...")
} else {
await this.browser?.close().catch(() => {})
console.log("closed local browser...")
}
this.browser = undefined
this.page = undefined
this.currentMousePosition = undefined
this.isConnectedToRemote = false
}
return {}
}
+4
View File
@@ -8,6 +8,8 @@ export interface BrowserSettings {
headless: boolean
// Chrome installation to use
// chromeType: "chromium" | "system"
remoteBrowserHost?: string
remoteBrowserEnabled?: boolean
}
export const DEFAULT_BROWSER_SETTINGS: BrowserSettings = {
@@ -16,6 +18,8 @@ export const DEFAULT_BROWSER_SETTINGS: BrowserSettings = {
height: 600,
},
headless: true,
remoteBrowserEnabled: false,
remoteBrowserHost: undefined,
// chromeType: "chromium",
}
+19
View File
@@ -40,6 +40,10 @@ export interface ExtensionMessage {
| "userCreditsPayments"
| "totalTasksSize"
| "addToInput"
| "browserConnectionResult"
| "browserConnectionInfo"
| "detectedChromePath"
| "scrollToSettings"
text?: string
action?:
| "chatButtonClicked"
@@ -80,6 +84,13 @@ export interface ExtensionMessage {
userCreditsUsage?: UsageTransaction[]
userCreditsPayments?: PaymentTransaction[]
totalTasksSize?: number | null
success?: boolean
values?: Record<string, any>
isBundled?: boolean
isConnected?: boolean
isRemote?: boolean
host?: string
isHeadless?: boolean
}
export type Invoke = "sendMessage" | "primaryButtonClick" | "secondaryButtonClick"
@@ -92,6 +103,7 @@ export interface ExtensionState {
apiConfiguration?: ApiConfiguration
autoApprovalSettings: AutoApprovalSettings
browserSettings: BrowserSettings
remoteBrowserHost?: string
chatSettings: ChatSettings
checkpointTrackerErrorMessage?: string
clineMessages: ClineMessage[]
@@ -202,6 +214,13 @@ export type BrowserActionResult = {
currentMousePosition?: string
}
export interface BrowserConnectionInfo {
isConnected: boolean
isRemote: boolean
host?: string
isHeadless: boolean
}
export interface ClineAskUseMcpServer {
serverName: string
type: "use_mcp_tool" | "access_mcp_resource"
+11 -1
View File
@@ -34,6 +34,12 @@ export interface WebviewMessage {
| "deleteMcpServer"
| "autoApprovalSettings"
| "browserSettings"
| "remoteBrowserHost"
| "remoteBrowserEnabled"
| "discoverBrowser"
| "testBrowserConnection"
| "browserConnectionResult"
| "browserRelaunchResult"
| "togglePlanActMode"
| "checkpointDiff"
| "checkpointRestore"
@@ -65,7 +71,11 @@ export interface WebviewMessage {
| "fetchUserCreditsData"
| "optionsResponse"
| "requestTotalTasksSize"
// | "relaunchChromeDebugMode"
| "relaunchChromeDebugMode"
| "getBrowserConnectionInfo"
| "getDetectedChromePath"
| "detectedChromePath"
| "scrollToSettings"
text?: string
disabled?: boolean
askResponse?: ClineAskResponse
@@ -1,235 +1,229 @@
import { VSCodeButton, VSCodeCheckbox, VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react"
import React, { useRef, useState } from "react"
import { useClickAway } from "react-use"
import styled from "styled-components"
import { BROWSER_VIEWPORT_PRESETS } from "../../../../src/shared/BrowserSettings"
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import React, { useEffect, useRef, useState } from "react"
import { useExtensionState } from "../../context/ExtensionStateContext"
import { vscode } from "../../utils/vscode"
import { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock"
import styled from "styled-components"
interface BrowserSettingsMenuProps {
disabled?: boolean
maxWidth?: number
}
export const BrowserSettingsMenu: React.FC<BrowserSettingsMenuProps> = ({ disabled = false, maxWidth }) => {
interface ConnectionInfo {
isConnected: boolean
isRemote: boolean
host?: string
isHeadless: boolean
}
export const BrowserSettingsMenu: React.FC<BrowserSettingsMenuProps> = ({ maxWidth }) => {
const { browserSettings } = useExtensionState()
const [showMenu, setShowMenu] = useState(false)
const [hasMouseEntered, setHasMouseEntered] = useState(false)
const containerRef = useRef<HTMLDivElement>(null)
const menuRef = useRef<HTMLDivElement>(null)
useClickAway(containerRef, () => {
if (showMenu) {
setShowMenu(false)
setHasMouseEntered(false)
}
const [showInfoPopover, setShowInfoPopover] = useState(false)
const [connectionInfo, setConnectionInfo] = useState<ConnectionInfo>({
isConnected: false,
isRemote: !!browserSettings.remoteBrowserEnabled,
host: browserSettings.remoteBrowserHost,
isHeadless: !!browserSettings.headless
})
const popoverRef = useRef<HTMLDivElement>(null)
const handleMouseEnter = () => {
setHasMouseEntered(true)
}
// Get actual connection info from the browser session
useEffect(() => {
// Request connection info when component mounts
vscode.postMessage({
type: "getBrowserConnectionInfo"
})
const handleMouseLeave = () => {
if (hasMouseEntered) {
setShowMenu(false)
setHasMouseEntered(false)
}
}
const handleControlsMouseLeave = (e: React.MouseEvent) => {
const menuElement = menuRef.current
if (menuElement && showMenu) {
const menuRect = menuElement.getBoundingClientRect()
// If mouse is moving towards the menu, don't close it
if (
e.clientY >= menuRect.top &&
e.clientY <= menuRect.bottom &&
e.clientX >= menuRect.left &&
e.clientX <= menuRect.right
) {
return
// Listen for connection info updates
const handleMessage = (event: MessageEvent) => {
const message = event.data
if (message.type === "browserConnectionInfo") {
setConnectionInfo({
isConnected: message.isConnected,
isRemote: message.isRemote,
host: message.host,
isHeadless: message.isHeadless
})
}
}
setShowMenu(false)
setHasMouseEntered(false)
window.addEventListener('message', handleMessage)
return () => {
window.removeEventListener('message', handleMessage)
}
}, [browserSettings.remoteBrowserHost, browserSettings.remoteBrowserEnabled, browserSettings.headless])
// Close popover when clicking outside
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (popoverRef.current && !popoverRef.current.contains(event.target as Node) &&
!event.composedPath().some(el => (el as HTMLElement).classList?.contains('browser-info-icon'))) {
setShowInfoPopover(false)
}
}
if (showInfoPopover) {
document.addEventListener('mousedown', handleClickOutside)
}
return () => {
document.removeEventListener('mousedown', handleClickOutside)
}
}, [showInfoPopover])
const openBrowserSettings = () => {
// First open the settings panel
vscode.postMessage({
type: "openSettings",
})
// After a short delay, send a message to scroll to browser settings
setTimeout(() => {
vscode.postMessage({
type: "scrollToSettings",
text: "browser-settings-section",
})
}, 300) // Give the settings panel time to open
}
const handleViewportChange = (event: Event) => {
const target = event.target as HTMLSelectElement
const selectedSize = BROWSER_VIEWPORT_PRESETS[target.value as keyof typeof BROWSER_VIEWPORT_PRESETS]
if (selectedSize) {
const toggleInfoPopover = () => {
setShowInfoPopover(!showInfoPopover)
// Request updated connection info when opening the popover
if (!showInfoPopover) {
vscode.postMessage({
type: "browserSettings",
browserSettings: {
...browserSettings,
viewport: selectedSize,
},
type: "getBrowserConnectionInfo"
})
}
}
const updateHeadless = (headless: boolean) => {
// Refresh connection info periodically when popover is open
useEffect(() => {
if (!showInfoPopover) return;
// Request connection info immediately
vscode.postMessage({
type: "browserSettings",
browserSettings: {
...browserSettings,
headless,
},
})
type: "getBrowserConnectionInfo"
});
// Set up interval to refresh every 2 seconds
const intervalId = setInterval(() => {
vscode.postMessage({
type: "getBrowserConnectionInfo"
});
}, 2000);
return () => clearInterval(intervalId);
}, [showInfoPopover]);
// Determine icon based on connection state
const getIconClass = () => {
if (connectionInfo.isRemote) {
return 'codicon-remote';
} else {
return connectionInfo.isConnected ? 'codicon-vm-running' : 'codicon-info';
}
}
// const updateChromeType = (chromeType: BrowserSettings["chromeType"]) => {
// vscode.postMessage({
// type: "browserSettings",
// browserSettings: {
// ...browserSettings,
// chromeType,
// },
// })
// }
// const relaunchChromeDebugMode = () => {
// vscode.postMessage({
// type: "relaunchChromeDebugMode",
// })
// }
// Determine icon color based on connection state
const getIconColor = () => {
if (connectionInfo.isRemote) {
return connectionInfo.isConnected ? 'var(--vscode-charts-blue)' : 'var(--vscode-foreground)';
} else if (connectionInfo.isConnected) {
return 'var(--vscode-charts-green)';
} else {
return 'var(--vscode-foreground)';
}
}
// Check connection status every second to keep icon in sync
useEffect(() => {
// Request connection info immediately
vscode.postMessage({
type: "getBrowserConnectionInfo"
});
// Set up interval to refresh every second
const intervalId = setInterval(() => {
vscode.postMessage({
type: "getBrowserConnectionInfo"
});
}, 1000);
return () => clearInterval(intervalId);
}, []);
return (
<div ref={containerRef} style={{ position: "relative", marginTop: "-1px" }} onMouseLeave={handleControlsMouseLeave}>
<VSCodeButton appearance="icon" onClick={() => setShowMenu(!showMenu)} disabled={disabled}>
<div ref={containerRef} style={{ position: "relative", marginTop: "-1px", display: "flex" }}>
<VSCodeButton
appearance="icon"
className="browser-info-icon"
onClick={toggleInfoPopover}
title="Browser connection info"
style={{ marginRight: "4px" }}>
<i
className={`codicon ${getIconClass()}`}
style={{
fontSize: "14.5px",
color: getIconColor()
}}
/>
</VSCodeButton>
{showInfoPopover && (
<InfoPopover ref={popoverRef}>
<h4 style={{ margin: "0 0 8px 0" }}>Browser Connection</h4>
<InfoRow>
<InfoLabel>Status:</InfoLabel>
<InfoValue style={{ color: connectionInfo.isConnected ? 'var(--vscode-charts-green)' : 'var(--vscode-errorForeground)' }}>
{connectionInfo.isConnected ? 'Connected' : 'Disconnected'}
</InfoValue>
</InfoRow>
<InfoRow>
<InfoLabel>Type:</InfoLabel>
<InfoValue>{connectionInfo.isRemote ? 'Remote' : 'Local'}</InfoValue>
</InfoRow>
{connectionInfo.isRemote && connectionInfo.host && (
<InfoRow>
<InfoLabel>Remote Host:</InfoLabel>
<InfoValue>{connectionInfo.host}</InfoValue>
</InfoRow>
)}
</InfoPopover>
)}
<VSCodeButton appearance="icon" onClick={openBrowserSettings}>
<i className="codicon codicon-settings-gear" style={{ fontSize: "14.5px" }} />
</VSCodeButton>
{showMenu && (
<SettingsMenu ref={menuRef} maxWidth={maxWidth} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
<SettingsGroup>
{/* <SettingsHeader>Headless Mode</SettingsHeader> */}
<VSCodeCheckbox
style={{ marginBottom: "8px", marginTop: -1 }}
checked={browserSettings.headless}
onChange={(e) => updateHeadless((e.target as HTMLInputElement).checked)}>
Run in headless mode
</VSCodeCheckbox>
<SettingsDescription>When enabled, Chrome will run in the background.</SettingsDescription>
</SettingsGroup>
{/* <SettingsGroup>
<SettingsHeader>Chrome Executable</SettingsHeader>
<VSCodeDropdown
style={{ width: "100%", marginBottom: "8px" }}
value={browserSettings.chromeType}
onChange={(e) =>
updateChromeType((e.target as HTMLSelectElement).value as BrowserSettings["chromeType"])
}>
<VSCodeOption value="chromium">Chromium (Auto-downloaded)</VSCodeOption>
<VSCodeOption value="system">System Chrome</VSCodeOption>
</VSCodeDropdown>
<SettingsDescription>
{browserSettings.chromeType === "system" ? (
<>
Cline will use your personal browser. You must{" "}
<VSCodeLink
href="#"
style={{ fontSize: "inherit" }}
onClick={(e: React.MouseEvent) => {
e.preventDefault()
relaunchChromeDebugMode()
}}>
relaunch Chrome in debug mode
</VSCodeLink>{" "}
to use this setting.
</>
) : (
"Cline will use a Chromium browser bundled with the extension."
)}
</SettingsDescription>
</SettingsGroup> */}
<SettingsGroup>
<SettingsHeader>Viewport Size</SettingsHeader>
<VSCodeDropdown
style={{ width: "100%" }}
value={
Object.entries(BROWSER_VIEWPORT_PRESETS).find(
([_, size]) =>
size.width === browserSettings.viewport.width &&
size.height === browserSettings.viewport.height,
)?.[0]
}
onChange={(event) => handleViewportChange(event as Event)}>
{Object.entries(BROWSER_VIEWPORT_PRESETS).map(([name]) => (
<VSCodeOption key={name} value={name}>
{name}
</VSCodeOption>
))}
</VSCodeDropdown>
</SettingsGroup>
</SettingsMenu>
)}
</div>
)
}
const SettingsMenu = styled.div<{ maxWidth?: number }>`
const InfoPopover = styled.div`
position: absolute;
top: calc(100% + 8px);
right: -2px;
background: ${CODE_BLOCK_BG_COLOR};
border: 1px solid var(--vscode-editorGroup-border);
padding: 8px;
border-radius: 3px;
z-index: 1000;
width: calc(100vw - 57px);
min-width: 0px;
max-width: ${(props) => (props.maxWidth ? `${props.maxWidth - 23}px` : "100vw")};
// Add invisible padding to create a safe hover zone
&::before {
content: "";
position: absolute;
top: -14px; // Same as margin-top in the parent's top property
left: 0;
right: -6px;
height: 14px;
}
&::after {
content: "";
position: absolute;
top: -6px;
right: 6px;
width: 10px;
height: 10px;
background: ${CODE_BLOCK_BG_COLOR};
border-left: 1px solid var(--vscode-editorGroup-border);
border-top: 1px solid var(--vscode-editorGroup-border);
transform: rotate(45deg);
z-index: 1; // Ensure arrow stays above the padding
}
top: 30px;
right: 0;
background-color: var(--vscode-editorWidget-background);
border: 1px solid var(--vscode-widget-border);
border-radius: 4px;
padding: 10px;
z-index: 100;
width: 250px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
`
const SettingsGroup = styled.div`
&:not(:last-child) {
margin-bottom: 8px;
// padding-bottom: 8px;
border-bottom: 1px solid var(--vscode-editorGroup-border);
}
const InfoRow = styled.div`
display: flex;
margin-bottom: 4px;
`
const SettingsHeader = styled.div`
font-size: 11px;
font-weight: 600;
margin-bottom: 6px;
color: var(--vscode-foreground);
const InfoLabel = styled.div`
flex: 0 0 90px;
font-weight: 500;
`
const SettingsDescription = styled.div<{ isLast?: boolean }>`
font-size: 11px;
color: var(--vscode-descriptionForeground);
margin-bottom: ${(props) => (props.isLast ? "0" : "8px")};
const InfoValue = styled.div`
flex: 1;
word-break: break-word;
`
export default BrowserSettingsMenu
@@ -307,7 +307,7 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
{displayState.url || "http"}
</div>
</div>
<BrowserSettingsMenu disabled={!shouldShowSettings} maxWidth={maxWidth} />
<BrowserSettingsMenu maxWidth={maxWidth} />
</div>
{/* Screenshot Area */}
@@ -0,0 +1,280 @@
import React, { useState, useEffect } from "react"
import { VSCodeButton, VSCodeCheckbox, VSCodeDropdown, VSCodeOption, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import { BROWSER_VIEWPORT_PRESETS } from "../../../../src/shared/BrowserSettings"
import { useExtensionState } from "../../context/ExtensionStateContext"
import { vscode } from "../../utils/vscode"
export const BrowserSettingsSection: React.FC = () => {
const { browserSettings } = useExtensionState()
const [testingConnection, setTestingConnection] = useState(false)
const [debugMode, setDebugMode] = useState(false)
const [testResult, setTestResult] = useState<{ success: boolean; message: string } | null>(null)
const [relaunchResult, setRelaunchResult] = useState<{ success: boolean; message: string } | null>(null)
const [isBundled, setIsBundled] = useState(false)
const [detectedChromePath, setDetectedChromePath] = useState<string | null>(null)
// Listen for browser connection test results and relaunch results
useEffect(() => {
const handleMessage = (event: MessageEvent) => {
const message = event.data
if (message.type === "browserConnectionResult") {
setTestResult({
success: message.success,
message: message.text,
})
setTestingConnection(false)
} else if (message.type === "browserRelaunchResult") {
setRelaunchResult({
success: message.success,
message: message.text,
})
setDebugMode(false)
} else if (message.type === "detectedChromePath") {
setDetectedChromePath(message.text)
setIsBundled(message.isBundled)
}
}
window.addEventListener("message", handleMessage)
return () => window.removeEventListener("message", handleMessage)
}, [])
// Request detected Chrome path on mount
useEffect(() => {
vscode.postMessage({
type: "getDetectedChromePath",
})
}, [])
const handleViewportChange = (event: Event) => {
const target = event.target as HTMLSelectElement
const selectedSize = BROWSER_VIEWPORT_PRESETS[target.value as keyof typeof BROWSER_VIEWPORT_PRESETS]
if (selectedSize) {
vscode.postMessage({
type: "browserSettings",
browserSettings: {
...browserSettings,
viewport: selectedSize,
},
})
}
}
const updateHeadless = (headless: boolean) => {
vscode.postMessage({
type: "browserSettings",
browserSettings: {
...browserSettings,
headless,
},
})
}
const updateRemoteBrowserEnabled = (enabled: boolean) => {
vscode.postMessage({
type: "remoteBrowserEnabled",
bool: enabled,
})
// If disabling, clear the host
if (!enabled) {
vscode.postMessage({
type: "remoteBrowserHost",
text: undefined,
})
}
}
const updateRemoteBrowserHost = (host: string | undefined) => {
vscode.postMessage({
type: "remoteBrowserHost",
text: host,
})
}
const testConnection = () => {
setTestingConnection(true)
setTestResult(null)
setRelaunchResult(null)
vscode.postMessage({
type: "testBrowserConnection",
text: browserSettings.remoteBrowserHost,
})
}
const discoverBrowser = () => {
setTestingConnection(true)
setTestResult(null)
setRelaunchResult(null)
vscode.postMessage({
type: "discoverBrowser",
})
}
const relaunchChromeDebugMode = () => {
setDebugMode(true)
setRelaunchResult(null)
setTestResult(null)
vscode.postMessage({
type: "relaunchChromeDebugMode",
})
}
return (
<div
id="browser-settings-section"
style={{ marginBottom: 20, borderTop: "1px solid var(--vscode-panel-border)", paddingTop: 15 }}>
<h3 style={{ color: "var(--vscode-foreground)", margin: "0 0 10px 0", fontSize: "14px" }}>Browser Settings</h3>
<div style={{ marginBottom: 15 }}>
<div style={{ marginBottom: 8 }}>
<label style={{ fontWeight: "500", display: "block", marginBottom: 5 }}>Viewport size</label>
<VSCodeDropdown
style={{ width: "100%" }}
value={
Object.entries(BROWSER_VIEWPORT_PRESETS).find(([_, size]) => {
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]) => (
<VSCodeOption key={name} value={name}>
{name}
</VSCodeOption>
))}
</VSCodeDropdown>
</div>
<p
style={{
fontSize: "12px",
color: "var(--vscode-descriptionForeground)",
margin: 0,
}}>
Set the size of the browser viewport for screenshots and interactions.
</p>
</div>
<div style={{ marginBottom: 15 }}>
<div style={{ marginBottom: 8 }}>
<label style={{ fontWeight: "500", display: "block", marginBottom: 5 }}>Chrome executable path</label>
<VSCodeTextField
style={{ width: "100%" }}
placeholder={
isBundled
? "(Using bundled Chromium)"
: detectedChromePath || "Checking for path to Chrome executable..."
}
onChange={(e: any) => {
const value = e.target.value
// Update VSCode configuration directly
vscode.postMessage({
type: "openExtensionSettings",
text: "chromeExecutablePath",
})
}}
/>
</div>
<p
style={{
fontSize: "12px",
color: "var(--vscode-descriptionForeground)",
margin: 0,
}}>
Detected path shown by default. If not found, Cline will download and use a bundled Chromium instead.
</p>
</div>
<div style={{ marginBottom: 15 }}>
<VSCodeCheckbox
style={{ marginBottom: "8px" }}
checked={browserSettings.headless}
onChange={(e) => updateHeadless((e.target as HTMLInputElement).checked)}>
Run in headless mode
</VSCodeCheckbox>
<p
style={{
fontSize: "12px",
color: "var(--vscode-descriptionForeground)",
margin: "0 0 8px 0px",
}}>
When enabled, Chrome will run in the background without a visible window. If disabled, a live Chrome instance
will pop up with a new tab. A remote Chrome must be restarted in headless mode
</p>
</div>
<div style={{ marginBottom: 15 }}>
<div style={{ marginBottom: 8 }}>
<VSCodeCheckbox
checked={browserSettings.remoteBrowserEnabled}
onChange={(e) => updateRemoteBrowserEnabled((e.target as HTMLInputElement).checked)}>
Use remote browser connection
</VSCodeCheckbox>
</div>
<p
style={{
fontSize: "12px",
color: "var(--vscode-descriptionForeground)",
margin: "0 0 8px 0px",
}}>
Enable Cline to use your real Chrome. Start Chrome in debug mode manually (--remote-debugging-port=9222) or
use the button below. Enter the host address or leave it blank for automatic discovery.
</p>
{browserSettings.remoteBrowserEnabled && (
<div style={{ marginLeft: 0 }}>
<VSCodeTextField
value={browserSettings.remoteBrowserHost || ""}
placeholder="http://localhost:9222"
style={{ width: "100%", marginBottom: 8 }}
onChange={(e: any) => updateRemoteBrowserHost(e.target.value || undefined)}
/>
<div style={{ display: "flex", gap: "10px", marginBottom: 8, justifyContent: "center" }}>
<VSCodeButton
style={{ flex: 1 }}
disabled={testingConnection}
onClick={browserSettings.remoteBrowserHost ? testConnection : discoverBrowser}>
{testingConnection ? "Testing..." : "Test Connection"}
</VSCodeButton>
<VSCodeButton style={{ flex: 1 }} disabled={debugMode} onClick={relaunchChromeDebugMode}>
{debugMode ? "Relaunching Browser..." : "Relaunch Browser with Debug Mode"}
</VSCodeButton>
</div>
{(testResult || relaunchResult) && (
<div
style={{
padding: "8px",
marginBottom: "8px",
backgroundColor:
(relaunchResult?.success ?? testResult?.success)
? "rgba(0, 128, 0, 0.1)"
: "rgba(255, 0, 0, 0.1)",
color:
testResult?.success || relaunchResult?.success
? "var(--vscode-terminal-ansiGreen)"
: "var(--vscode-terminal-ansiRed)",
borderRadius: "3px",
fontSize: "11px",
}}>
{testResult?.message || relaunchResult?.message}
</div>
)}
<p
style={{
fontSize: "12px",
color: "var(--vscode-descriptionForeground)",
margin: 0,
}}></p>
</div>
)}
</div>
</div>
)
}
export default BrowserSettingsSection
@@ -11,11 +11,11 @@ import { memo, useCallback, useEffect, useState } from "react"
import { useExtensionState } from "../../context/ExtensionStateContext"
import { validateApiConfiguration, validateModelId } from "../../utils/validate"
import { vscode } from "../../utils/vscode"
import SettingsButton from "../common/SettingsButton"
import ApiOptions from "./ApiOptions"
import { TabButton } from "../mcp/McpView"
import { useEvent } from "react-use"
import { ExtensionMessage } from "../../../../src/shared/ExtensionMessage"
import BrowserSettingsSection from "./BrowserSettingsSection"
const { IS_DEV } = process.env
type SettingsViewProps = {
@@ -32,6 +32,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
telemetrySetting,
setTelemetrySetting,
chatSettings,
remoteBrowserHost,
planActSeparateModelsSetting,
setPlanActSeparateModelsSetting,
} = useExtensionState()
@@ -113,6 +114,24 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
setPendingTabChange(null)
}
break
case "scrollToSettings":
setTimeout(() => {
const elementId = message.text
if (elementId) {
const element = document.getElementById(elementId)
if (element) {
element.scrollIntoView({ behavior: "smooth" })
element.style.transition = "background-color 0.5s ease"
element.style.backgroundColor = "var(--vscode-textPreformat-background)"
setTimeout(() => {
element.style.backgroundColor = "transparent"
}, 1200)
}
}
}, 300)
break
}
},
[pendingTabChange],
@@ -279,6 +298,9 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
</p>
</div>
{/* Browser Settings Section */}
<BrowserSettingsSection />
{IS_DEV && (
<>
<div style={{ marginTop: "10px", marginBottom: "4px" }}>Debug</div>
@@ -296,22 +318,6 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
</>
)}
<div
style={{
marginTop: "auto",
paddingRight: 8,
display: "flex",
justifyContent: "center",
}}>
<SettingsButton
onClick={() => vscode.postMessage({ type: "openExtensionSettings" })}
style={{
margin: "0 0 16px 0",
}}>
<i className="codicon codicon-settings-gear" />
Advanced Settings
</SettingsButton>
</div>
<div
style={{
textAlign: "center",
@@ -319,6 +325,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
fontSize: "12px",
lineHeight: "1.2",
padding: "0 8px 15px 0",
marginTop: "auto",
}}>
<p
style={{
@@ -26,6 +26,7 @@ interface ExtensionStateContextType extends ExtensionState {
setTelemetrySetting: (value: TelemetrySetting) => void
setShowAnnouncement: (value: boolean) => void
setPlanActSeparateModelsSetting: (value: boolean) => void
setRemoteBrowserEnabled: (value: boolean) => void
}
const ExtensionStateContext = createContext<ExtensionStateContextType | undefined>(undefined)
@@ -188,6 +189,14 @@ export const ExtensionStateContextProvider: React.FC<{
...prevState,
shouldShowAnnouncement: value,
})),
setRemoteBrowserEnabled: (value) =>
setState((prevState) => ({
...prevState,
browserSettings: {
...prevState.browserSettings,
remoteBrowserEnabled: value,
},
})),
}
return <ExtensionStateContext.Provider value={contextValue}>{children}</ExtensionStateContext.Provider>