Skip to content

Commit 5bec372

Browse files
committed
Browser Automation Improvements
* Added multi-tab remote Chrome support * Added support for hover * Properly caching remote browser host in global state * Cleanup functions
1 parent b73bc39 commit 5bec372

File tree

9 files changed

+391
-457
lines changed

9 files changed

+391
-457
lines changed

src/core/Cline.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2437,7 +2437,8 @@ export class Cline {
24372437
}
24382438
break
24392439
} else {
2440-
let browserActionResult: BrowserActionResult
2440+
// Initialize with empty object to avoid "used before assigned" errors
2441+
let browserActionResult: BrowserActionResult = {}
24412442
if (action === "launch") {
24422443
if (!url) {
24432444
this.consecutiveMistakeCount++
@@ -2523,9 +2524,9 @@ export class Cline {
25232524
pushToolResult(
25242525
formatResponse.toolResult(
25252526
`The browser action has been executed. The console logs and screenshot have been captured for your analysis.\n\nConsole logs:\n${
2526-
browserActionResult.logs || "(No new logs)"
2527+
browserActionResult?.logs || "(No new logs)"
25272528
}\n\n(REMEMBER: if you need to proceed to using non-\`browser_action\` tools or launch a new browser, you MUST first close this browser. For example, if after analyzing the logs and screenshot you need to edit a file, you must first close the browser before you can use the write_to_file tool.)`,
2528-
browserActionResult.screenshot ? [browserActionResult.screenshot] : [],
2529+
browserActionResult?.screenshot ? [browserActionResult.screenshot] : [],
25292530
),
25302531
)
25312532
break

src/core/webview/ClineProvider.ts

Lines changed: 20 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import { McpHub } from "../../services/mcp/McpHub"
3030
import { McpServerManager } from "../../services/mcp/McpServerManager"
3131
import { ShadowCheckpointService } from "../../services/checkpoints/ShadowCheckpointService"
3232
import { BrowserSession } from "../../services/browser/BrowserSession"
33-
import { discoverChromeInstances } from "../../services/browser/browserDiscovery"
33+
import { discoverChromeHostUrl, tryChromeHostUrl } from "../../services/browser/browserDiscovery"
3434
import { fileExistsAtPath } from "../../utils/fs"
3535
import { playSound, setSoundEnabled, setSoundVolume } from "../../utils/sound"
3636
import { singleCompletionHandler } from "../../utils/single-completion-handler"
@@ -1279,74 +1279,17 @@ export class ClineProvider implements vscode.WebviewViewProvider {
12791279
await this.postStateToWebview()
12801280
break
12811281
case "testBrowserConnection":
1282-
try {
1283-
const browserSession = new BrowserSession(this.context)
1284-
// If no text is provided, try auto-discovery
1285-
if (!message.text) {
1286-
try {
1287-
const discoveredHost = await discoverChromeInstances()
1288-
if (discoveredHost) {
1289-
// Test the connection to the discovered host
1290-
const result = await browserSession.testConnection(discoveredHost)
1291-
// Send the result back to the webview
1292-
await this.postMessageToWebview({
1293-
type: "browserConnectionResult",
1294-
success: result.success,
1295-
text: `Auto-discovered and tested connection to Chrome at ${discoveredHost}: ${result.message}`,
1296-
values: { endpoint: result.endpoint },
1297-
})
1298-
} else {
1299-
await this.postMessageToWebview({
1300-
type: "browserConnectionResult",
1301-
success: false,
1302-
text: "No Chrome instances found on the network. Make sure Chrome is running with remote debugging enabled (--remote-debugging-port=9222).",
1303-
})
1304-
}
1305-
} catch (error) {
1306-
await this.postMessageToWebview({
1307-
type: "browserConnectionResult",
1308-
success: false,
1309-
text: `Error during auto-discovery: ${error instanceof Error ? error.message : String(error)}`,
1310-
})
1311-
}
1312-
} else {
1313-
// Test the provided URL
1314-
const result = await browserSession.testConnection(message.text)
1315-
1316-
// Send the result back to the webview
1317-
await this.postMessageToWebview({
1318-
type: "browserConnectionResult",
1319-
success: result.success,
1320-
text: result.message,
1321-
values: { endpoint: result.endpoint },
1322-
})
1323-
}
1324-
} catch (error) {
1325-
await this.postMessageToWebview({
1326-
type: "browserConnectionResult",
1327-
success: false,
1328-
text: `Error testing connection: ${error instanceof Error ? error.message : String(error)}`,
1329-
})
1330-
}
1331-
break
1332-
case "discoverBrowser":
1333-
try {
1334-
const discoveredHost = await discoverChromeInstances()
1335-
1336-
if (discoveredHost) {
1337-
// Don't update the remoteBrowserHost state when auto-discovering
1338-
// This way we don't override the user's preference
1339-
1340-
// Test the connection to get the endpoint
1341-
const browserSession = new BrowserSession(this.context)
1342-
const result = await browserSession.testConnection(discoveredHost)
1343-
1282+
// If no text is provided, try auto-discovery
1283+
if (!message.text) {
1284+
// Use testBrowserConnection for auto-discovery
1285+
const chromeHostUrl = await discoverChromeHostUrl()
1286+
if (chromeHostUrl) {
13441287
// Send the result back to the webview
13451288
await this.postMessageToWebview({
13461289
type: "browserConnectionResult",
1347-
success: true,
1348-
text: `Successfully discovered and connected to Chrome at ${discoveredHost}`,
1349-
values: { endpoint: result.endpoint },
1290+
success: !!chromeHostUrl,
1291+
text: `Auto-discovered and tested connection to Chrome: ${chromeHostUrl}`,
1292+
values: { endpoint: chromeHostUrl },
13501293
})
13511294
} else {
13521295
await this.postMessageToWebview({
@@ -1355,11 +1298,17 @@ export class ClineProvider implements vscode.WebviewViewProvider {
13551298
text: "No Chrome instances found on the network. Make sure Chrome is running with remote debugging enabled (--remote-debugging-port=9222).",
13561299
})
13571300
}
1358-
} catch (error) {
1301+
} else {
1302+
// Test the provided URL
1303+
const customHostUrl = message.text
1304+
const hostIsValid = await tryChromeHostUrl(message.text)
1305+
// Send the result back to the webview
13591306
await this.postMessageToWebview({
13601307
type: "browserConnectionResult",
1361-
success: false,
1362-
text: `Error discovering browser: ${error instanceof Error ? error.message : String(error)}`,
1308+
success: hostIsValid,
1309+
text: hostIsValid
1310+
? `Successfully connected to Chrome: ${customHostUrl}`
1311+
: "Failed to connect to Chrome",
13631312
})
13641313
}
13651314
break
@@ -2393,6 +2342,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
23932342
screenshotQuality: screenshotQuality ?? 75,
23942343
remoteBrowserHost,
23952344
remoteBrowserEnabled: remoteBrowserEnabled ?? false,
2345+
cachedChromeHostUrl: (await this.getGlobalState("cachedChromeHostUrl")) as string | undefined,
23962346
preferredLanguage: preferredLanguage ?? "English",
23972347
writeDelayMs: writeDelayMs ?? 1000,
23982348
terminalOutputLineLimit: terminalOutputLineLimit ?? 500,
@@ -2548,6 +2498,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
25482498
screenshotQuality: stateValues.screenshotQuality ?? 75,
25492499
remoteBrowserHost: stateValues.remoteBrowserHost,
25502500
remoteBrowserEnabled: stateValues.remoteBrowserEnabled ?? false,
2501+
cachedChromeHostUrl: stateValues.cachedChromeHostUrl as string | undefined,
25512502
fuzzyMatchThreshold: stateValues.fuzzyMatchThreshold ?? 1.0,
25522503
writeDelayMs: stateValues.writeDelayMs ?? 1000,
25532504
terminalOutputLineLimit: stateValues.terminalOutputLineLimit ?? 500,

src/core/webview/__tests__/ClineProvider.test.ts

Lines changed: 7 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,12 @@ jest.mock("../../../services/browser/BrowserSession", () => ({
7979

8080
// Mock browserDiscovery
8181
jest.mock("../../../services/browser/browserDiscovery", () => ({
82-
discoverChromeInstances: jest.fn().mockImplementation(async () => {
82+
discoverChromeHostUrl: jest.fn().mockImplementation(async () => {
8383
return "http://localhost:9222"
8484
}),
85+
tryChromeHostUrl: jest.fn().mockImplementation(async (url) => {
86+
return url === "http://localhost:9222"
87+
}),
8588
}))
8689
jest.mock(
8790
"@modelcontextprotocol/sdk/types.js",
@@ -1867,9 +1870,9 @@ describe("ClineProvider", () => {
18671870
type: "testBrowserConnection",
18681871
})
18691872

1870-
// Verify discoverChromeInstances was called
1871-
const { discoverChromeInstances } = require("../../../services/browser/browserDiscovery")
1872-
expect(discoverChromeInstances).toHaveBeenCalled()
1873+
// Verify discoverChromeHostUrl was called
1874+
const { discoverChromeHostUrl } = require("../../../services/browser/browserDiscovery")
1875+
expect(discoverChromeHostUrl).toHaveBeenCalled()
18731876

18741877
// Verify postMessage was called with success result
18751878
expect(mockPostMessage).toHaveBeenCalledWith(
@@ -1880,77 +1883,6 @@ describe("ClineProvider", () => {
18801883
}),
18811884
)
18821885
})
1883-
1884-
test("handles discoverBrowser message", async () => {
1885-
// Get the message handler
1886-
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
1887-
1888-
// Test browser discovery
1889-
await messageHandler({
1890-
type: "discoverBrowser",
1891-
})
1892-
1893-
// Verify discoverChromeInstances was called
1894-
const { discoverChromeInstances } = require("../../../services/browser/browserDiscovery")
1895-
expect(discoverChromeInstances).toHaveBeenCalled()
1896-
1897-
// Verify postMessage was called with success result
1898-
expect(mockPostMessage).toHaveBeenCalledWith(
1899-
expect.objectContaining({
1900-
type: "browserConnectionResult",
1901-
success: true,
1902-
text: expect.stringContaining("Successfully discovered and connected to Chrome"),
1903-
}),
1904-
)
1905-
})
1906-
1907-
test("handles errors during browser discovery", async () => {
1908-
// Mock discoverChromeInstances to throw an error
1909-
const { discoverChromeInstances } = require("../../../services/browser/browserDiscovery")
1910-
discoverChromeInstances.mockImplementationOnce(() => {
1911-
throw new Error("Discovery error")
1912-
})
1913-
1914-
// Get the message handler
1915-
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
1916-
1917-
// Test browser discovery with error
1918-
await messageHandler({
1919-
type: "discoverBrowser",
1920-
})
1921-
1922-
// Verify postMessage was called with error result
1923-
expect(mockPostMessage).toHaveBeenCalledWith(
1924-
expect.objectContaining({
1925-
type: "browserConnectionResult",
1926-
success: false,
1927-
text: expect.stringContaining("Error discovering browser"),
1928-
}),
1929-
)
1930-
})
1931-
1932-
test("handles case when no browsers are discovered", async () => {
1933-
// Mock discoverChromeInstances to return null (no browsers found)
1934-
const { discoverChromeInstances } = require("../../../services/browser/browserDiscovery")
1935-
discoverChromeInstances.mockImplementationOnce(() => null)
1936-
1937-
// Get the message handler
1938-
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
1939-
1940-
// Test browser discovery with no browsers found
1941-
await messageHandler({
1942-
type: "discoverBrowser",
1943-
})
1944-
1945-
// Verify postMessage was called with failure result
1946-
expect(mockPostMessage).toHaveBeenCalledWith(
1947-
expect.objectContaining({
1948-
type: "browserConnectionResult",
1949-
success: false,
1950-
text: expect.stringContaining("No Chrome instances found"),
1951-
}),
1952-
)
1953-
})
19541886
})
19551887
})
19561888

0 commit comments

Comments
 (0)