Skip to content

Commit 6cfc1cd

Browse files
authored
Merge pull request #1528 from afshawnlotfi/chrome-remote-webtools
Chrome remote webtools
2 parents 9dfdc42 + ce3e22e commit 6cfc1cd

File tree

10 files changed

+847
-36
lines changed

10 files changed

+847
-36
lines changed

src/core/webview/ClineProvider.ts

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ import WorkspaceTracker from "../../integrations/workspace/WorkspaceTracker"
3030
import { McpHub } from "../../services/mcp/McpHub"
3131
import { McpServerManager } from "../../services/mcp/McpServerManager"
3232
import { ShadowCheckpointService } from "../../services/checkpoints/ShadowCheckpointService"
33+
import { BrowserSession } from "../../services/browser/BrowserSession"
34+
import { discoverChromeInstances } from "../../services/browser/browserDiscovery"
3335
import { fileExistsAtPath } from "../../utils/fs"
3436
import { playSound, setSoundEnabled, setSoundVolume } from "../../utils/sound"
3537
import { singleCompletionHandler } from "../../utils/single-completion-handler"
@@ -1262,6 +1264,105 @@ export class ClineProvider implements vscode.WebviewViewProvider {
12621264
await this.updateGlobalState("browserViewportSize", browserViewportSize)
12631265
await this.postStateToWebview()
12641266
break
1267+
case "remoteBrowserHost":
1268+
await this.updateGlobalState("remoteBrowserHost", message.text)
1269+
await this.postStateToWebview()
1270+
break
1271+
case "remoteBrowserEnabled":
1272+
// Store the preference in global state
1273+
// remoteBrowserEnabled now means "enable remote browser connection"
1274+
await this.updateGlobalState("remoteBrowserEnabled", message.bool ?? false)
1275+
// If disabling remote browser connection, clear the remoteBrowserHost
1276+
if (!message.bool) {
1277+
await this.updateGlobalState("remoteBrowserHost", undefined)
1278+
}
1279+
await this.postStateToWebview()
1280+
break
1281+
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+
1344+
// Send the result back to the webview
1345+
await this.postMessageToWebview({
1346+
type: "browserConnectionResult",
1347+
success: true,
1348+
text: `Successfully discovered and connected to Chrome at ${discoveredHost}`,
1349+
values: { endpoint: result.endpoint },
1350+
})
1351+
} else {
1352+
await this.postMessageToWebview({
1353+
type: "browserConnectionResult",
1354+
success: false,
1355+
text: "No Chrome instances found on the network. Make sure Chrome is running with remote debugging enabled (--remote-debugging-port=9222).",
1356+
})
1357+
}
1358+
} catch (error) {
1359+
await this.postMessageToWebview({
1360+
type: "browserConnectionResult",
1361+
success: false,
1362+
text: `Error discovering browser: ${error instanceof Error ? error.message : String(error)}`,
1363+
})
1364+
}
1365+
break
12651366
case "fuzzyMatchThreshold":
12661367
await this.updateGlobalState("fuzzyMatchThreshold", message.value)
12671368
await this.postStateToWebview()
@@ -2197,6 +2298,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
21972298
soundVolume,
21982299
browserViewportSize,
21992300
screenshotQuality,
2301+
remoteBrowserHost,
2302+
remoteBrowserEnabled,
22002303
preferredLanguage,
22012304
writeDelayMs,
22022305
terminalOutputLimit,
@@ -2255,6 +2358,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
22552358
soundVolume: soundVolume ?? 0.5,
22562359
browserViewportSize: browserViewportSize ?? "900x600",
22572360
screenshotQuality: screenshotQuality ?? 75,
2361+
remoteBrowserHost,
2362+
remoteBrowserEnabled: remoteBrowserEnabled ?? false,
22582363
preferredLanguage: preferredLanguage ?? "English",
22592364
writeDelayMs: writeDelayMs ?? 1000,
22602365
terminalOutputLimit: terminalOutputLimit ?? TERMINAL_OUTPUT_LIMIT,
@@ -2408,6 +2513,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
24082513
soundVolume: stateValues.soundVolume,
24092514
browserViewportSize: stateValues.browserViewportSize ?? "900x600",
24102515
screenshotQuality: stateValues.screenshotQuality ?? 75,
2516+
remoteBrowserHost: stateValues.remoteBrowserHost,
2517+
remoteBrowserEnabled: stateValues.remoteBrowserEnabled ?? false,
24112518
fuzzyMatchThreshold: stateValues.fuzzyMatchThreshold ?? 1.0,
24122519
writeDelayMs: stateValues.writeDelayMs ?? 1000,
24132520
terminalOutputLimit: stateValues.terminalOutputLimit ?? TERMINAL_OUTPUT_LIMIT,

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

