Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions src/core/webview/ClineProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ import WorkspaceTracker from "../../integrations/workspace/WorkspaceTracker"
import { McpHub } from "../../services/mcp/McpHub"
import { McpServerManager } from "../../services/mcp/McpServerManager"
import { ShadowCheckpointService } from "../../services/checkpoints/ShadowCheckpointService"
import { BrowserSession } from "../../services/browser/BrowserSession"
import { discoverChromeInstances } from "../../services/browser/browserDiscovery"
import { fileExistsAtPath } from "../../utils/fs"
import { playSound, setSoundEnabled, setSoundVolume } from "../../utils/sound"
import { singleCompletionHandler } from "../../utils/single-completion-handler"
Expand Down Expand Up @@ -1262,6 +1264,105 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await this.updateGlobalState("browserViewportSize", browserViewportSize)
await this.postStateToWebview()
break
case "remoteBrowserHost":
await this.updateGlobalState("remoteBrowserHost", message.text)
await this.postStateToWebview()
break
case "remoteBrowserEnabled":
// Store the preference in global state
// remoteBrowserEnabled now means "enable remote browser connection"
await this.updateGlobalState("remoteBrowserEnabled", message.bool ?? false)
// If disabling remote browser connection, clear the remoteBrowserHost
if (!message.bool) {
await this.updateGlobalState("remoteBrowserHost", undefined)
}
await this.postStateToWebview()
break
case "testBrowserConnection":
try {
const browserSession = new BrowserSession(this.context)
// 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 browserSession = new BrowserSession(this.context)
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 "fuzzyMatchThreshold":
await this.updateGlobalState("fuzzyMatchThreshold", message.value)
await this.postStateToWebview()
Expand Down Expand Up @@ -2188,6 +2289,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
soundVolume,
browserViewportSize,
screenshotQuality,
remoteBrowserHost,
remoteBrowserEnabled,
preferredLanguage,
writeDelayMs,
terminalOutputLimit,
Expand Down Expand Up @@ -2246,6 +2349,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
soundVolume: soundVolume ?? 0.5,
browserViewportSize: browserViewportSize ?? "900x600",
screenshotQuality: screenshotQuality ?? 75,
remoteBrowserHost,
remoteBrowserEnabled: remoteBrowserEnabled ?? false,
preferredLanguage: preferredLanguage ?? "English",
writeDelayMs: writeDelayMs ?? 1000,
terminalOutputLimit: terminalOutputLimit ?? TERMINAL_OUTPUT_LIMIT,
Expand Down Expand Up @@ -2399,6 +2504,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
soundVolume: stateValues.soundVolume,
browserViewportSize: stateValues.browserViewportSize ?? "900x600",
screenshotQuality: stateValues.screenshotQuality ?? 75,
remoteBrowserHost: stateValues.remoteBrowserHost,
remoteBrowserEnabled: stateValues.remoteBrowserEnabled ?? false,
fuzzyMatchThreshold: stateValues.fuzzyMatchThreshold ?? 1.0,
writeDelayMs: stateValues.writeDelayMs ?? 1000,
terminalOutputLimit: stateValues.terminalOutputLimit ?? TERMINAL_OUTPUT_LIMIT,
Expand Down
223 changes: 197 additions & 26 deletions src/core/webview/__tests__/ClineProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,34 @@ jest.mock("../../contextProxy", () => {
// Mock dependencies
jest.mock("vscode")
jest.mock("delay")

// Mock BrowserSession
jest.mock("../../../services/browser/BrowserSession", () => ({
BrowserSession: jest.fn().mockImplementation(() => ({
testConnection: jest.fn().mockImplementation(async (url) => {
if (url === "http://localhost:9222") {
return {
success: true,
message: "Successfully connected to Chrome",
endpoint: "ws://localhost:9222/devtools/browser/123",
}
} else {
return {
success: false,
message: "Failed to connect to Chrome",
endpoint: undefined,
}
}
}),
})),
}))

// Mock browserDiscovery
jest.mock("../../../services/browser/browserDiscovery", () => ({
discoverChromeInstances: jest.fn().mockImplementation(async () => {
return "http://localhost:9222"
}),
}))
jest.mock(
"@modelcontextprotocol/sdk/types.js",
() => ({
Expand Down Expand Up @@ -94,31 +122,7 @@ jest.mock("delay", () => {
return delayFn
})

// Mock MCP-related modules
jest.mock(
"@modelcontextprotocol/sdk/types.js",
() => ({
CallToolResultSchema: {},
ListResourcesResultSchema: {},
ListResourceTemplatesResultSchema: {},
ListToolsResultSchema: {},
ReadResourceResultSchema: {},
ErrorCode: {
InvalidRequest: "InvalidRequest",
MethodNotFound: "MethodNotFound",
InternalError: "InternalError",
},
McpError: class McpError extends Error {
code: string
constructor(code: string, message: string) {
super(message)
this.code = code
this.name = "McpError"
}
},
}),
{ virtual: true },
)
// MCP-related modules are mocked once above (lines 87-109)

jest.mock(
"@modelcontextprotocol/sdk/client/index.js",
Expand Down Expand Up @@ -598,7 +602,7 @@ describe("ClineProvider", () => {
expect(mockPostMessage).toHaveBeenCalled()
})

test("requestDelaySeconds defaults to 5 seconds", async () => {
test("requestDelaySeconds defaults to 10 seconds", async () => {
// Mock globalState.get to return undefined for requestDelaySeconds
;(mockContext.globalState.get as jest.Mock).mockImplementation((key: string) => {
if (key === "requestDelaySeconds") {
Expand Down Expand Up @@ -1591,6 +1595,173 @@ describe("ClineProvider", () => {
])
})
})

describe("browser connection features", () => {
beforeEach(async () => {
// Reset mocks
jest.clearAllMocks()
await provider.resolveWebviewView(mockWebviewView)
})

// Mock BrowserSession and discoverChromeInstances
jest.mock("../../../services/browser/BrowserSession", () => ({
BrowserSession: jest.fn().mockImplementation(() => ({
testConnection: jest.fn().mockImplementation(async (url) => {
if (url === "http://localhost:9222") {
return {
success: true,
message: "Successfully connected to Chrome",
endpoint: "ws://localhost:9222/devtools/browser/123",
}
} else {
return {
success: false,
message: "Failed to connect to Chrome",
endpoint: undefined,
}
}
}),
})),
}))

jest.mock("../../../services/browser/browserDiscovery", () => ({
discoverChromeInstances: jest.fn().mockImplementation(async () => {
return "http://localhost:9222"
}),
}))

test("handles testBrowserConnection with provided URL", async () => {
// Get the message handler
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]

// Test with valid URL
await messageHandler({
type: "testBrowserConnection",
text: "http://localhost:9222",
})

// Verify postMessage was called with success result
expect(mockPostMessage).toHaveBeenCalledWith(
expect.objectContaining({
type: "browserConnectionResult",
success: true,
text: expect.stringContaining("Successfully connected to Chrome"),
}),
)

// Reset mock
mockPostMessage.mockClear()

// Test with invalid URL
await messageHandler({
type: "testBrowserConnection",
text: "http://inlocalhost:9222",
})

// Verify postMessage was called with failure result
expect(mockPostMessage).toHaveBeenCalledWith(
expect.objectContaining({
type: "browserConnectionResult",
success: false,
text: expect.stringContaining("Failed to connect to Chrome"),
}),
)
})

test("handles testBrowserConnection with auto-discovery", async () => {
// Get the message handler
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]

// Test auto-discovery (no URL provided)
await messageHandler({
type: "testBrowserConnection",
})

// Verify discoverChromeInstances was called
const { discoverChromeInstances } = require("../../../services/browser/browserDiscovery")
expect(discoverChromeInstances).toHaveBeenCalled()

// Verify postMessage was called with success result
expect(mockPostMessage).toHaveBeenCalledWith(
expect.objectContaining({
type: "browserConnectionResult",
success: true,
text: expect.stringContaining("Auto-discovered and tested connection to Chrome"),
}),
)
})

test("handles discoverBrowser message", async () => {
// Get the message handler
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]

// Test browser discovery
await messageHandler({
type: "discoverBrowser",
})

// Verify discoverChromeInstances was called
const { discoverChromeInstances } = require("../../../services/browser/browserDiscovery")
expect(discoverChromeInstances).toHaveBeenCalled()

// Verify postMessage was called with success result
expect(mockPostMessage).toHaveBeenCalledWith(
expect.objectContaining({
type: "browserConnectionResult",
success: true,
text: expect.stringContaining("Successfully discovered and connected to Chrome"),
}),
)
})

test("handles errors during browser discovery", async () => {
// Mock discoverChromeInstances to throw an error
const { discoverChromeInstances } = require("../../../services/browser/browserDiscovery")
discoverChromeInstances.mockImplementationOnce(() => {
throw new Error("Discovery error")
})

// Get the message handler
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]

// Test browser discovery with error
await messageHandler({
type: "discoverBrowser",
})

// Verify postMessage was called with error result
expect(mockPostMessage).toHaveBeenCalledWith(
expect.objectContaining({
type: "browserConnectionResult",
success: false,
text: expect.stringContaining("Error discovering browser"),
}),
)
})

test("handles case when no browsers are discovered", async () => {
// Mock discoverChromeInstances to return null (no browsers found)
const { discoverChromeInstances } = require("../../../services/browser/browserDiscovery")
discoverChromeInstances.mockImplementationOnce(() => null)

// Get the message handler
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]

// Test browser discovery with no browsers found
await messageHandler({
type: "discoverBrowser",
})

// Verify postMessage was called with failure result
expect(mockPostMessage).toHaveBeenCalledWith(
expect.objectContaining({
type: "browserConnectionResult",
success: false,
text: expect.stringContaining("No Chrome instances found"),
}),
)
})
})
})

describe("ContextProxy integration", () => {
Expand Down
Loading