Skip to content

Commit 373c7df

Browse files
committed
add setting to save browser sessions
1 parent f47dd2d commit 373c7df

File tree

7 files changed

+98
-25
lines changed

7 files changed

+98
-25
lines changed

src/core/webview/ClineProvider.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ type GlobalStateKey =
120120
| "experimentalDiffStrategy"
121121
| "autoApprovalEnabled"
122122
| "customModes" // Array of custom modes
123+
| "keepBrowserOpen"
123124

124125
export const GlobalFileNames = {
125126
apiConversationHistory: "api_conversation_history.json",
@@ -1177,6 +1178,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
11771178
}
11781179
await this.postStateToWebview()
11791180
break
1181+
case "keepBrowserOpen":
1182+
await this.updateGlobalState("keepBrowserOpen", message.bool ?? false)
1183+
await this.postStateToWebview()
1184+
break
11801185
case "updateMcpTimeout":
11811186
if (message.serverName && typeof message.timeout === "number") {
11821187
try {
@@ -1836,6 +1841,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
18361841
enhancementApiConfigId,
18371842
experimentalDiffStrategy,
18381843
autoApprovalEnabled,
1844+
keepBrowserOpen,
18391845
} = await this.getState()
18401846

18411847
const allowedCommands = vscode.workspace.getConfiguration("roo-cline").get<string[]>("allowedCommands") || []
@@ -1844,6 +1850,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
18441850
version: this.context.extension?.packageJSON?.version ?? "",
18451851
apiConfiguration,
18461852
customInstructions,
1853+
keepBrowserOpen: keepBrowserOpen ?? false,
18471854
alwaysAllowReadOnly: alwaysAllowReadOnly ?? false,
18481855
alwaysAllowWrite: alwaysAllowWrite ?? false,
18491856
alwaysAllowExecute: alwaysAllowExecute ?? false,
@@ -2001,6 +2008,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
20012008
experimentalDiffStrategy,
20022009
autoApprovalEnabled,
20032010
customModes,
2011+
keepBrowserOpen,
20042012
] = await Promise.all([
20052013
this.getGlobalState("apiProvider") as Promise<ApiProvider | undefined>,
20062014
this.getGlobalState("apiModelId") as Promise<string | undefined>,
@@ -2070,6 +2078,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
20702078
this.getGlobalState("experimentalDiffStrategy") as Promise<boolean | undefined>,
20712079
this.getGlobalState("autoApprovalEnabled") as Promise<boolean | undefined>,
20722080
this.customModesManager.getCustomModes(),
2081+
this.getGlobalState("keepBrowserOpen") as Promise<boolean | undefined>,
20732082
])
20742083