Lines changed: 197 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,34 @@ jest.mock("../../contextProxy", () => {
5555
// Mock dependencies
5656
jest.mock("vscode")
5757
jest.mock("delay")
58+
59+
// Mock BrowserSession
60+
jest.mock("../../../services/browser/BrowserSession", () => ({
61+
BrowserSession: jest.fn().mockImplementation(() => ({
62+
testConnection: jest.fn().mockImplementation(async (url) => {
63+
if (url === "http://localhost:9222") {
64+
return {
65+
success: true,
66+
message: "Successfully connected to Chrome",
67+
endpoint: "ws://localhost:9222/devtools/browser/123",
68+
}
69+
} else {
70+
return {
71+
success: false,
72+
message: "Failed to connect to Chrome",
73+
endpoint: undefined,
74+
}
75+
}
76+
}),
77+
})),
78+
}))
79+
80+
// Mock browserDiscovery
81+
jest.mock("../../../services/browser/browserDiscovery", () => ({
82+
discoverChromeInstances: jest.fn().mockImplementation(async () => {
83+
return "http://localhost:9222"
84+
}),
85+
}))
5886
jest.mock(
5987
"@modelcontextprotocol/sdk/types.js",
6088
() => ({
@@ -94,31 +122,7 @@ jest.mock("delay", () => {
94122
return delayFn
95123
})
96124

97-
// Mock MCP-related modules
98-
jest.mock(
99-
"@modelcontextprotocol/sdk/types.js",
100-
() => ({
101-
CallToolResultSchema: {},
102-
ListResourcesResultSchema: {},
103-
ListResourceTemplatesResultSchema: {},
104-
ListToolsResultSchema: {},
105-
ReadResourceResultSchema: {},
106-
ErrorCode: {
107-
InvalidRequest: "InvalidRequest",
108-
MethodNotFound: "MethodNotFound",
109-
InternalError: "InternalError",
110-
},
111-
McpError: class McpError extends Error {
112-
code: string
113-
constructor(code: string, message: string) {
114-
super(message)
115-
this.code = code
116-
this.name = "McpError"
117-
}
118-
},
119-
}),
120-
{ virtual: true },
121-
)
125+
// MCP-related modules are mocked once above (lines 87-109)
122126

123127
jest.mock(
124128
"@modelcontextprotocol/sdk/client/index.js",
@@ -598,7 +602,7 @@ describe("ClineProvider", () => {
598602
expect(mockPostMessage).toHaveBeenCalled()
599603
})
600604

601-
test("requestDelaySeconds defaults to 5 seconds", async () => {
605+
test("requestDelaySeconds defaults to 10 seconds", async () => {
602606
// Mock globalState.get to return undefined for requestDelaySeconds
603607
;(mockContext.globalState.get as jest.Mock).mockImplementation((key: string) => {
604608
if (key === "requestDelaySeconds") {
@@ -1781,6 +1785,173 @@ describe("ClineProvider", () => {
17811785
])
17821786
})
17831787
})
1788+
1789+
describe("browser connection features", () => {
1790+
beforeEach(async () => {
1791+
// Reset mocks
1792+
jest.clearAllMocks()
1793+
await provider.resolveWebviewView(mockWebviewView)
1794+
})
1795+
1796+
// Mock BrowserSession and discoverChromeInstances
1797+
jest.mock("../../../services/browser/BrowserSession", () => ({
1798+
BrowserSession: jest.fn().mockImplementation(() => ({
1799+
testConnection: jest.fn().mockImplementation(async (url) => {
1800+
if (url === "http://localhost:9222") {
1801+
return {
1802+
success: true,
1803+
message: "Successfully connected to Chrome",
1804+
endpoint: "ws://localhost:9222/devtools/browser/123",
1805+
}
1806+
} else {
1807+
return {
1808+
success: false,
1809+
message: "Failed to connect to Chrome",
1810+
endpoint: undefined,
1811+
}
1812+
}
1813+
}),
1814+
})),
1815+
}))
1816+
1817+
jest.mock("../../../services/browser/browserDiscovery", () => ({
1818+
discoverChromeInstances: jest.fn().mockImplementation(async () => {
1819+
return "http://localhost:9222"
1820+
}),
1821+
}))
1822+
1823+
test("handles testBrowserConnection with provided URL", async () => {
1824+
// Get the message handler
1825+
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
1826+
1827+
// Test with valid URL
1828+
await messageHandler({
1829+
type: "testBrowserConnection",
1830+
text: "http://localhost:9222",
1831+
})
1832+
1833+
// Verify postMessage was called with success result
1834+
expect(mockPostMessage).toHaveBeenCalledWith(
1835+
expect.objectContaining({
1836+
type: "browserConnectionResult",
1837+
success: true,
1838+
text: expect.stringContaining("Successfully connected to Chrome"),
1839+
}),
1840+
)
1841+
1842+
// Reset mock
1843+
mockPostMessage.mockClear()
1844+
1845+
// Test with invalid URL
1846+
await messageHandler({
1847+
type: "testBrowserConnection",
1848+
text: "http://inlocalhost:9222",
1849+
})
1850+
1851+
// Verify postMessage was called with failure result
1852+
expect(mockPostMessage).toHaveBeenCalledWith(
1853+
expect.objectContaining({
1854+
type: "browserConnectionResult",
1855+
success: false,
1856+
text: expect.stringContaining("Failed to connect to Chrome"),
1857+
}),
1858+
)
1859+
})
1860+
1861+
test("handles testBrowserConnection with auto-discovery", async () => {
1862+
// Get the message handler
1863+
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
1864+
1865+
// Test auto-discovery (no URL provided)
1866+
await messageHandler({
1867+
type: "testBrowserConnection",
1868+
})
1869+
1870+
// Verify discoverChromeInstances was called
1871+
const { discoverChromeInstances } = require("../../../services/browser/browserDiscovery")
1872+
expect(discoverChromeInstances).toHaveBeenCalled()
1873+
1874+
// Verify postMessage was called with success result
1875+
expect(mockPostMessage).toHaveBeenCalledWith(
1876+
expect.objectContaining({
1877+
type: "browserConnectionResult",
1878+
success: true,
1879+
text: expect.stringContaining("Auto-discovered and tested connection to Chrome"),
1880+
}),
1881+
)
1882+
})
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+
})
1954+
})
17841955
})
17851956

17861957
describe("ContextProxy integration", () => {

0 commit comments

Comments
 (0)