mirror of
https://github.com/cline/cline.git
synced 2026-09-12 17:19:32 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e8f78b1731 | ||
|
|
2d9245b4a6 | ||
|
|
616d56468f |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Filter Cline Provider models
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Feature to open basic settings & scroll a section into view with a highlight animation
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Can test on WebIDE
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Remote browser control via devtools protocol
|
||||
@@ -31,6 +31,36 @@ If you're planning to work on a bigger feature, please create a [feature request
|
||||
- Run `npm run test` to run tests locally
|
||||
- Before submitting PR, run `npm run format:fix` to format your code
|
||||
|
||||
3. **Linux-specific Setup**
|
||||
VS Code extension tests on Linux require the following system libraries:
|
||||
|
||||
- `libatk1.0-0`
|
||||
- `libatk-bridge2.0-0`
|
||||
- `libxkbfile1`
|
||||
- `libx11-xcb1`
|
||||
- `libxcomposite1`
|
||||
- `libxdamage1`
|
||||
- `libxfixes3`
|
||||
- `libxrandr2`
|
||||
- `libgbm1`
|
||||
- `libdrm2`
|
||||
- `libgtk-3-0`
|
||||
- `dbus`
|
||||
- `xvfb`
|
||||
|
||||
These libraries provide necessary GUI components and system services for the test environment.
|
||||
|
||||
For example, on Debian-based distributions (e.g., Ubuntu), you can install these libraries using apt:
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install -y \
|
||||
libatk1.0-0 libatk-bridge2.0-0 libxkbfile1 libx11-xcb1 \
|
||||
libxcomposite1 libxdamage1 libxfixes3 libxrandr2 libgbm1 \
|
||||
libdrm2 libgtk-3-0 dbus xvfb
|
||||
```
|
||||
|
||||
- Run `npm run test:ci` to run tests locally
|
||||
|
||||
## Writing and Submitting Code
|
||||
|
||||
Anyone can contribute code to Cline, but we ask that you follow these guidelines to ensure your contributions can be smoothly integrated:
|
||||
|
||||
+1
-10
@@ -232,16 +232,6 @@
|
||||
"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": [
|
||||
@@ -290,6 +280,7 @@
|
||||
"format": "prettier . --check",
|
||||
"format:fix": "prettier . --write",
|
||||
"test": "vscode-test",
|
||||
"test:ci": "node scripts/test-ci.js",
|
||||
"install:all": "npm install && cd webview-ui && npm install",
|
||||
"dev:webview": "cd webview-ui && npm run dev",
|
||||
"build:webview": "cd webview-ui && npm run build",
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env node
|
||||
const { execSync } = require("child_process")
|
||||
const process = require("process")
|
||||
|
||||
try {
|
||||
if (process.platform === "linux") {
|
||||
console.log("Detected Linux environment.")
|
||||
|
||||
execSync("which xvfb-run", { stdio: "ignore" })
|
||||
|
||||
console.log("xvfb-run is installed. Running tests with xvfb-run...")
|
||||
execSync("xvfb-run -a npm run test", { stdio: "inherit" })
|
||||
} else {
|
||||
console.log("Non-Linux environment detected. Running tests normally.")
|
||||
execSync("npm run test", { stdio: "inherit" })
|
||||
}
|
||||
} catch (error) {
|
||||
if (process.platform === "linux") {
|
||||
console.error(
|
||||
`Error: xvfb-run is not installed.\n` +
|
||||
`Please install it using the following command:\n` +
|
||||
` Debian/Ubuntu: sudo apt install xvfb\n` +
|
||||
` RHEL/CentOS: sudo yum install xvfb\n` +
|
||||
` Arch Linux: sudo pacman -S xvfb`,
|
||||
)
|
||||
} else {
|
||||
console.error("Error running tests:", error.message)
|
||||
}
|
||||
process.exit(1)
|
||||
}
|
||||
@@ -41,8 +41,6 @@ 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
|
||||
@@ -117,8 +115,6 @@ 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.
|
||||
@@ -578,107 +574,6 @@ 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)
|
||||
@@ -1006,13 +901,6 @@ 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)
|
||||
@@ -2195,11 +2083,6 @@ 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,
|
||||
@@ -2264,8 +2147,6 @@ 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>,
|
||||
@@ -2330,8 +2211,6 @@ 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
|
||||
@@ -2372,13 +2251,6 @@ 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,
|
||||
@@ -2436,7 +2308,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: mergedBrowserSettings,
|
||||
browserSettings: browserSettings || DEFAULT_BROWSER_SETTINGS,
|
||||
chatSettings: chatSettings || DEFAULT_CHAT_SETTINGS,
|
||||
userInfo,
|
||||
previousModeApiProvider,
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import * as vscode from "vscode"
|
||||
import * as fs from "fs/promises"
|
||||
import * as path from "path"
|
||||
import { Browser, Page, ScreenshotOptions, TimeoutError, launch, connect } from "puppeteer-core"
|
||||
import { Browser, Page, ScreenshotOptions, TimeoutError, launch } 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 {
|
||||
@@ -18,15 +16,13 @@ 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) {
|
||||
@@ -34,11 +30,6 @@ 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) {
|
||||
@@ -64,6 +55,16 @@ 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?",
|
||||
@@ -102,30 +103,29 @@ 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,102 +135,34 @@ export class BrowserSession {
|
||||
defaultViewport: this.browserSettings.viewport,
|
||||
headless: this.browserSettings.headless,
|
||||
})
|
||||
}
|
||||
|
||||
async launchRemoteBrowser() {
|
||||
let remoteBrowserHost = this.browserSettings.remoteBrowserHost
|
||||
let browserWSEndpoint: string | undefined = this.cachedWebSocketEndpoint
|
||||
let reconnectionAttempted = 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,
|
||||
// })
|
||||
// }
|
||||
|
||||
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).",
|
||||
)
|
||||
// (latest version of puppeteer does not add headless to user agent)
|
||||
this.page = await this.browser?.newPage()
|
||||
}
|
||||
|
||||
async closeBrowser(): Promise<BrowserActionResult> {
|
||||
if (this.browser || this.page) {
|
||||
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...")
|
||||
}
|
||||
|
||||
console.log("closing browser...")
|
||||
await this.browser?.close().catch(() => {})
|
||||
this.browser = undefined
|
||||
this.page = undefined
|
||||
this.currentMousePosition = undefined
|
||||
|
||||
@@ -1,245 +0,0 @@
|
||||
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,8 +8,6 @@ export interface BrowserSettings {
|
||||
headless: boolean
|
||||
// Chrome installation to use
|
||||
// chromeType: "chromium" | "system"
|
||||
remoteBrowserHost?: string
|
||||
remoteBrowserEnabled?: boolean
|
||||
}
|
||||
|
||||
export const DEFAULT_BROWSER_SETTINGS: BrowserSettings = {
|
||||
@@ -18,8 +16,6 @@ export const DEFAULT_BROWSER_SETTINGS: BrowserSettings = {
|
||||
height: 600,
|
||||
},
|
||||
headless: true,
|
||||
remoteBrowserEnabled: false,
|
||||
remoteBrowserHost: undefined,
|
||||
// chromeType: "chromium",
|
||||
}
|
||||
|
||||
|
||||
@@ -40,8 +40,6 @@ export interface ExtensionMessage {
|
||||
| "userCreditsPayments"
|
||||
| "totalTasksSize"
|
||||
| "addToInput"
|
||||
| "browserConnectionResult"
|
||||
| "scrollToSettings"
|
||||
text?: string
|
||||
action?:
|
||||
| "chatButtonClicked"
|
||||
@@ -82,8 +80,6 @@ export interface ExtensionMessage {
|
||||
userCreditsUsage?: UsageTransaction[]
|
||||
userCreditsPayments?: PaymentTransaction[]
|
||||
totalTasksSize?: number | null
|
||||
success?: boolean
|
||||
values?: Record<string, any>
|
||||
}
|
||||
|
||||
export type Invoke = "sendMessage" | "primaryButtonClick" | "secondaryButtonClick"
|
||||
@@ -96,7 +92,6 @@ export interface ExtensionState {
|
||||
apiConfiguration?: ApiConfiguration
|
||||
autoApprovalSettings: AutoApprovalSettings
|
||||
browserSettings: BrowserSettings
|
||||
remoteBrowserHost?: string
|
||||
chatSettings: ChatSettings
|
||||
checkpointTrackerErrorMessage?: string
|
||||
clineMessages: ClineMessage[]
|
||||
|
||||
@@ -34,11 +34,6 @@ export interface WebviewMessage {
|
||||
| "deleteMcpServer"
|
||||
| "autoApprovalSettings"
|
||||
| "browserSettings"
|
||||
| "remoteBrowserHost"
|
||||
| "remoteBrowserEnabled"
|
||||
| "discoverBrowser"
|
||||
| "testBrowserConnection"
|
||||
| "browserConnectionResult"
|
||||
| "togglePlanActMode"
|
||||
| "checkpointDiff"
|
||||
| "checkpointRestore"
|
||||
@@ -70,7 +65,6 @@ export interface WebviewMessage {
|
||||
| "fetchUserCreditsData"
|
||||
| "optionsResponse"
|
||||
| "requestTotalTasksSize"
|
||||
| "scrollToSettings"
|
||||
// | "relaunchChromeDebugMode"
|
||||
text?: string
|
||||
disabled?: boolean
|
||||
|
||||
@@ -1,38 +1,235 @@
|
||||
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
|
||||
import React, { useRef } from "react"
|
||||
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 { 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> = ({ maxWidth }) => {
|
||||
export const BrowserSettingsMenu: React.FC<BrowserSettingsMenuProps> = ({ disabled = false, maxWidth }) => {
|
||||
const { browserSettings } = useExtensionState()
|
||||
const [showMenu, setShowMenu] = useState(false)
|
||||
const [hasMouseEntered, setHasMouseEntered] = useState(false)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const menuRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const openBrowserSettings = () => {
|
||||
// First open the settings panel
|
||||
vscode.postMessage({
|
||||
type: "openSettings",
|
||||
})
|
||||
useClickAway(containerRef, () => {
|
||||
if (showMenu) {
|
||||
setShowMenu(false)
|
||||
setHasMouseEntered(false)
|
||||
}
|
||||
})
|
||||
|
||||
// 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 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) => {
|
||||
vscode.postMessage({
|
||||
type: "browserSettings",
|
||||
browserSettings: {
|
||||
...browserSettings,
|
||||
headless,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 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" }}>
|
||||
<VSCodeButton appearance="icon" onClick={openBrowserSettings}>
|
||||
<div ref={containerRef} style={{ position: "relative", marginTop: "-1px" }} onMouseLeave={handleControlsMouseLeave}>
|
||||
<VSCodeButton appearance="icon" onClick={() => setShowMenu(!showMenu)} disabled={disabled}>
|
||||
<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 maxWidth={maxWidth} />
|
||||
<BrowserSettingsMenu disabled={!shouldShowSettings} maxWidth={maxWidth} />
|
||||
</div>
|
||||
|
||||
{/* Screenshot Area */}
|
||||
|
||||
@@ -1,239 +0,0 @@
|
||||
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
|
||||
@@ -60,8 +60,12 @@ const OpenRouterModelPicker: React.FC<OpenRouterModelPickerProps> = ({ isPopup }
|
||||
}, [])
|
||||
|
||||
const modelIds = useMemo(() => {
|
||||
return Object.keys(openRouterModels).sort((a, b) => a.localeCompare(b))
|
||||
}, [openRouterModels])
|
||||
const unfilteredModelIds = Object.keys(openRouterModels).sort((a, b) => a.localeCompare(b))
|
||||
|
||||
return apiConfiguration?.apiProvider === "cline"
|
||||
? unfilteredModelIds.filter((id) => !id.includes(":free"))
|
||||
: unfilteredModelIds
|
||||
}, [openRouterModels, apiConfiguration?.apiProvider])
|
||||
|
||||
const searchableItems = useMemo(() => {
|
||||
return modelIds.map((id) => ({
|
||||
|
||||
@@ -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,7 +32,6 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
||||
telemetrySetting,
|
||||
setTelemetrySetting,
|
||||
chatSettings,
|
||||
remoteBrowserHost,
|
||||
planActSeparateModelsSetting,
|
||||
setPlanActSeparateModelsSetting,
|
||||
} = useExtensionState()
|
||||
@@ -114,24 +113,6 @@ 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],
|
||||
@@ -298,9 +279,6 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Browser Settings Section */}
|
||||
<BrowserSettingsSection />
|
||||
|
||||
{IS_DEV && (
|
||||
<>
|
||||
<div style={{ marginTop: "10px", marginBottom: "4px" }}>Debug</div>
|
||||
@@ -318,6 +296,22 @@ 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",
|
||||
@@ -325,7 +319,6 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
||||
fontSize: "12px",
|
||||
lineHeight: "1.2",
|
||||
padding: "0 8px 15px 0",
|
||||
marginTop: "auto",
|
||||
}}>
|
||||
<p
|
||||
style={{
|
||||
|
||||
@@ -26,7 +26,6 @@ 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)
|
||||
@@ -189,14 +188,6 @@ 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