Skip to content

Commit 699ae18

Browse files
committed
Add browser settings to change headless mode and size
1 parent e35d69d commit 699ae18

File tree

11 files changed

+462
-40
lines changed

11 files changed

+462
-40
lines changed

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/core/Cline.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ import { fixModelHtmlEscaping } from "../utils/string"
5656
import { OpenAiHandler } from "../api/providers/openai"
5757
import CheckpointTracker from "../integrations/checkpoints/CheckpointTracker"
5858
import getFolderSize from "get-folder-size"
59+
import { BrowserSettings } from "../shared/BrowserSettings"
5960

6061
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) ?? path.join(os.homedir(), "Desktop") // may or may not exist but fs checking existence would immediately ask for permission which would be bad UX, need to come up with a better solution
6162

@@ -69,10 +70,11 @@ export class Cline {
6970
api: ApiHandler
7071
private terminalManager: TerminalManager
7172
private urlContentFetcher: UrlContentFetcher
72-
private browserSession: BrowserSession
73+
browserSession: BrowserSession
7374
private didEditFile: boolean = false
7475
customInstructions?: string
7576
autoApprovalSettings: AutoApprovalSettings
77+
private browserSettings: BrowserSettings
7678
apiConversationHistory: Anthropic.MessageParam[] = []
7779
clineMessages: ClineMessage[] = []
7880
private askResponse?: ClineAskResponse
@@ -107,6 +109,7 @@ export class Cline {
107109
provider: ClineProvider,
108110
apiConfiguration: ApiConfiguration,
109111
autoApprovalSettings: AutoApprovalSettings,
112+
browserSettings: BrowserSettings,
110113
customInstructions?: string,
111114
task?: string,
112115
images?: string[],
@@ -116,10 +119,11 @@ export class Cline {
116119
this.api = buildApiHandler(apiConfiguration)
117120
this.terminalManager = new TerminalManager()
118121
this.urlContentFetcher = new UrlContentFetcher(provider.context)
119-
this.browserSession = new BrowserSession(provider.context)
122+
this.browserSession = new BrowserSession(provider.context, browserSettings)
120123
this.diffViewProvider = new DiffViewProvider(cwd)
121124
this.customInstructions = customInstructions
122125
this.autoApprovalSettings = autoApprovalSettings
126+
this.browserSettings = browserSettings
123127
if (historyItem) {
124128
this.taskId = historyItem.id
125129
this.conversationHistoryDeletedRange = historyItem.conversationHistoryDeletedRange
@@ -132,6 +136,11 @@ export class Cline {
132136
}
133137
}
134138

139+
updateBrowserSettings(browserSettings: BrowserSettings) {
140+
this.browserSettings = browserSettings
141+
this.browserSession.browserSettings = browserSettings
142+
}
143+
135144
// Storing task to disk for history
136145

137146
private async ensureTaskDirectoryExists(): Promise<string> {
@@ -1177,7 +1186,12 @@ export class Cline {
11771186
throw new Error("MCP hub not available")
11781187
}
11791188

1180-
let systemPrompt = await SYSTEM_PROMPT(cwd, this.api.getModel().info.supportsComputerUse ?? false, mcpHub)
1189+
let systemPrompt = await SYSTEM_PROMPT(
1190+
cwd,
1191+
this.api.getModel().info.supportsComputerUse ?? false,
1192+
mcpHub,
1193+
this.browserSettings,
1194+
)
11811195
let settingsCustomInstructions = this.customInstructions?.trim()
11821196
const clineRulesFilePath = path.resolve(cwd, GlobalFileNames.clineRules)
11831197
let clineRulesFileInstructions: string | undefined

src/core/prompts/system.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ import defaultShell from "default-shell"
22
import os from "os"
33
import osName from "os-name"
44
import { McpHub } from "../../services/mcp/McpHub"
5+
import { BrowserSettings } from "../../shared/BrowserSettings"
56

67
export const SYSTEM_PROMPT = async (
78
cwd: string,
89
supportsComputerUse: boolean,
910
mcpHub: McpHub,
11+
browserSettings: BrowserSettings,
1012
) => `You are Cline, a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices.
1113
1214
====
@@ -143,7 +145,7 @@ Usage:
143145
Description: Request to interact with a Puppeteer-controlled browser. Every action, except \`close\`, will be responded to with a screenshot of the browser's current state, along with any new console logs. You may only perform one browser action per message, and wait for the user's response including a screenshot and logs to determine the next action.
144146
- The sequence of actions **must always start with** launching the browser at a URL, and **must always end with** closing the browser. If you need to visit a new URL that is not possible to navigate to from the current webpage, you must first close the browser, then launch again at the new URL.
145147
- While the browser is active, only the \`browser_action\` tool can be used. No other tools should be called during this time. You may proceed to use other tools only after closing the browser. For example if you run into an error and need to fix a file, you must close the browser, then use other tools to make the necessary changes, then re-launch the browser to verify the result.
146-
- The browser window has a resolution of **900x600** pixels. When performing any click actions, ensure the coordinates are within this resolution range.
148+
- The browser window has a resolution of **${browserSettings.viewport.width}x${browserSettings.viewport.height}** pixels. When performing any click actions, ensure the coordinates are within this resolution range.
147149
- Before clicking on any elements such as icons, links, or buttons, you must consult the provided screenshot of the page to determine the coordinates of the element. The click should be targeted at the **center of the element**, not on its edges.
148150
Parameters:
149151
- action: (required) The action to perform. The available actions are:
@@ -161,7 +163,7 @@ Parameters:
161163
- Example: \`<action>close</action>\`
162164
- url: (optional) Use this for providing the URL for the \`launch\` action.
163165
* Example: <url>https://example.com</url>
164-
- coordinate: (optional) The X and Y coordinates for the \`click\` action. Coordinates should be within the **900x600** resolution.
166+
- coordinate: (optional) The X and Y coordinates for the \`click\` action. Coordinates should be within the **${browserSettings.viewport.width}x${browserSettings.viewport.height}** resolution.
165167
* Example: <coordinate>450,300</coordinate>
166168
- text: (optional) Use this for providing the text for the \`type\` action.
167169
* Example: <text>Hello, world!</text>

src/core/webview/ClineProvider.ts

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { openMention } from "../mentions"
2323
import { getNonce } from "./getNonce"
2424
import { getUri } from "./getUri"
2525
import { AutoApprovalSettings, DEFAULT_AUTO_APPROVAL_SETTINGS } from "../../shared/AutoApprovalSettings"
26+
import { BrowserSettings, DEFAULT_BROWSER_SETTINGS } from "../../shared/BrowserSettings"
2627

2728
/*
2829
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
@@ -61,6 +62,7 @@ type GlobalStateKey =
6162
| "openRouterModelId"
6263
| "openRouterModelInfo"
6364
| "autoApprovalSettings"
65+
| "browserSettings"
6466

6567
export const GlobalFileNames = {
6668
apiConversationHistory: "api_conversation_history.json",
@@ -210,17 +212,18 @@ export class ClineProvider implements vscode.WebviewViewProvider {
210212

211213
async initClineWithTask(task?: string, images?: string[]) {
212214
await this.clearTask() // ensures that an exising task doesn't exist before starting a new one, although this shouldn't be possible since user must clear task before starting a new one
213-
const { apiConfiguration, customInstructions, autoApprovalSettings } = await this.getState()
214-
this.cline = new Cline(this, apiConfiguration, autoApprovalSettings, customInstructions, task, images)
215+
const { apiConfiguration, customInstructions, autoApprovalSettings, browserSettings } = await this.getState()
216+
this.cline = new Cline(this, apiConfiguration, autoApprovalSettings, browserSettings, customInstructions, task, images)
215217
}
216218

217219
async initClineWithHistoryItem(historyItem: HistoryItem) {
218220
await this.clearTask()
219-
const { apiConfiguration, customInstructions, autoApprovalSettings } = await this.getState()
221+
const { apiConfiguration, customInstructions, autoApprovalSettings, browserSettings } = await this.getState()
220222
this.cline = new Cline(
221223
this,
222224
apiConfiguration,
223225
autoApprovalSettings,
226+
browserSettings,
224227
customInstructions,
225228
undefined,
226229
undefined,
@@ -436,6 +439,20 @@ export class ClineProvider implements vscode.WebviewViewProvider {
436439
await this.postStateToWebview()
437440
}
438441
break
442+
case "browserSettings":
443+
if (message.browserSettings) {
444+
await this.updateGlobalState("browserSettings", message.browserSettings)
445+
if (this.cline) {
446+
this.cline.updateBrowserSettings(message.browserSettings)
447+
}
448+
await this.postStateToWebview()
449+
}
450+
break
451+
// case "relaunchChromeDebugMode":
452+
// if (this.cline) {
453+
// this.cline.browserSession.relaunchChromeDebugMode()
454+
// }
455+
// break
439456
case "askResponse":
440457
this.cline?.handleWebviewAskResponse(message.askResponse!, message.text, message.images)
441458
break
@@ -908,8 +925,14 @@ export class ClineProvider implements vscode.WebviewViewProvider {
908925
}
909926

910927
async getStateToPostToWebview(): Promise<ExtensionState> {
911-
const { apiConfiguration, lastShownAnnouncementId, customInstructions, taskHistory, autoApprovalSettings } =
912-
await this.getState()
928+
const {
929+
apiConfiguration,
930+
lastShownAnnouncementId,
931+
customInstructions,
932+
taskHistory,
933+
autoApprovalSettings,
934+
browserSettings,
935+
} = await this.getState()
913936
return {
914937
version: this.context.extension?.packageJSON?.version ?? "",
915938
apiConfiguration,
@@ -921,6 +944,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
921944
taskHistory: (taskHistory || []).filter((item) => item.ts && item.task).sort((a, b) => b.ts - a.ts),
922945
shouldShowAnnouncement: lastShownAnnouncementId !== this.latestAnnouncementId,
923946
autoApprovalSettings,
947+
browserSettings,
924948
}
925949
}
926950

@@ -1006,6 +1030,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
10061030
customInstructions,
10071031
taskHistory,
10081032
autoApprovalSettings,
1033+
browserSettings,
10091034
] = await Promise.all([
10101035
this.getGlobalState("apiProvider") as Promise<ApiProvider | undefined>,
10111036
this.getGlobalState("apiModelId") as Promise<string | undefined>,
@@ -1036,6 +1061,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
10361061
this.getGlobalState("customInstructions") as Promise<string | undefined>,
10371062
this.getGlobalState("taskHistory") as Promise<HistoryItem[] | undefined>,
10381063
this.getGlobalState("autoApprovalSettings") as Promise<AutoApprovalSettings | undefined>,
1064+
this.getGlobalState("browserSettings") as Promise<BrowserSettings | undefined>,
10391065
])
10401066

10411067
let apiProvider: ApiProvider
@@ -1084,6 +1110,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
10841110
customInstructions,
10851111
taskHistory,
10861112
autoApprovalSettings: autoApprovalSettings || DEFAULT_AUTO_APPROVAL_SETTINGS, // default value can be 0 or empty string
1113+
browserSettings: browserSettings || DEFAULT_BROWSER_SETTINGS,
10871114
}
10881115
}
10891116

src/services/browser/BrowserSession.ts

Lines changed: 93 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,26 @@ import pWaitFor from "p-wait-for"
88
import delay from "delay"
99
import { fileExistsAtPath } from "../../utils/fs"
1010
import { BrowserActionResult } from "../../shared/ExtensionMessage"
11+
import { BrowserSettings } from "../../shared/BrowserSettings"
12+
// import * as chromeLauncher from "chrome-launcher"
1113

1214
interface PCRStats {
1315
puppeteer: { launch: typeof launch }
1416
executablePath: string
1517
}
1618

19+
// const DEBUG_PORT = 9222 // Chrome's default debugging port
20+
1721
export class BrowserSession {
1822
private context: vscode.ExtensionContext
1923
private browser?: Browser
2024
private page?: Page
2125
private currentMousePosition?: string
26+
browserSettings: BrowserSettings
2227

23-
constructor(context: vscode.ExtensionContext) {
28+
constructor(context: vscode.ExtensionContext, browserSettings: BrowserSettings) {
2429
this.context = context
30+
this.browserSettings = browserSettings
2531
}
2632

2733
private async ensureChromiumExists(): Promise<PCRStats> {
@@ -45,6 +51,70 @@ export class BrowserSession {
4551
return stats
4652
}
4753

54+
// private async checkExistingChromeDebugger(): Promise<boolean> {
55+
// try {
56+
// // Try to connect to existing debugger
57+
// const response = await fetch(`http://localhost:${DEBUG_PORT}/json/version`)
58+
// return response.ok
59+
// } catch {
60+
// return false
61+
// }
62+
// }
63+
64+
// async relaunchChromeDebugMode() {
65+
// const result = await vscode.window.showWarningMessage(
66+
// "This will close your existing Chrome tabs and relaunch Chrome in debug mode. Are you sure?",
67+
// { modal: true },
68+
// "Yes",
69+
// )
70+
71+
// if (result !== "Yes") {
72+
// return
73+
// }
74+
75+
// // // Kill any existing Chrome instances
76+
// // await chromeLauncher.killAll()
77+
78+
// // // Launch Chrome with debug port
79+
// // const launcher = new chromeLauncher.Launcher({
80+
// // port: DEBUG_PORT,
81+
// // chromeFlags: ["--remote-debugging-port=" + DEBUG_PORT, "--no-first-run", "--no-default-browser-check"],
82+
// // })
83+
84+
// // await launcher.launch()
85+
// const installation = chromeLauncher.Launcher.getFirstInstallation()
86+
// if (!installation) {
87+
// throw new Error("Could not find Chrome installation on this system")
88+
// }
89+
// console.log("chrome installation", installation)
90+
// }
91+
92+
// private async getSystemChromeExecutablePath(): Promise<string> {
93+
// // Find installed Chrome
94+
// const installation = chromeLauncher.Launcher.getFirstInstallation()
95+
// if (!installation) {
96+
// throw new Error("Could not find Chrome installation on this system")
97+
// }
98+
// console.log("chrome installation", installation)
99+
// return installation
100+
// }
101+
102+
// /**
103+
// * Helper to detect user’s default Chrome data dir.
104+
// * Adjust for OS if needed.
105+
// */
106+
// private getDefaultChromeUserDataDir(): string {
107+
// const homedir = require("os").homedir()
108+
// switch (process.platform) {
109+
// case "win32":
110+
// return path.join(homedir, "AppData", "Local", "Google", "Chrome", "User Data")
111+
// case "darwin":
112+
// return path.join(homedir, "Library", "Application Support", "Google", "Chrome")
113+
// default:
114+
// return path.join(homedir, ".config", "google-chrome")
115+
// }
116+
// }
117+
48118
async launchBrowser() {
49119
console.log("launch browser called")
50120
if (this.browser) {
@@ -58,12 +128,29 @@ export class BrowserSession {
58128
"--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",
59129
],
60130
executablePath: stats.executablePath,
61-
defaultViewport: {
62-
width: 900,
63-
height: 600,
64-
},
65-
// headless: false,
131+
defaultViewport: this.browserSettings.viewport,
132+
headless: this.browserSettings.headless,
66133
})
134+
135+
// if (this.browserSettings.chromeType === "system") {
136+
// const userDataDir = this.getDefaultChromeUserDataDir()
137+
// this.browser = await stats.puppeteer.launch({
138+
// args: [`--user-data-dir=${userDataDir}`, "--profile-directory=Default"],
139+
// executablePath: await this.getSystemChromeExecutablePath(),
140+
// defaultViewport: this.browserSettings.viewport,
141+
// headless: this.browserSettings.headless,
142+
// })
143+
// } else {
144+
// this.browser = await stats.puppeteer.launch({
145+
// args: [
146+
// "--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",
147+
// ],
148+
// executablePath: stats.executablePath,
149+
// defaultViewport: this.browserSettings.viewport,
150+
// headless: this.browserSettings.headless,
151+
// })
152+
// }
153+
67154
// (latest version of puppeteer does not add headless to user agent)
68155
this.page = await this.browser?.newPage()
69156
}

src/shared/BrowserSettings.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
export interface BrowserSettings {
2+
// Viewport size settings
3+
viewport: {
4+
width: number
5+
height: number
6+
}
7+
// Browser mode settings
8+
headless: boolean
9+
// Chrome installation to use
10+
// chromeType: "chromium" | "system"
11+
}
12+
13+
export const DEFAULT_BROWSER_SETTINGS: BrowserSettings = {
14+
viewport: {
15+
width: 900,
16+
height: 600,
17+
},
18+
headless: true,
19+
// chromeType: "chromium",
20+
}
21+
22+
export const BROWSER_VIEWPORT_PRESETS = {
23+
"Large Desktop (1280x800)": { width: 1280, height: 800 },
24+
"Small Desktop (900x600)": { width: 900, height: 600 },
25+
"Tablet (768x1024)": { width: 768, height: 1024 },
26+
"Mobile (360x640)": { width: 360, height: 640 },
27+
} as const

0 commit comments

Comments
 (0)