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