20752084
let apiProvider: ApiProvider
@@ -2185,6 +2194,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
21852194
experimentalDiffStrategy: experimentalDiffStrategy ?? false,
21862195
autoApprovalEnabled: autoApprovalEnabled ?? false,
21872196
customModes,
2197+
keepBrowserOpen: keepBrowserOpen ?? false,
21882198
}
21892199
}
21902200

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,7 @@ describe("ClineProvider", () => {
320320
requestDelaySeconds: 5,
321321
mode: defaultModeSlug,
322322
customModes: [],
323+
keepBrowserOpen: false,
323324
}
324325

325326
const message: ExtensionMessage = {

src/services/browser/BrowserSession.ts

Lines changed: 65 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,39 @@ export class BrowserSession {
1919
private browser?: Browser
2020
private page?: Page
2121
private currentMousePosition?: string
22+
private sessionTimeout?: NodeJS.Timeout
23+
private readonly SESSION_TIMEOUT = 30 * 60 * 1000 // 30 minutes
2224

2325
constructor(context: vscode.ExtensionContext) {
2426
this.context = context
2527
}
2628

29+
private async getKeepBrowserOpen(): Promise<boolean> {
30+
return this.context.globalState.get<boolean>("keepBrowserOpen") ?? false
31+
}
32+
33+
private async resetSessionTimeout() {
34+
if (this.sessionTimeout) {
35+
clearTimeout(this.sessionTimeout)
36+
}
37+
38+
const keepBrowserOpen = await this.getKeepBrowserOpen()
39+
if (keepBrowserOpen) {
40+
this.sessionTimeout = setTimeout(() => {
41+
console.log("Browser session timed out after inactivity")
42+
this.browser?.close().catch(() => {})
43+
this.browser = undefined
44+
this.page = undefined
45+
this.currentMousePosition = undefined
46+
}, this.SESSION_TIMEOUT)
47+
} else {
48+
this.sessionTimeout = setTimeout(() => {
49+
console.log("Browser session timed out after inactivity")
50+
this.closeBrowser()
51+
}, this.SESSION_TIMEOUT)
52+
}
53+
}
54+
2755
private async ensureChromiumExists(): Promise<PCRStats> {
2856
const globalStoragePath = this.context?.globalStorageUri?.fsPath
2957
if (!globalStoragePath) {
@@ -47,46 +75,58 @@ export class BrowserSession {
4775

4876
async launchBrowser(): Promise<void> {
4977
console.log("launch browser called")
50-
if (this.browser) {
51-
// throw new Error("Browser already launched")
78+
if (this.browser || this.page) {
5279
await this.closeBrowser() // this may happen when the model launches a browser again after having used it already before
5380
}
5481

5582
const stats = await this.ensureChromiumExists()
56-
this.browser = await stats.puppeteer.launch({
57-
args: [
58-
"--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",
59-
],
60-
executablePath: stats.executablePath,
61-
defaultViewport: (() => {
62-
const size = (this.context.globalState.get("browserViewportSize") as string | undefined) || "900x600"
63-
const [width, height] = size.split("x").map(Number)
64-
return { width, height }
65-
})(),
66-
// headless: false,
67-
})
68-
// (latest version of puppeteer does not add headless to user agent)
69-
this.page = await this.browser?.newPage()
83+
if (!this.browser || !this.page) {
84+
this.browser = await stats.puppeteer.launch({
85+
args: [
86+
"--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",
87+
],
88+
executablePath: stats.executablePath,
89+
defaultViewport: (() => {
90+
const size =
91+
(this.context.globalState.get("browserViewportSize") as string | undefined) || "900x600"
92+
const [width, height] = size.split("x").map(Number)
93+
return { width, height }
94+
})(),
95+
// headless: false,
96+
})
97+
// (latest version of puppeteer does not add headless to user agent)
98+
this.page = await this.browser?.newPage()
99+
}
70100
}
71101

72102
async closeBrowser(): Promise<BrowserActionResult> {
73103
if (this.browser || this.page) {
74-
console.log("closing browser...")
75-
await this.browser?.close().catch(() => {})
76-
this.browser = undefined
77-
this.page = undefined
78-
this.currentMousePosition = undefined
104+
const keepBrowserOpen = await this.getKeepBrowserOpen()
105+
if (!keepBrowserOpen) {
106+
console.log("closing browser...")
107+
await this.browser?.close().catch(() => {})
108+
this.browser = undefined
109+
this.page = undefined
110+
this.currentMousePosition = undefined
111+
} else {
112+
console.log("keeping browser open...")
113+
}
79114
}
80115
return {}
81116
}
82117

83118
async doAction(action: (page: Page) => Promise<void>): Promise<BrowserActionResult> {
84-
if (!this.page) {
85-
throw new Error(
86-
"Browser is not launched. This may occur if the browser was automatically closed by a non-`browser_action` tool.",
87-
)
119+
if (!this.page || !this.browser) {
120+
console.log("No browser/page found, launching new one")
121+
await this.launchBrowser()
122+
if (!this.page) {
123+
throw new Error("Failed to launch browser")
124+
}
88125
}
89126

127+
// Reset timeout on each action
128+
await this.resetSessionTimeout()
129+
90130
const logs: string[] = []
91131
let lastLogTs = Date.now()
92132

src/shared/ExtensionMessage.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export interface ExtensionMessage {
4141
| "autoApprovalEnabled"
4242
| "updateCustomMode"
4343
| "deleteCustomMode"
44+
| "keepBrowserOpen"
4445
text?: string
4546
action?:
4647
| "chatButtonClicked"
@@ -51,6 +52,7 @@ export interface ExtensionMessage {
5152
| "didBecomeVisible"
5253
invoke?: "sendMessage" | "primaryButtonClick" | "secondaryButtonClick"
5354
state?: ExtensionState
55+
bool?: boolean // Used for boolean settings like keepBrowserOpen
5456
images?: string[]
5557
ollamaModels?: string[]
5658
lmStudioModels?: string[]
@@ -111,6 +113,7 @@ export interface ExtensionState {
111113
autoApprovalEnabled?: boolean
112114
customModes: ModeConfig[]
113115
toolRequirements?: Record<string, boolean> // Map of tool names to their requirements (e.g. {"apply_diff": true} if diffEnabled)
116+
keepBrowserOpen: boolean // Controls whether browser sessions persist between actions
114117
}
115118

116119
export interface ClineMessage {

src/shared/WebviewMessage.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ export interface WebviewMessage {
8080
| "deleteCustomMode"
8181
| "setopenAiCustomModelInfo"
8282
| "openCustomModesSettings"
83+
| "keepBrowserOpen"
8384
text?: string
8485
disabled?: boolean
8586
askResponse?: ClineAskResponse

webview-ui/src/components/settings/SettingsView.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
5353
listApiConfigMeta,
5454
experimentalDiffStrategy,
5555
setExperimentalDiffStrategy,
56+
keepBrowserOpen,
57+
setKeepBrowserOpen,
5658
} = useExtensionState()
5759
const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
5860
const [modelIdErrorMessage, setModelIdErrorMessage] = useState<string | undefined>(undefined)
@@ -93,6 +95,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
9395
apiConfiguration,
9496
})
9597
vscode.postMessage({ type: "experimentalDiffStrategy", bool: experimentalDiffStrategy })
98+
vscode.postMessage({ type: "keepBrowserOpen", bool: keepBrowserOpen })
9699
onDone()
97100
}
98101
}
@@ -428,6 +431,17 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
428431

429432
<div style={{ marginBottom: 40 }}>
430433
<h3 style={{ color: "var(--vscode-foreground)", margin: "0 0 15px 0" }}>Browser Settings</h3>
434+
<div style={{ marginBottom: 15 }}>
435+
<VSCodeCheckbox
436+
checked={keepBrowserOpen}
437+
onChange={(e: any) => setKeepBrowserOpen(e.target.checked)}>
438+
<span style={{ fontWeight: "500" }}>Keep browser open between actions</span>
439+
</VSCodeCheckbox>
440+
<p style={{ fontSize: "12px", marginTop: "5px", color: "var(--vscode-descriptionForeground)" }}>
441+
When enabled, the browser will stay open between actions during web development tasks,
442+
improving performance. The browser will still close after 30 minutes of inactivity.
443+
</p>
444+
</div>
431445
<div style={{ marginBottom: 15 }}>
432446
<label style={{ fontWeight: "500", display: "block", marginBottom: 5 }}>Viewport size</label>
433447
<select

webview-ui/src/context/ExtensionStateContext.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ export interface ExtensionStateContextType extends ExtensionState {
6969
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
7070
customModes: ModeConfig[]
7171
setCustomModes: (value: ModeConfig[]) => void
72+
keepBrowserOpen: boolean
73+
setKeepBrowserOpen: (value: boolean) => void
7274
}
7375

7476
export const ExtensionStateContext = createContext<ExtensionStateContextType | undefined>(undefined)
@@ -101,6 +103,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
101103
experimentalDiffStrategy: false,
102104
autoApprovalEnabled: false,
103105
customModes: [],
106+
keepBrowserOpen: false,
104107
})
105108

106109
const [didHydrateState, setDidHydrateState] = useState(false)
@@ -282,6 +285,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
282285
setAutoApprovalEnabled: (value) => setState((prevState) => ({ ...prevState, autoApprovalEnabled: value })),
283286
handleInputChange,
284287
setCustomModes: (value) => setState((prevState) => ({ ...prevState, customModes: value })),
288+
setKeepBrowserOpen: (value) => setState((prevState) => ({ ...prevState, keepBrowserOpen: value })),
285289
}
286290

287291
return <ExtensionStateContext.Provider value={contextValue}>{children}</ExtensionStateContext.Provider>

0 commit comments

Comments
 (0)