mirror of
https://github.com/cline/cline.git
synced 2026-09-06 20:41:02 +08:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9fb63f7d05 | |||
| f951e9b516 | |||
| 097171d016 | |||
| 0160b18a56 | |||
| 173020f2cd | |||
| 2f37207e2b | |||
| 8dbd25cf6e | |||
| b6c590d0c4 | |||
| e51b39e7f2 | |||
| 78bb82ace8 |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Feature to open basic settings & scroll a section into view with a highlight animation
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Remote browser control via devtools protocol
|
||||
@@ -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": [
|
||||
|
||||
@@ -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,107 @@ 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 "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)
|
||||
@@ -901,6 +1006,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)
|
||||
@@ -2083,6 +2195,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 +2264,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 +2330,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 +2372,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 +2436,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,
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
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 { discoverChromeInstances, testBrowserConnection } from "./browserDiscovery"
|
||||
// import * as chromeLauncher from "chrome-launcher"
|
||||
|
||||
interface PCRStats {
|
||||
@@ -16,13 +18,15 @@ interface PCRStats {
|
||||
executablePath: string
|
||||
}
|
||||
|
||||
// const DEBUG_PORT = 9222 // Chrome's default debugging port
|
||||
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
|
||||
|
||||
constructor(context: vscode.ExtensionContext, browserSettings: BrowserSettings) {
|
||||
@@ -30,6 +34,11 @@ export class BrowserSession {
|
||||
this.browserSettings = browserSettings
|
||||
}
|
||||
|
||||
// Tests remote browser connection
|
||||
async testConnection(host: string): Promise<{ success: boolean; message: string; endpoint?: string }> {
|
||||
return testBrowserConnection(host)
|
||||
}
|
||||
|
||||
private async ensureChromiumExists(): Promise<PCRStats> {
|
||||
const globalStoragePath = this.context?.globalStorageUri?.fsPath
|
||||
if (!globalStoragePath) {
|
||||
@@ -55,16 +64,6 @@ export class BrowserSession {
|
||||
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() {
|
||||
// const result = await vscode.window.showWarningMessage(
|
||||
// "This will close your existing Chrome tabs and relaunch Chrome in debug mode. Are you sure?",
|
||||
@@ -103,29 +102,30 @@ export class BrowserSession {
|
||||
// return installation
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * Helper to detect user’s 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")
|
||||
// }
|
||||
// }
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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 stats = await this.ensureChromiumExists()
|
||||
this.browser = await stats.puppeteer.launch({
|
||||
args: [
|
||||
@@ -135,34 +135,102 @@ export class BrowserSession {
|
||||
defaultViewport: this.browserSettings.viewport,
|
||||
headless: this.browserSettings.headless,
|
||||
})
|
||||
}
|
||||
|
||||
// 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 = () => {
|
||||
const size = (this.context.globalState.get("browserViewportSize") as string | undefined) || "900x600"
|
||||
const [width, height] = size.split("x").map(Number)
|
||||
return { width, height }
|
||||
}
|
||||
|
||||
// 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()
|
||||
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()
|
||||
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.browserSettings.remoteBrowserEnabled && this.browser) {
|
||||
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
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
import * as vscode from "vscode"
|
||||
import * as os from "os"
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a shell command and return stdout and stderr
|
||||
*/
|
||||
export async function executeShellCommand(command: string): Promise<{ stdout: string; stderr: string }> {
|
||||
return new Promise<{ stdout: string; stderr: string }>((resolve) => {
|
||||
const cp = require("child_process")
|
||||
cp.exec(command, (err: any, stdout: string, stderr: string) => {
|
||||
resolve({ stdout, stderr })
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Docker gateway IP
|
||||
*/
|
||||
export async function getDockerGatewayIP(): Promise<string | null> {
|
||||
try {
|
||||
if (process.platform === "linux") {
|
||||
try {
|
||||
// this looks sketchy: command cross-platform availability -Andrei
|
||||
const { stdout } = await executeShellCommand("ip route | grep default | awk '{print $3}'")
|
||||
return stdout.trim()
|
||||
} catch (error) {
|
||||
console.log("Could not determine Docker gateway IP:", error)
|
||||
}
|
||||
}
|
||||
return null
|
||||
} catch (error) {
|
||||
console.log("Could not determine Docker gateway IP:", error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Docker host IP
|
||||
*/
|
||||
export async function getDockerHostIP(): Promise<string | null> {
|
||||
try {
|
||||
// Try to resolve host.docker.internal (works on Docker Desktop)
|
||||
return new Promise((resolve) => {
|
||||
const dns = require("dns")
|
||||
dns.lookup("host.docker.internal", (err: any, address: string) => {
|
||||
if (err) {
|
||||
resolve(null)
|
||||
} else {
|
||||
resolve(address)
|
||||
}
|
||||
})
|
||||
})
|
||||
} catch (error) {
|
||||
console.log("Could not determine Docker host IP:", error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan a network range for Chrome debugging port
|
||||
*/
|
||||
export async function scanNetworkForChrome(baseIP: string): Promise<string | null> {
|
||||
if (!baseIP || !baseIP.match(/^\d+\.\d+\.\d+\./)) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Extract the network prefix (e.g., "192.168.65.")
|
||||
const networkPrefix = baseIP.split(".").slice(0, 3).join(".") + "."
|
||||
|
||||
// Common Docker host IPs to try first
|
||||
const priorityIPs = [
|
||||
networkPrefix + "1", // Common gateway
|
||||
networkPrefix + "2", // Common host
|
||||
networkPrefix + "254", // Common host in some Docker setups
|
||||
]
|
||||
|
||||
console.log(`Scanning priority IPs in network ${networkPrefix}*`)
|
||||
|
||||
// Check priority IPs first
|
||||
for (const ip of priorityIPs) {
|
||||
const isOpen = await isPortOpen(ip, 9222)
|
||||
if (isOpen) {
|
||||
console.log(`Found Chrome debugging port open on ${ip}`)
|
||||
return ip
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Discover Chrome instances on the network
|
||||
*/
|
||||
export async function discoverChromeInstances(): Promise<string | null> {
|
||||
// Get all network interfaces
|
||||
const networkInterfaces = os.networkInterfaces()
|
||||
const ipAddresses = []
|
||||
|
||||
// Always try localhost first
|
||||
ipAddresses.push("localhost")
|
||||
ipAddresses.push("127.0.0.1")
|
||||
|
||||
// Try to get Docker gateway IP
|
||||
const gatewayIP = await getDockerGatewayIP()
|
||||
if (gatewayIP) {
|
||||
console.log("Found Docker gateway IP:", gatewayIP)
|
||||
ipAddresses.push(gatewayIP)
|
||||
}
|
||||
|
||||
// Try to get Docker host IP
|
||||
const hostIP = await getDockerHostIP()
|
||||
if (hostIP) {
|
||||
console.log("Found Docker host IP:", hostIP)
|
||||
ipAddresses.push(hostIP)
|
||||
}
|
||||
|
||||
// Add all local IP addresses from network interfaces
|
||||
const localIPs: string[] = []
|
||||
Object.values(networkInterfaces).forEach((interfaces) => {
|
||||
if (!interfaces) return
|
||||
interfaces.forEach((iface) => {
|
||||
// Only consider IPv4 addresses
|
||||
if (iface.family === "IPv4" || iface.family === (4 as any)) {
|
||||
localIPs.push(iface.address)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// Add local IPs to the list
|
||||
ipAddresses.push(...localIPs)
|
||||
|
||||
// Scan network for Chrome debugging port
|
||||
for (const ip of localIPs) {
|
||||
const chromeIP = await scanNetworkForChrome(ip)
|
||||
if (chromeIP && !ipAddresses.includes(chromeIP)) {
|
||||
console.log("Found potential Chrome host via network scan:", chromeIP)
|
||||
ipAddresses.push(chromeIP)
|
||||
}
|
||||
}
|
||||
|
||||
// Remove duplicates
|
||||
const uniqueIPs = [...new Set(ipAddresses)]
|
||||
console.log("IP Addresses to try:", uniqueIPs)
|
||||
|
||||
// Try connecting to each IP address
|
||||
for (const ip of uniqueIPs) {
|
||||
const connection = await tryConnect(ip)
|
||||
if (connection) {
|
||||
console.log(`Successfully connected to Chrome at: ${connection.ip}`)
|
||||
// Store the successful IP for future use
|
||||
console.log(`✅ Found Chrome at ${connection.ip} - You can hardcode this IP if needed`)
|
||||
|
||||
// Return the host URL and endpoint
|
||||
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)}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +40,8 @@ export interface ExtensionMessage {
|
||||
| "userCreditsPayments"
|
||||
| "totalTasksSize"
|
||||
| "addToInput"
|
||||
| "browserConnectionResult"
|
||||
| "scrollToSettings"
|
||||
text?: string
|
||||
action?:
|
||||
| "chatButtonClicked"
|
||||
@@ -80,6 +82,8 @@ export interface ExtensionMessage {
|
||||
userCreditsUsage?: UsageTransaction[]
|
||||
userCreditsPayments?: PaymentTransaction[]
|
||||
totalTasksSize?: number | null
|
||||
success?: boolean
|
||||
values?: Record<string, any>
|
||||
}
|
||||
|
||||
export type Invoke = "sendMessage" | "primaryButtonClick" | "secondaryButtonClick"
|
||||
@@ -92,6 +96,7 @@ export interface ExtensionState {
|
||||
apiConfiguration?: ApiConfiguration
|
||||
autoApprovalSettings: AutoApprovalSettings
|
||||
browserSettings: BrowserSettings
|
||||
remoteBrowserHost?: string
|
||||
chatSettings: ChatSettings
|
||||
checkpointTrackerErrorMessage?: string
|
||||
clineMessages: ClineMessage[]
|
||||
|
||||
@@ -34,6 +34,11 @@ export interface WebviewMessage {
|
||||
| "deleteMcpServer"
|
||||
| "autoApprovalSettings"
|
||||
| "browserSettings"
|
||||
| "remoteBrowserHost"
|
||||
| "remoteBrowserEnabled"
|
||||
| "discoverBrowser"
|
||||
| "testBrowserConnection"
|
||||
| "browserConnectionResult"
|
||||
| "togglePlanActMode"
|
||||
| "checkpointDiff"
|
||||
| "checkpointRestore"
|
||||
@@ -65,6 +70,7 @@ export interface WebviewMessage {
|
||||
| "fetchUserCreditsData"
|
||||
| "optionsResponse"
|
||||
| "requestTotalTasksSize"
|
||||
| "scrollToSettings"
|
||||
// | "relaunchChromeDebugMode"
|
||||
text?: string
|
||||
disabled?: boolean
|
||||
|
||||
@@ -1,235 +1,38 @@
|
||||
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, { useRef } from "react"
|
||||
import { useExtensionState } from "../../context/ExtensionStateContext"
|
||||
import { vscode } from "../../utils/vscode"
|
||||
import { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock"
|
||||
|
||||
interface BrowserSettingsMenuProps {
|
||||
disabled?: boolean
|
||||
maxWidth?: number
|
||||
}
|
||||
|
||||
export const BrowserSettingsMenu: React.FC<BrowserSettingsMenuProps> = ({ disabled = false, maxWidth }) => {
|
||||
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 handleMouseEnter = () => {
|
||||
setHasMouseEntered(true)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
setShowMenu(false)
|
||||
setHasMouseEntered(false)
|
||||
}
|
||||
|
||||
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) => {
|
||||
const openBrowserSettings = () => {
|
||||
// First open the settings panel
|
||||
vscode.postMessage({
|
||||
type: "browserSettings",
|
||||
browserSettings: {
|
||||
...browserSettings,
|
||||
headless,
|
||||
},
|
||||
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 updateChromeType = (chromeType: BrowserSettings["chromeType"]) => {
|
||||
// vscode.postMessage({
|
||||
// type: "browserSettings",
|
||||
// browserSettings: {
|
||||
// ...browserSettings,
|
||||
// chromeType,
|
||||
// },
|
||||
// })
|
||||
// }
|
||||
|
||||
// const relaunchChromeDebugMode = () => {
|
||||
// vscode.postMessage({
|
||||
// type: "relaunchChromeDebugMode",
|
||||
// })
|
||||
// }
|
||||
|
||||
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" }}>
|
||||
<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 }>`
|
||||
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
|
||||
}
|
||||
`
|
||||
|
||||
const SettingsGroup = styled.div`
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 8px;
|
||||
// padding-bottom: 8px;
|
||||
border-bottom: 1px solid var(--vscode-editorGroup-border);
|
||||
}
|
||||
`
|
||||
|
||||
const SettingsHeader = styled.div`
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 6px;
|
||||
color: var(--vscode-foreground);
|
||||
`
|
||||
|
||||
const SettingsDescription = styled.div<{ isLast?: boolean }>`
|
||||
font-size: 11px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
margin-bottom: ${(props) => (props.isLast ? "0" : "8px")};
|
||||
`
|
||||
|
||||
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,239 @@
|
||||
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 [testResult, setTestResult] = useState<{ success: boolean; message: string } | null>(null)
|
||||
|
||||
// Listen for browser connection test results
|
||||
useEffect(() => {
|
||||
const handleMessage = (event: MessageEvent) => {
|
||||
const message = event.data
|
||||
if (message.type === "browserConnectionResult") {
|
||||
setTestResult({
|
||||
success: message.success,
|
||||
message: message.text,
|
||||
})
|
||||
setTestingConnection(false)
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("message", handleMessage)
|
||||
return () => window.removeEventListener("message", handleMessage)
|
||||
}, [])
|
||||
|
||||
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)
|
||||
vscode.postMessage({
|
||||
type: "testBrowserConnection",
|
||||
text: browserSettings.remoteBrowserHost,
|
||||
})
|
||||
}
|
||||
|
||||
const discoverBrowser = () => {
|
||||
setTestingConnection(true)
|
||||
setTestResult(null)
|
||||
vscode.postMessage({
|
||||
type: "discoverBrowser",
|
||||
})
|
||||
}
|
||||
|
||||
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 }}>
|
||||
<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 0 20px",
|
||||
}}>
|
||||
When enabled, Chrome will run in the background without a visible window.
|
||||
</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="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,
|
||||
}}>
|
||||
Path to Chrome executable for browser use functionality. If not set, the extension will attempt to find it
|
||||
automatically.
|
||||
</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 20px",
|
||||
}}>
|
||||
Connect to a Chrome browser running with remote debugging enabled (--remote-debugging-port=9222). This allows
|
||||
Cline to use your existing browser session with all authentication cookies.
|
||||
</p>
|
||||
|
||||
{browserSettings.remoteBrowserEnabled && (
|
||||
<div style={{ marginLeft: 20 }}>
|
||||
<div style={{ display: "flex", gap: "5px", marginBottom: 8 }}>
|
||||
<VSCodeTextField
|
||||
value={browserSettings.remoteBrowserHost || ""}
|
||||
placeholder="http://localhost:9222"
|
||||
style={{ flexGrow: 1 }}
|
||||
onChange={(e: any) => updateRemoteBrowserHost(e.target.value || undefined)}
|
||||
/>
|
||||
<VSCodeButton
|
||||
disabled={testingConnection}
|
||||
onClick={browserSettings.remoteBrowserHost ? testConnection : discoverBrowser}>
|
||||
{testingConnection ? "Testing..." : "Test Connection"}
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
|
||||
{testResult && (
|
||||
<div
|
||||
style={{
|
||||
padding: "8px",
|
||||
marginBottom: "8px",
|
||||
backgroundColor: testResult.success ? "rgba(0, 128, 0, 0.1)" : "rgba(255, 0, 0, 0.1)",
|
||||
color: testResult.success
|
||||
? "var(--vscode-terminal-ansiGreen)"
|
||||
: "var(--vscode-terminal-ansiRed)",
|
||||
borderRadius: "3px",
|
||||
fontSize: "11px",
|
||||
}}>
|
||||
{testResult.message}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
margin: 0,
|
||||
}}>
|
||||
Enter the DevTools Protocol host address or leave empty to auto-discover Chrome instances.
|
||||
</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>
|
||||
|
||||
Reference in New Issue
Block a user