browser-use 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/dist/agent/service.js +2 -0
  2. package/dist/agent/system_prompt.md +269 -0
  3. package/dist/agent/system_prompt_anthropic_flash.md +240 -0
  4. package/dist/agent/system_prompt_browser_use.md +18 -0
  5. package/dist/agent/system_prompt_browser_use_flash.md +15 -0
  6. package/dist/agent/system_prompt_browser_use_no_thinking.md +17 -0
  7. package/dist/agent/system_prompt_flash.md +16 -0
  8. package/dist/agent/system_prompt_flash_anthropic.md +30 -0
  9. package/dist/agent/system_prompt_no_thinking.md +245 -0
  10. package/dist/browser/cloud/index.d.ts +1 -0
  11. package/dist/browser/cloud/index.js +1 -0
  12. package/dist/browser/cloud/management.d.ts +130 -0
  13. package/dist/browser/cloud/management.js +140 -0
  14. package/dist/browser/events.d.ts +61 -3
  15. package/dist/browser/events.js +66 -0
  16. package/dist/browser/profile.d.ts +1 -0
  17. package/dist/browser/profile.js +25 -8
  18. package/dist/browser/session.d.ts +59 -2
  19. package/dist/browser/session.js +943 -131
  20. package/dist/browser/watchdogs/base.js +34 -1
  21. package/dist/browser/watchdogs/captcha-watchdog.d.ts +26 -0
  22. package/dist/browser/watchdogs/captcha-watchdog.js +151 -0
  23. package/dist/browser/watchdogs/index.d.ts +1 -0
  24. package/dist/browser/watchdogs/index.js +1 -0
  25. package/dist/browser/watchdogs/screenshot-watchdog.js +4 -3
  26. package/dist/cli.d.ts +120 -0
  27. package/dist/cli.js +1816 -4
  28. package/dist/controller/service.js +106 -362
  29. package/dist/controller/views.d.ts +9 -6
  30. package/dist/controller/views.js +8 -5
  31. package/dist/dom/dom_tree/index.js +24 -11
  32. package/dist/filesystem/file-system.js +1 -1
  33. package/dist/llm/litellm/chat.d.ts +11 -0
  34. package/dist/llm/litellm/chat.js +16 -0
  35. package/dist/llm/litellm/index.d.ts +1 -0
  36. package/dist/llm/litellm/index.js +1 -0
  37. package/dist/llm/models.js +29 -3
  38. package/dist/llm/oci-raw/chat.d.ts +64 -0
  39. package/dist/llm/oci-raw/chat.js +350 -0
  40. package/dist/llm/oci-raw/index.d.ts +2 -0
  41. package/dist/llm/oci-raw/index.js +2 -0
  42. package/dist/llm/oci-raw/serializer.d.ts +12 -0
  43. package/dist/llm/oci-raw/serializer.js +128 -0
  44. package/dist/mcp/server.d.ts +1 -0
  45. package/dist/mcp/server.js +62 -13
  46. package/dist/skill-cli/direct.d.ts +100 -0
  47. package/dist/skill-cli/direct.js +984 -0
  48. package/dist/skill-cli/index.d.ts +2 -0
  49. package/dist/skill-cli/index.js +2 -0
  50. package/dist/skill-cli/server.d.ts +2 -0
  51. package/dist/skill-cli/server.js +472 -11
  52. package/dist/skill-cli/tunnel.d.ts +61 -0
  53. package/dist/skill-cli/tunnel.js +257 -0
  54. package/dist/sync/auth.d.ts +8 -0
  55. package/dist/sync/auth.js +12 -0
  56. package/dist/utils.d.ts +1 -1
  57. package/dist/utils.js +2 -1
  58. package/package.json +22 -4
@@ -2,7 +2,7 @@ import fs from 'node:fs';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
4
  import { isIP } from 'node:net';
5
- import { execFile } from 'node:child_process';
5
+ import { execFile, execFileSync, } from 'node:child_process';
6
6
  import { promisify } from 'node:util';
7
7
  import { createLogger } from '../logging-config.js';
8
8
  import { match_url_with_domain_pattern, uuid7str } from '../utils.js';
@@ -10,13 +10,14 @@ import { EventBus, } from '../event-bus.js';
10
10
  import { async_playwright, } from './types.js';
11
11
  import { BrowserProfile, CHROME_DOCKER_ARGS, DEFAULT_BROWSER_PROFILE, } from './profile.js';
12
12
  import { BrowserStateSummary, BrowserError, URLNotAllowedError, } from './views.js';
13
- import { AgentFocusChangedEvent, BrowserConnectedEvent, BrowserLaunchEvent, BrowserStartEvent, BrowserStoppedEvent, BrowserStopEvent, DialogOpenedEvent, DownloadProgressEvent, DownloadStartedEvent, FileDownloadedEvent, TabClosedEvent, TabCreatedEvent, } from './events.js';
13
+ import { AgentFocusChangedEvent, BrowserConnectedEvent, BrowserErrorEvent, BrowserLaunchEvent, BrowserReconnectedEvent, BrowserReconnectingEvent, BrowserStartEvent, BrowserStoppedEvent, BrowserStopEvent, DialogOpenedEvent, DownloadProgressEvent, DownloadStartedEvent, FileDownloadedEvent, TabClosedEvent, TabCreatedEvent, } from './events.js';
14
14
  import { DOMElementNode, DOMState } from '../dom/views.js';
15
15
  import { normalize_url } from './utils.js';
16
16
  import { DomService } from '../dom/service.js';
17
17
  import { showDVDScreensaver, showSpinner, withDVDScreensaver, } from './dvd-screensaver.js';
18
18
  import { SessionManager } from './session-manager.js';
19
19
  import { AboutBlankWatchdog } from './watchdogs/aboutblank-watchdog.js';
20
+ import { CaptchaWatchdog, } from './watchdogs/captcha-watchdog.js';
20
21
  import { CDPSessionWatchdog } from './watchdogs/cdp-session-watchdog.js';
21
22
  import { CrashWatchdog } from './watchdogs/crash-watchdog.js';
22
23
  import { DefaultActionWatchdog } from './watchdogs/default-action-watchdog.js';
@@ -31,6 +32,147 @@ import { ScreenshotWatchdog } from './watchdogs/screenshot-watchdog.js';
31
32
  import { SecurityWatchdog } from './watchdogs/security-watchdog.js';
32
33
  import { StorageStateWatchdog } from './watchdogs/storage-state-watchdog.js';
33
34
  const execFileAsync = promisify(execFile);
35
+ const PLAYWRIGHT_OPTION_KEY_OVERRIDES = {
36
+ extra_http_headers: 'extraHTTPHeaders',
37
+ };
38
+ const EMPTY_DOM_RETRY_DELAY_MS = 250;
39
+ const REMOTE_RECONNECT_DELAYS_MS = [1000, 2000, 4000];
40
+ const REMOTE_RECONNECT_ATTEMPT_TIMEOUT_MS = 15_000;
41
+ const cloneBrowserProfileConfig = (profile) => typeof structuredClone === 'function'
42
+ ? structuredClone(profile.config)
43
+ : JSON.parse(JSON.stringify(profile.config));
44
+ const detectSystemChromeVariant = (executablePath) => {
45
+ const normalizedPath = String(executablePath ?? '')
46
+ .trim()
47
+ .toLowerCase();
48
+ if (!normalizedPath) {
49
+ return 'chrome';
50
+ }
51
+ if (normalizedPath.includes('chromium')) {
52
+ return 'chromium';
53
+ }
54
+ if (normalizedPath.includes('chrome canary') ||
55
+ normalizedPath.includes('chrome sxs')) {
56
+ return 'chrome-canary';
57
+ }
58
+ if (normalizedPath.includes('google-chrome-beta')) {
59
+ return 'chrome-beta';
60
+ }
61
+ if (normalizedPath.includes('google-chrome-unstable')) {
62
+ return 'chrome-unstable';
63
+ }
64
+ return 'chrome';
65
+ };
66
+ export const systemChrome = {
67
+ findExecutable() {
68
+ if (process.platform === 'darwin') {
69
+ const candidates = [
70
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
71
+ '/Applications/Chromium.app/Contents/MacOS/Chromium',
72
+ '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
73
+ ];
74
+ return candidates.find((candidate) => fs.existsSync(candidate)) ?? null;
75
+ }
76
+ if (process.platform === 'linux') {
77
+ const commands = [
78
+ 'google-chrome',
79
+ 'google-chrome-stable',
80
+ 'google-chrome-beta',
81
+ 'google-chrome-unstable',
82
+ 'chromium',
83
+ 'chromium-browser',
84
+ ];
85
+ for (const command of commands) {
86
+ try {
87
+ const resolved = execFileSync('which', [command], {
88
+ encoding: 'utf8',
89
+ stdio: ['ignore', 'pipe', 'ignore'],
90
+ }).trim();
91
+ if (resolved) {
92
+ return resolved;
93
+ }
94
+ }
95
+ catch {
96
+ // Ignore missing commands and try the next candidate.
97
+ }
98
+ }
99
+ return null;
100
+ }
101
+ if (process.platform === 'win32') {
102
+ const candidates = [
103
+ path.join(process.env.ProgramFiles ?? 'C:\\Program Files', 'Google', 'Chrome', 'Application', 'chrome.exe'),
104
+ path.join(process.env['ProgramFiles(x86)'] ?? 'C:\\Program Files (x86)', 'Google', 'Chrome', 'Application', 'chrome.exe'),
105
+ path.join(process.env.LOCALAPPDATA ?? '', 'Google', 'Chrome', 'Application', 'chrome.exe'),
106
+ path.join(process.env.LOCALAPPDATA ?? '', 'Google', 'Chrome SxS', 'Application', 'chrome.exe'),
107
+ path.join(process.env.LOCALAPPDATA ?? '', 'Chromium', 'Application', 'chrome.exe'),
108
+ path.join(process.env.ProgramFiles ?? 'C:\\Program Files', 'Chromium', 'Application', 'chrome.exe'),
109
+ path.join(process.env['ProgramFiles(x86)'] ?? 'C:\\Program Files (x86)', 'Chromium', 'Application', 'chrome.exe'),
110
+ ];
111
+ return candidates.find((candidate) => fs.existsSync(candidate)) ?? null;
112
+ }
113
+ return null;
114
+ },
115
+ getUserDataDir(executablePath = systemChrome.findExecutable()) {
116
+ const variant = detectSystemChromeVariant(executablePath);
117
+ if (process.platform === 'darwin') {
118
+ const applicationSupportDir = path.join(os.homedir(), 'Library', 'Application Support');
119
+ if (variant === 'chromium') {
120
+ return path.join(applicationSupportDir, 'Chromium');
121
+ }
122
+ if (variant === 'chrome-canary') {
123
+ return path.join(applicationSupportDir, 'Google', 'Chrome Canary');
124
+ }
125
+ return path.join(applicationSupportDir, 'Google', 'Chrome');
126
+ }
127
+ if (process.platform === 'linux') {
128
+ if (variant === 'chromium') {
129
+ return path.join(os.homedir(), '.config', 'chromium');
130
+ }
131
+ if (variant === 'chrome-beta') {
132
+ return path.join(os.homedir(), '.config', 'google-chrome-beta');
133
+ }
134
+ if (variant === 'chrome-unstable') {
135
+ return path.join(os.homedir(), '.config', 'google-chrome-unstable');
136
+ }
137
+ return path.join(os.homedir(), '.config', 'google-chrome');
138
+ }
139
+ if (process.platform === 'win32') {
140
+ const localAppData = process.env.LOCALAPPDATA ?? path.join(os.homedir(), 'AppData', 'Local');
141
+ if (variant === 'chromium') {
142
+ return path.join(localAppData, 'Chromium', 'User Data');
143
+ }
144
+ if (variant === 'chrome-canary') {
145
+ return path.join(localAppData, 'Google', 'Chrome SxS', 'User Data');
146
+ }
147
+ return path.join(localAppData, 'Google', 'Chrome', 'User Data');
148
+ }
149
+ return null;
150
+ },
151
+ listProfiles(userDataDir = systemChrome.getUserDataDir()) {
152
+ if (!userDataDir) {
153
+ return [];
154
+ }
155
+ const localStatePath = path.join(userDataDir, 'Local State');
156
+ if (!fs.existsSync(localStatePath)) {
157
+ return [];
158
+ }
159
+ try {
160
+ const raw = fs.readFileSync(localStatePath, 'utf8');
161
+ const localState = JSON.parse(raw);
162
+ const infoCache = localState.profile?.info_cache ?? {};
163
+ return Object.entries(infoCache)
164
+ .map(([directory, info]) => ({
165
+ directory,
166
+ name: info?.name || directory,
167
+ email: info?.user_name || '',
168
+ }))
169
+ .sort((left, right) => left.directory.localeCompare(right.directory));
170
+ }
171
+ catch {
172
+ return [];
173
+ }
174
+ },
175
+ };
34
176
  const createEmptyDomState = () => {
35
177
  const root = new DOMElementNode(true, null, 'html', '/html[1]', {}, []);
36
178
  return new DOMState(root, {});
@@ -77,11 +219,18 @@ export class BrowserSession {
77
219
  _maxRecentEvents = 100;
78
220
  _watchdogs = new Set();
79
221
  _defaultWatchdogsAttached = false;
222
+ _captchaWatchdog = null;
223
+ RECONNECT_WAIT_TIMEOUT = 54;
224
+ _reconnecting = false;
225
+ _reconnectTask = null;
226
+ _reconnectWaitPromise = Promise.resolve();
227
+ _resolveReconnectWait = null;
228
+ _intentionalStop = false;
229
+ _disconnectAwareBrowser = null;
230
+ _browserDisconnectHandler = null;
80
231
  constructor(init = {}) {
81
232
  const sourceProfileConfig = init.browser_profile
82
- ? typeof structuredClone === 'function'
83
- ? structuredClone(init.browser_profile.config)
84
- : JSON.parse(JSON.stringify(init.browser_profile.config))
233
+ ? cloneBrowserProfileConfig(init.browser_profile)
85
234
  : (init.profile ?? {});
86
235
  this.browser_profile = new BrowserProfile(sourceProfileConfig);
87
236
  this.id = init.id ?? uuid7str();
@@ -121,12 +270,56 @@ export class BrowserSession {
121
270
  this._attachDialogHandler(this.agent_current_page);
122
271
  this._recordRecentEvent('session_initialized', { url: this.currentUrl });
123
272
  }
273
+ static from_system_chrome(init = {}) {
274
+ const executablePath = systemChrome.findExecutable();
275
+ if (!executablePath) {
276
+ throw new Error('Chrome not found. Please install Chrome or use BrowserSession with an explicit executable_path.\n' +
277
+ 'Expected locations:\n' +
278
+ ' macOS: /Applications/Google Chrome.app/Contents/MacOS/Google Chrome\n' +
279
+ ' Linux: /usr/bin/google-chrome or /usr/bin/chromium\n' +
280
+ ' Windows: C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe');
281
+ }
282
+ const userDataDir = systemChrome.getUserDataDir(executablePath);
283
+ if (!userDataDir) {
284
+ throw new Error('Could not detect Chrome profile directory for your platform.\n' +
285
+ 'Expected locations:\n' +
286
+ ' macOS: ~/Library/Application Support/Google/Chrome\n' +
287
+ ' Linux: ~/.config/google-chrome\n' +
288
+ ' Windows: %LocalAppData%\\Google\\Chrome\\User Data');
289
+ }
290
+ const availableProfiles = systemChrome.listProfiles(userDataDir);
291
+ const selectedProfileDirectory = init.profile_directory ?? availableProfiles[0]?.directory ?? 'Default';
292
+ if (typeof init.profile_directory === 'undefined' && availableProfiles[0]) {
293
+ createLogger('browser_use.browser.session').info(`Auto-selected Chrome profile: ${availableProfiles[0].name} (${availableProfiles[0].directory})`);
294
+ }
295
+ const sourceProfileConfig = init.browser_profile
296
+ ? cloneBrowserProfileConfig(init.browser_profile)
297
+ : (init.profile ?? {});
298
+ const { browser_profile: _browserProfile, profile: _profile, profile_directory: _profileDirectory, ...sessionInit } = init;
299
+ return new BrowserSession({
300
+ ...sessionInit,
301
+ browser_profile: new BrowserProfile({
302
+ ...sourceProfileConfig,
303
+ executable_path: executablePath,
304
+ user_data_dir: userDataDir,
305
+ profile_directory: selectedProfileDirectory,
306
+ }),
307
+ });
308
+ }
309
+ static list_chrome_profiles() {
310
+ const executablePath = systemChrome.findExecutable();
311
+ const userDataDir = systemChrome.getUserDataDir(executablePath);
312
+ return systemChrome.listProfiles(userDataDir);
313
+ }
124
314
  attach_watchdog(watchdog) {
125
315
  if (this._watchdogs.has(watchdog)) {
126
316
  return;
127
317
  }
128
318
  watchdog.attach_to_session();
129
319
  this._watchdogs.add(watchdog);
320
+ if (watchdog instanceof CaptchaWatchdog) {
321
+ this._captchaWatchdog = watchdog;
322
+ }
130
323
  }
131
324
  attach_watchdogs(watchdogs) {
132
325
  for (const watchdog of watchdogs) {
@@ -139,12 +332,16 @@ export class BrowserSession {
139
332
  }
140
333
  watchdog.detach_from_session();
141
334
  this._watchdogs.delete(watchdog);
335
+ if (watchdog === this._captchaWatchdog) {
336
+ this._captchaWatchdog = null;
337
+ }
142
338
  }
143
339
  detach_all_watchdogs() {
144
340
  for (const watchdog of [...this._watchdogs]) {
145
341
  this.detach_watchdog(watchdog);
146
342
  }
147
343
  this._defaultWatchdogsAttached = false;
344
+ this._captchaWatchdog = null;
148
345
  }
149
346
  get_watchdogs() {
150
347
  return [...this._watchdogs];
@@ -179,6 +376,10 @@ export class BrowserSession {
179
376
  new StorageStateWatchdog({ browser_session: this }),
180
377
  new DefaultActionWatchdog({ browser_session: this }),
181
378
  ];
379
+ if (this.browser_profile.config.captcha_solver) {
380
+ this._captchaWatchdog = new CaptchaWatchdog({ browser_session: this });
381
+ watchdogs.push(this._captchaWatchdog);
382
+ }
182
383
  const configuredHarPath = this.browser_profile.config.record_har_path;
183
384
  if (typeof configuredHarPath === 'string' &&
184
385
  configuredHarPath.trim().length > 0) {
@@ -187,6 +388,9 @@ export class BrowserSession {
187
388
  this.attach_watchdogs(watchdogs);
188
389
  this._defaultWatchdogsAttached = true;
189
390
  }
391
+ async wait_if_captcha_solving(timeoutSeconds) {
392
+ return (this._captchaWatchdog?.wait_if_captcha_solving(timeoutSeconds) ?? null);
393
+ }
190
394
  _formatTabId(pageId) {
191
395
  const normalized = Number.isFinite(pageId) && pageId >= 0 ? Math.floor(pageId) : 0;
192
396
  return String(normalized).padStart(4, '0').slice(-4);
@@ -400,6 +604,118 @@ export class BrowserSession {
400
604
  this._attachDialogHandler(page);
401
605
  this.agent_current_page = page ?? null;
402
606
  }
607
+ async _syncCurrentTabFromPage(page) {
608
+ if (!page) {
609
+ return;
610
+ }
611
+ let resolvedUrl = null;
612
+ try {
613
+ const rawUrl = page.url();
614
+ if (typeof rawUrl === 'string' && rawUrl.trim()) {
615
+ resolvedUrl = normalize_url(rawUrl);
616
+ this.currentUrl = resolvedUrl;
617
+ }
618
+ }
619
+ catch {
620
+ // Ignore transient URL read failures.
621
+ }
622
+ let resolvedTitle = null;
623
+ if (typeof page.title === 'function') {
624
+ try {
625
+ const title = await page.title();
626
+ if (typeof title === 'string' && title.trim()) {
627
+ resolvedTitle = title;
628
+ }
629
+ }
630
+ catch {
631
+ // Ignore transient title read failures.
632
+ }
633
+ }
634
+ if (!resolvedTitle) {
635
+ resolvedTitle = resolvedUrl ?? this.currentTitle ?? this.currentUrl;
636
+ }
637
+ this.currentTitle = resolvedTitle;
638
+ const currentTab = this._tabs[this.currentTabIndex];
639
+ if (currentTab) {
640
+ if (resolvedUrl) {
641
+ currentTab.url = resolvedUrl;
642
+ }
643
+ currentTab.title = resolvedTitle;
644
+ this._syncSessionManagerFromTabs();
645
+ }
646
+ }
647
+ _syncTabsWithBrowserPages() {
648
+ const pages = this.browser_context?.pages?.() ?? [];
649
+ if (!pages.length) {
650
+ return;
651
+ }
652
+ const nextTabs = [];
653
+ const nextTabPages = new Map();
654
+ const usedPageIds = new Set();
655
+ const knownPageMappings = Array.from(this.tabPages.entries());
656
+ for (const page of pages) {
657
+ this._attachDialogHandler(page ?? null);
658
+ let pageId = null;
659
+ for (const [candidateId, candidatePage] of knownPageMappings) {
660
+ if (candidatePage === page && !usedPageIds.has(candidateId)) {
661
+ pageId = candidateId;
662
+ break;
663
+ }
664
+ }
665
+ if (pageId === null) {
666
+ pageId = this._tabCounter++;
667
+ }
668
+ usedPageIds.add(pageId);
669
+ const existingTab = this._tabs.find((tab) => tab.page_id === pageId);
670
+ const tab = existingTab
671
+ ? { ...existingTab }
672
+ : this._createTabInfo({
673
+ page_id: pageId,
674
+ url: 'about:blank',
675
+ title: 'about:blank',
676
+ });
677
+ try {
678
+ const rawUrl = page.url();
679
+ if (typeof rawUrl === 'string' && rawUrl.trim()) {
680
+ tab.url = normalize_url(rawUrl);
681
+ }
682
+ }
683
+ catch {
684
+ // Keep existing tab url when page url is not readable.
685
+ }
686
+ if (!existingTab || !tab.title || tab.title === 'about:blank') {
687
+ tab.title = tab.url;
688
+ }
689
+ nextTabs.push(tab);
690
+ nextTabPages.set(pageId, page);
691
+ }
692
+ if (!nextTabs.length) {
693
+ return;
694
+ }
695
+ this._tabs = nextTabs;
696
+ this.tabPages = nextTabPages;
697
+ const activePage = this.agent_current_page && pages.includes(this.agent_current_page)
698
+ ? this.agent_current_page
699
+ : (pages[0] ?? null);
700
+ if (activePage) {
701
+ const activeIndex = this._tabs.findIndex((tab) => this.tabPages.get(tab.page_id) === activePage);
702
+ if (activeIndex !== -1) {
703
+ this.currentTabIndex = activeIndex;
704
+ }
705
+ }
706
+ if (this.currentTabIndex < 0 || this.currentTabIndex >= this._tabs.length) {
707
+ this.currentTabIndex = Math.max(0, this._tabs.length - 1);
708
+ }
709
+ const activeTab = this._tabs[this.currentTabIndex] ?? null;
710
+ if (activeTab) {
711
+ this.currentUrl = activeTab.url;
712
+ this.currentTitle = activeTab.title || activeTab.url;
713
+ this.agent_current_page = this.tabPages.get(activeTab.page_id) ?? null;
714
+ this.human_current_page =
715
+ this.human_current_page ?? this.agent_current_page;
716
+ }
717
+ this._syncSessionManagerFromTabs();
718
+ }
403
719
  _captureClosedPopupMessage(dialogType, message) {
404
720
  const normalizedType = String(dialogType || 'alert').trim() || 'alert';
405
721
  const normalizedMessage = String(message || '').trim();
@@ -600,6 +916,65 @@ export class BrowserSession {
600
916
  get is_stopping() {
601
917
  return this._stoppingPromise !== null;
602
918
  }
919
+ get is_reconnecting() {
920
+ return this._reconnecting;
921
+ }
922
+ get should_gate_watchdog_events() {
923
+ return Boolean(this.initialized ||
924
+ this.browser ||
925
+ this.browser_context ||
926
+ this.cdp_url ||
927
+ this.wss_url ||
928
+ this._reconnecting);
929
+ }
930
+ get is_cdp_connected() {
931
+ try {
932
+ if (this.browser) {
933
+ const browser = this.browser;
934
+ if (typeof browser.isConnected === 'function' &&
935
+ !browser.isConnected()) {
936
+ return false;
937
+ }
938
+ }
939
+ if (this.browser_context) {
940
+ const contextBrowser = this.browser_context.browser?.();
941
+ if (contextBrowser &&
942
+ typeof contextBrowser.isConnected === 'function' &&
943
+ !contextBrowser.isConnected()) {
944
+ return false;
945
+ }
946
+ return true;
947
+ }
948
+ return Boolean(this.browser);
949
+ }
950
+ catch {
951
+ return false;
952
+ }
953
+ }
954
+ async wait_for_reconnect(timeoutSeconds = this.RECONNECT_WAIT_TIMEOUT) {
955
+ if (!this._reconnecting) {
956
+ return;
957
+ }
958
+ const timeoutMs = Number.isFinite(timeoutSeconds) && timeoutSeconds > 0
959
+ ? timeoutSeconds * 1000
960
+ : this.RECONNECT_WAIT_TIMEOUT * 1000;
961
+ let timeoutHandle = null;
962
+ try {
963
+ await Promise.race([
964
+ this._reconnectWaitPromise,
965
+ new Promise((_, reject) => {
966
+ timeoutHandle = setTimeout(() => {
967
+ reject(new Error(`Reconnection wait timed out after ${Math.round(timeoutMs / 1000)}s`));
968
+ }, timeoutMs);
969
+ }),
970
+ ]);
971
+ }
972
+ finally {
973
+ if (timeoutHandle) {
974
+ clearTimeout(timeoutHandle);
975
+ }
976
+ }
977
+ }
603
978
  claim_agent(agentId, mode = 'exclusive') {
604
979
  if (!agentId) {
605
980
  return false;
@@ -785,11 +1160,276 @@ export class BrowserSession {
785
1160
  if (convertedValue === undefined) {
786
1161
  continue;
787
1162
  }
788
- const normalizedKey = rawKey.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
1163
+ const normalizedKey = PLAYWRIGHT_OPTION_KEY_OVERRIDES[rawKey] ??
1164
+ rawKey.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
789
1165
  result[normalizedKey] = convertedValue;
790
1166
  }
791
1167
  return result;
792
1168
  }
1169
+ async set_extra_headers(headers) {
1170
+ const normalizedHeaders = Object.fromEntries(Object.entries(headers)
1171
+ .map(([key, value]) => [String(key).trim(), String(value)])
1172
+ .filter(([key]) => key.length > 0));
1173
+ if (!this.browser_context ||
1174
+ Object.keys(normalizedHeaders).length === 0 ||
1175
+ typeof this.browser_context.setExtraHTTPHeaders !== 'function') {
1176
+ return;
1177
+ }
1178
+ await this.browser_context.setExtraHTTPHeaders(normalizedHeaders);
1179
+ }
1180
+ async _applyConfiguredExtraHttpHeaders() {
1181
+ const configuredHeaders = this.browser_profile.config.extra_http_headers;
1182
+ if (!configuredHeaders || Object.keys(configuredHeaders).length === 0) {
1183
+ return;
1184
+ }
1185
+ await this.set_extra_headers(configuredHeaders);
1186
+ }
1187
+ _usesRemoteBrowserConnection() {
1188
+ return Boolean(this.cdp_url || this.wss_url);
1189
+ }
1190
+ async _connectToConfiguredBrowser(playwright) {
1191
+ const connectOptions = this._toPlaywrightOptions(this.browser_profile.kwargs_for_connect());
1192
+ if (this.cdp_url) {
1193
+ return await playwright.chromium.connectOverCDP(this.cdp_url, connectOptions ?? {});
1194
+ }
1195
+ if (this.wss_url) {
1196
+ return await playwright.chromium.connect(this.wss_url, connectOptions ?? {});
1197
+ }
1198
+ throw new Error('Cannot connect to a remote browser without cdp_url or wss_url');
1199
+ }
1200
+ async _ensureBrowserContextFromBrowser(browser) {
1201
+ const existingContexts = (typeof browser?.contexts === 'function' ? browser.contexts() : []) ?? [];
1202
+ if (existingContexts.length > 0) {
1203
+ return existingContexts[0] ?? null;
1204
+ }
1205
+ if (typeof browser?.newContext === 'function') {
1206
+ const contextOptions = this._toPlaywrightOptions(this.browser_profile.kwargs_for_new_context());
1207
+ return await browser.newContext(contextOptions ?? {});
1208
+ }
1209
+ return null;
1210
+ }
1211
+ _beginReconnectWait() {
1212
+ this._reconnectWaitPromise = new Promise((resolve) => {
1213
+ this._resolveReconnectWait = resolve;
1214
+ });
1215
+ }
1216
+ _endReconnectWait() {
1217
+ this._resolveReconnectWait?.();
1218
+ this._resolveReconnectWait = null;
1219
+ this._reconnectWaitPromise = Promise.resolve();
1220
+ }
1221
+ _detachRemoteDisconnectHandler() {
1222
+ if (!this._disconnectAwareBrowser || !this._browserDisconnectHandler) {
1223
+ this._disconnectAwareBrowser = null;
1224
+ this._browserDisconnectHandler = null;
1225
+ return;
1226
+ }
1227
+ if (typeof this._disconnectAwareBrowser.off === 'function') {
1228
+ this._disconnectAwareBrowser.off('disconnected', this._browserDisconnectHandler);
1229
+ }
1230
+ else if (typeof this._disconnectAwareBrowser.removeListener === 'function') {
1231
+ this._disconnectAwareBrowser.removeListener('disconnected', this._browserDisconnectHandler);
1232
+ }
1233
+ this._disconnectAwareBrowser = null;
1234
+ this._browserDisconnectHandler = null;
1235
+ }
1236
+ _attachRemoteDisconnectHandler(browser) {
1237
+ this._detachRemoteDisconnectHandler();
1238
+ if (!this._usesRemoteBrowserConnection()) {
1239
+ return;
1240
+ }
1241
+ const browserWithEvents = browser;
1242
+ if (!browserWithEvents || typeof browserWithEvents.on !== 'function') {
1243
+ return;
1244
+ }
1245
+ const onDisconnected = () => {
1246
+ this._handleUnexpectedRemoteDisconnect();
1247
+ };
1248
+ browserWithEvents.on('disconnected', onDisconnected);
1249
+ this._disconnectAwareBrowser = browserWithEvents;
1250
+ this._browserDisconnectHandler = onDisconnected;
1251
+ }
1252
+ _handleUnexpectedRemoteDisconnect() {
1253
+ if (this._intentionalStop ||
1254
+ this._reconnecting ||
1255
+ !this._usesRemoteBrowserConnection()) {
1256
+ return;
1257
+ }
1258
+ this.logger.warning('Remote browser connection closed unexpectedly; attempting to reconnect');
1259
+ this._recordRecentEvent('browser_disconnected', {
1260
+ url: this.currentUrl,
1261
+ });
1262
+ const reconnectTask = this._auto_reconnect();
1263
+ this._reconnectTask = reconnectTask;
1264
+ void reconnectTask.finally(() => {
1265
+ if (this._reconnectTask === reconnectTask) {
1266
+ this._reconnectTask = null;
1267
+ }
1268
+ });
1269
+ }
1270
+ async _restorePagesAfterReconnect(preferredUrl, preferredTabIndex) {
1271
+ if (!this.browser_context) {
1272
+ this.agent_current_page = null;
1273
+ this.human_current_page = null;
1274
+ return;
1275
+ }
1276
+ let pages = this.browser_context.pages?.() ?? [];
1277
+ if (!pages.length && typeof this.browser_context.newPage === 'function') {
1278
+ const createdPage = await this.browser_context.newPage();
1279
+ if (createdPage) {
1280
+ pages = this.browser_context.pages?.() ?? [createdPage];
1281
+ }
1282
+ }
1283
+ this.tabPages = new Map();
1284
+ this.agent_current_page = null;
1285
+ this.human_current_page = null;
1286
+ this._syncTabsWithBrowserPages();
1287
+ if (!pages.length) {
1288
+ this.currentTabIndex = 0;
1289
+ this.currentUrl = normalize_url(preferredUrl ?? 'about:blank');
1290
+ this.currentTitle = this.currentUrl;
1291
+ if (!this._tabs.length) {
1292
+ this._tabs = [
1293
+ this._createTabInfo({
1294
+ page_id: this._tabCounter++,
1295
+ url: this.currentUrl,
1296
+ title: this.currentTitle,
1297
+ }),
1298
+ ];
1299
+ }
1300
+ this._syncSessionManagerFromTabs();
1301
+ return;
1302
+ }
1303
+ const normalizedPreferredUrl = typeof preferredUrl === 'string' && preferredUrl.trim().length > 0
1304
+ ? normalize_url(preferredUrl)
1305
+ : null;
1306
+ const pageByUrl = normalizedPreferredUrl == null
1307
+ ? null
1308
+ : (pages.find((page) => {
1309
+ try {
1310
+ return normalize_url(page.url()) === normalizedPreferredUrl;
1311
+ }
1312
+ catch {
1313
+ return false;
1314
+ }
1315
+ }) ?? null);
1316
+ const clampedIndex = preferredTabIndex >= 0 && preferredTabIndex < pages.length
1317
+ ? preferredTabIndex
1318
+ : 0;
1319
+ const nextPage = pageByUrl ?? pages[clampedIndex] ?? pages[0] ?? null;
1320
+ const nextTabIndex = nextPage
1321
+ ? this._tabs.findIndex((tab) => this.tabPages.get(tab.page_id) === nextPage)
1322
+ : -1;
1323
+ if (nextTabIndex >= 0) {
1324
+ this.currentTabIndex = nextTabIndex;
1325
+ }
1326
+ this._setActivePage(nextPage);
1327
+ this.human_current_page = nextPage;
1328
+ await this._syncCurrentTabFromPage(nextPage);
1329
+ }
1330
+ async reconnect(options = {}) {
1331
+ if (!this._usesRemoteBrowserConnection()) {
1332
+ throw new Error('Cannot reconnect without a remote browser connection');
1333
+ }
1334
+ const preferredUrl = typeof options.preferred_url === 'string'
1335
+ ? options.preferred_url
1336
+ : this.currentUrl;
1337
+ const preferredTabIndex = typeof options.preferred_tab_index === 'number'
1338
+ ? options.preferred_tab_index
1339
+ : this.currentTabIndex;
1340
+ this._detachRemoteDisconnectHandler();
1341
+ this.cachedBrowserState = null;
1342
+ this.currentPageLoadingStatus = null;
1343
+ this.browser = null;
1344
+ this.browser_context = null;
1345
+ this.agent_current_page = null;
1346
+ this.human_current_page = null;
1347
+ this._dialogHandlersAttached = new WeakSet();
1348
+ this.session_manager.clear();
1349
+ const playwright = this.playwright ?? (await async_playwright());
1350
+ this.playwright = playwright;
1351
+ this.browser = await this._connectToConfiguredBrowser(playwright);
1352
+ this.ownsBrowserResources = false;
1353
+ this.browser_context = await this._ensureBrowserContextFromBrowser(this.browser);
1354
+ await this._applyConfiguredExtraHttpHeaders();
1355
+ await this._restorePagesAfterReconnect(preferredUrl, preferredTabIndex);
1356
+ this._attachRemoteDisconnectHandler(this.browser);
1357
+ this.initialized = true;
1358
+ this._recordRecentEvent('browser_reconnected', {
1359
+ url: this.currentUrl,
1360
+ });
1361
+ }
1362
+ async _auto_reconnect(maxAttempts = 3) {
1363
+ if (this._reconnecting || !this._usesRemoteBrowserConnection()) {
1364
+ return;
1365
+ }
1366
+ this._reconnecting = true;
1367
+ this._beginReconnectWait();
1368
+ const startTime = Date.now();
1369
+ const preferredUrl = this.currentUrl;
1370
+ const preferredTabIndex = this.currentTabIndex;
1371
+ try {
1372
+ await this.event_bus.dispatch(new BrowserStoppedEvent({
1373
+ reason: 'connection_lost',
1374
+ }));
1375
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
1376
+ if (this._intentionalStop) {
1377
+ return;
1378
+ }
1379
+ await this.event_bus.dispatch(new BrowserReconnectingEvent({
1380
+ cdp_url: this.cdp_url ?? this.wss_url ?? 'remote',
1381
+ attempt,
1382
+ max_attempts: maxAttempts,
1383
+ }));
1384
+ try {
1385
+ await Promise.race([
1386
+ this.reconnect({
1387
+ preferred_url: preferredUrl,
1388
+ preferred_tab_index: preferredTabIndex,
1389
+ }),
1390
+ new Promise((_, reject) => {
1391
+ setTimeout(() => {
1392
+ reject(new Error(`Reconnect attempt timed out after ${Math.round(REMOTE_RECONNECT_ATTEMPT_TIMEOUT_MS / 1000)}s`));
1393
+ }, REMOTE_RECONNECT_ATTEMPT_TIMEOUT_MS);
1394
+ }),
1395
+ ]);
1396
+ if (this._intentionalStop) {
1397
+ return;
1398
+ }
1399
+ await this.event_bus.dispatch(new BrowserConnectedEvent({
1400
+ cdp_url: this.cdp_url ?? this.wss_url ?? 'remote',
1401
+ }));
1402
+ await this.event_bus.dispatch(new BrowserReconnectedEvent({
1403
+ cdp_url: this.cdp_url ?? this.wss_url ?? 'remote',
1404
+ attempt,
1405
+ downtime_seconds: (Date.now() - startTime) / 1000,
1406
+ }));
1407
+ return;
1408
+ }
1409
+ catch (error) {
1410
+ this.logger.warning(`Reconnect attempt ${attempt}/${maxAttempts} failed: ${error.message}`);
1411
+ if (attempt >= maxAttempts) {
1412
+ break;
1413
+ }
1414
+ const delayMs = REMOTE_RECONNECT_DELAYS_MS[attempt - 1] ??
1415
+ REMOTE_RECONNECT_DELAYS_MS[REMOTE_RECONNECT_DELAYS_MS.length - 1];
1416
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
1417
+ }
1418
+ }
1419
+ await this.event_bus.dispatch(new BrowserErrorEvent({
1420
+ error_type: 'ReconnectionFailed',
1421
+ message: `Failed to reconnect after ${maxAttempts} attempts (${((Date.now() - startTime) / 1000).toFixed(1)}s)`,
1422
+ details: {
1423
+ cdp_url: this.cdp_url ?? this.wss_url ?? 'remote',
1424
+ max_attempts: maxAttempts,
1425
+ },
1426
+ }));
1427
+ }
1428
+ finally {
1429
+ this._reconnecting = false;
1430
+ this._endReconnectWait();
1431
+ }
1432
+ }
793
1433
  _isSandboxLaunchError(error) {
794
1434
  const message = error instanceof Error ? error.message : String(error);
795
1435
  return (/no usable sandbox/i.test(message) ||
@@ -846,6 +1486,7 @@ export class BrowserSession {
846
1486
  }
847
1487
  async start() {
848
1488
  this.attach_default_watchdogs();
1489
+ this._intentionalStop = false;
849
1490
  if (this.initialized) {
850
1491
  return this;
851
1492
  }
@@ -878,12 +1519,11 @@ export class BrowserSession {
878
1519
  const playwright = this.playwright ?? (await async_playwright());
879
1520
  this.playwright = playwright;
880
1521
  if (this.cdp_url) {
881
- this.browser = await playwright.chromium.connectOverCDP(this.cdp_url);
1522
+ this.browser = await this._connectToConfiguredBrowser(playwright);
882
1523
  this.ownsBrowserResources = false;
883
1524
  }
884
1525
  else if (this.wss_url) {
885
- const connectOptions = this._toPlaywrightOptions(this.browser_profile.kwargs_for_connect());
886
- this.browser = await playwright.chromium.connect(this.wss_url, connectOptions ?? {});
1526
+ this.browser = await this._connectToConfiguredBrowser(playwright);
887
1527
  this.ownsBrowserResources = false;
888
1528
  }
889
1529
  else {
@@ -905,14 +1545,11 @@ export class BrowserSession {
905
1545
  if (existingContexts.length > 0) {
906
1546
  this.browser_context = existingContexts[0] ?? null;
907
1547
  }
908
- else if (typeof this.browser?.newContext === 'function') {
909
- const contextOptions = this._toPlaywrightOptions(this.browser_profile.kwargs_for_new_context());
910
- this.browser_context = await this.browser.newContext(contextOptions ?? {});
911
- }
912
1548
  else {
913
- this.browser_context = null;
1549
+ this.browser_context = await this._ensureBrowserContextFromBrowser(this.browser);
914
1550
  }
915
1551
  }
1552
+ await this._applyConfiguredExtraHttpHeaders();
916
1553
  await ensurePage();
917
1554
  if (!this.human_current_page ||
918
1555
  this.human_current_page.isClosed?.()) {
@@ -938,6 +1575,7 @@ export class BrowserSession {
938
1575
  this.initialized = true;
939
1576
  this._recordRecentEvent('browser_started', { url: this.currentUrl });
940
1577
  this.logger.debug(`Started ${this.describe()} with profile ${this.browser_profile.toString()}`);
1578
+ this._attachRemoteDisconnectHandler(this.browser);
941
1579
  await this.event_bus.dispatch(new BrowserConnectedEvent({
942
1580
  cdp_url: this.cdp_url ?? this.wss_url ?? 'playwright',
943
1581
  }));
@@ -964,7 +1602,7 @@ export class BrowserSession {
964
1602
  // Connect to browser via CDP
965
1603
  try {
966
1604
  const playwright = await import('playwright');
967
- const browser = await playwright.chromium.connectOverCDP(cdpUrl);
1605
+ const browser = await this._connectToConfiguredBrowser(playwright);
968
1606
  this.browser = browser;
969
1607
  this.playwright = playwright;
970
1608
  // Get or create context
@@ -975,6 +1613,7 @@ export class BrowserSession {
975
1613
  else {
976
1614
  this.browser_context = (await browser.newContext());
977
1615
  }
1616
+ await this._applyConfiguredExtraHttpHeaders();
978
1617
  // Get or create page
979
1618
  if (!this.browser_context) {
980
1619
  throw new Error('Browser context not available');
@@ -991,6 +1630,7 @@ export class BrowserSession {
991
1630
  }
992
1631
  // We don't own this browser since we're connecting to existing one
993
1632
  this.ownsBrowserResources = false;
1633
+ this._attachRemoteDisconnectHandler(this.browser);
994
1634
  this.initialized = true;
995
1635
  this.logger.info(`Successfully connected to browser PID ${browserPid}`);
996
1636
  }
@@ -1025,6 +1665,11 @@ export class BrowserSession {
1025
1665
  }
1026
1666
  async _shutdown_browser_session() {
1027
1667
  this.initialized = false;
1668
+ this._intentionalStop = true;
1669
+ this._reconnecting = false;
1670
+ this._endReconnectWait();
1671
+ this._reconnectTask = null;
1672
+ this._detachRemoteDisconnectHandler();
1028
1673
  this.attachedAgentId = null;
1029
1674
  this.attachedSharedAgentIds.clear();
1030
1675
  const closeWithTimeout = async (label, operation, timeoutMs = 3000) => {
@@ -1101,7 +1746,7 @@ export class BrowserSession {
1101
1746
  else {
1102
1747
  try {
1103
1748
  const domService = new DomService(page, this.logger);
1104
- domState = await this._withAbort(domService.get_clickable_elements(), signal);
1749
+ domState = await this._withAbort(domService.get_clickable_elements(this.browser_profile.highlight_elements, -1, this.browser_profile.viewport_expansion), signal);
1105
1750
  }
1106
1751
  catch (error) {
1107
1752
  if (this._isAbortError(error)) {
@@ -1110,6 +1755,29 @@ export class BrowserSession {
1110
1755
  this.logger.debug(`Failed to build DOM tree: ${error.message}`);
1111
1756
  domState = createEmptyDomState();
1112
1757
  }
1758
+ const liveUrl = typeof page.url === 'function'
1759
+ ? normalize_url(page.url())
1760
+ : this.currentUrl;
1761
+ const shouldRetryEmptyDom = Object.keys(domState.selector_map).length === 0 &&
1762
+ !this._is_new_tab_page(liveUrl) &&
1763
+ !liveUrl.toLowerCase().endsWith('.pdf');
1764
+ if (shouldRetryEmptyDom) {
1765
+ this.logger.debug(`Empty DOM detected for ${liveUrl}; retrying once`);
1766
+ await this._waitWithAbort(EMPTY_DOM_RETRY_DELAY_MS, signal);
1767
+ try {
1768
+ const retryDomService = new DomService(page, this.logger);
1769
+ const retriedDomState = await this._withAbort(retryDomService.get_clickable_elements(this.browser_profile.highlight_elements, -1, this.browser_profile.viewport_expansion), signal);
1770
+ if (Object.keys(retriedDomState.selector_map).length > 0) {
1771
+ domState = retriedDomState;
1772
+ }
1773
+ }
1774
+ catch (error) {
1775
+ if (this._isAbortError(error)) {
1776
+ throw error;
1777
+ }
1778
+ this.logger.debug(`Retry after empty DOM failed: ${error.message}`);
1779
+ }
1780
+ }
1113
1781
  }
1114
1782
  let screenshot = null;
1115
1783
  if (options.include_screenshot && page?.screenshot) {
@@ -1176,6 +1844,9 @@ export class BrowserSession {
1176
1844
  }
1177
1845
  }
1178
1846
  const pendingNetworkRequests = await this._getPendingNetworkRequests(page);
1847
+ if (page) {
1848
+ await this._syncCurrentTabFromPage(page);
1849
+ }
1179
1850
  if (pageInfo &&
1180
1851
  Number.isFinite(pageInfo.viewport_width) &&
1181
1852
  Number.isFinite(pageInfo.viewport_height)) {
@@ -1225,6 +1896,7 @@ export class BrowserSession {
1225
1896
  return summary;
1226
1897
  }
1227
1898
  async get_current_page() {
1899
+ this._syncTabsWithBrowserPages();
1228
1900
  if (this.agent_current_page) {
1229
1901
  return this.agent_current_page;
1230
1902
  }
@@ -1275,6 +1947,7 @@ export class BrowserSession {
1275
1947
  this._throwIfAborted(signal);
1276
1948
  this._assert_url_allowed(url);
1277
1949
  const normalized = normalize_url(url);
1950
+ let completedUrl = normalized;
1278
1951
  const waitUntil = options.wait_until ?? 'domcontentloaded';
1279
1952
  const timeoutMs = typeof options.timeout_ms === 'number' &&
1280
1953
  Number.isFinite(options.timeout_ms)
@@ -1294,6 +1967,7 @@ export class BrowserSession {
1294
1967
  await this._withAbort(page.goto(normalized, gotoOptions), signal);
1295
1968
  const finalUrl = page.url();
1296
1969
  this._assert_url_allowed(finalUrl);
1970
+ completedUrl = normalize_url(finalUrl);
1297
1971
  await this._waitForStableNetwork(page, signal);
1298
1972
  }
1299
1973
  catch (error) {
@@ -1309,16 +1983,24 @@ export class BrowserSession {
1309
1983
  }
1310
1984
  }
1311
1985
  this._throwIfAborted(signal);
1312
- this.currentUrl = normalized;
1313
- this.currentTitle = normalized;
1314
- this.historyStack.push(normalized);
1315
- if (this._tabs[this.currentTabIndex]) {
1316
- this._tabs[this.currentTabIndex].url = normalized;
1317
- this._tabs[this.currentTabIndex].title = normalized;
1986
+ if (page) {
1987
+ await this._syncCurrentTabFromPage(page);
1988
+ completedUrl = this.currentUrl || completedUrl;
1989
+ }
1990
+ else {
1991
+ this.currentUrl = normalized;
1992
+ this.currentTitle = normalized;
1993
+ if (this._tabs[this.currentTabIndex]) {
1994
+ this._tabs[this.currentTabIndex].url = normalized;
1995
+ this._tabs[this.currentTabIndex].title = normalized;
1996
+ }
1997
+ this._syncSessionManagerFromTabs();
1998
+ }
1999
+ if (this.historyStack[this.historyStack.length - 1] !== completedUrl) {
2000
+ this.historyStack.push(completedUrl);
1318
2001
  }
1319
- this._syncSessionManagerFromTabs();
1320
2002
  this._setActivePage(page ?? null);
1321
- this._recordRecentEvent('navigation_completed', { url: normalized });
2003
+ this._recordRecentEvent('navigation_completed', { url: completedUrl });
1322
2004
  this.cachedBrowserState = null;
1323
2005
  return this.agent_current_page;
1324
2006
  }
@@ -1327,11 +2009,14 @@ export class BrowserSession {
1327
2009
  this._throwIfAborted(signal);
1328
2010
  this._assert_url_allowed(url);
1329
2011
  const normalized = normalize_url(url);
2012
+ let completedUrl = normalized;
1330
2013
  const waitUntil = options.wait_until ?? 'domcontentloaded';
1331
2014
  const timeoutMs = typeof options.timeout_ms === 'number' &&
1332
2015
  Number.isFinite(options.timeout_ms)
1333
2016
  ? Math.max(0, options.timeout_ms)
1334
2017
  : null;
2018
+ const previousTabIndex = this.currentTabIndex;
2019
+ const previousTab = this._tabs[this.currentTabIndex] ?? null;
1335
2020
  const newTab = this._createTabInfo({
1336
2021
  page_id: this._tabCounter++,
1337
2022
  url: normalized,
@@ -1342,11 +2027,6 @@ export class BrowserSession {
1342
2027
  this.currentUrl = normalized;
1343
2028
  this.currentTitle = normalized;
1344
2029
  this.historyStack.push(normalized);
1345
- this._recordRecentEvent('tab_created', {
1346
- url: normalized,
1347
- page_id: newTab.page_id,
1348
- tab_id: newTab.tab_id,
1349
- });
1350
2030
  let page = null;
1351
2031
  try {
1352
2032
  page =
@@ -1362,6 +2042,7 @@ export class BrowserSession {
1362
2042
  await this._withAbort(page.goto(normalized, gotoOptions), signal);
1363
2043
  const finalUrl = page.url();
1364
2044
  this._assert_url_allowed(finalUrl);
2045
+ completedUrl = normalize_url(finalUrl);
1365
2046
  await this._waitForStableNetwork(page, signal);
1366
2047
  }
1367
2048
  }
@@ -1369,29 +2050,82 @@ export class BrowserSession {
1369
2050
  if (this._isAbortError(error)) {
1370
2051
  throw error;
1371
2052
  }
2053
+ const message = error.message ?? 'Failed to open new tab';
1372
2054
  this._recordRecentEvent('tab_navigation_failed', {
1373
2055
  url: normalized,
1374
2056
  page_id: newTab.page_id,
1375
2057
  tab_id: newTab.tab_id,
1376
- error_message: error.message ?? 'Failed to open new tab',
2058
+ error_message: message,
1377
2059
  });
1378
- this.logger.debug(`Failed to open new tab via Playwright: ${error.message}`);
2060
+ this.logger.debug(`Failed to open new tab via Playwright: ${message}`);
2061
+ if (page?.close) {
2062
+ try {
2063
+ await page.close();
2064
+ }
2065
+ catch {
2066
+ // Ignore best-effort tab close failures during rollback.
2067
+ }
2068
+ }
2069
+ this._tabs = this._tabs.filter((tab) => tab.page_id !== newTab.page_id);
2070
+ this.tabPages.delete(newTab.page_id);
2071
+ if (this.historyStack[this.historyStack.length - 1] === normalized) {
2072
+ this.historyStack.pop();
2073
+ }
2074
+ if (this._tabs.length > 0) {
2075
+ let restoredIndex = previousTab
2076
+ ? this._tabs.findIndex((tab) => tab.page_id === previousTab.page_id)
2077
+ : -1;
2078
+ if (restoredIndex === -1) {
2079
+ restoredIndex = Math.min(previousTabIndex, this._tabs.length - 1);
2080
+ }
2081
+ this.currentTabIndex = Math.max(0, restoredIndex);
2082
+ const restoredTab = this._tabs[this.currentTabIndex];
2083
+ this.currentUrl = restoredTab.url;
2084
+ this.currentTitle = restoredTab.title;
2085
+ const restoredPage = this.tabPages.get(restoredTab.page_id) ?? null;
2086
+ this._setActivePage(restoredPage);
2087
+ await this._syncCurrentTabFromPage(restoredPage);
2088
+ }
2089
+ else {
2090
+ this.currentTabIndex = 0;
2091
+ this.currentUrl = 'about:blank';
2092
+ this.currentTitle = 'about:blank';
2093
+ this._setActivePage(null);
2094
+ }
2095
+ this._syncSessionManagerFromTabs();
2096
+ this.cachedBrowserState = null;
2097
+ throw new BrowserError(message);
1379
2098
  }
1380
2099
  this.tabPages.set(newTab.page_id, page);
1381
2100
  this._syncSessionManagerFromTabs();
1382
2101
  this._setActivePage(page);
2102
+ if (page) {
2103
+ await this._syncCurrentTabFromPage(page);
2104
+ completedUrl = this.currentUrl || completedUrl;
2105
+ }
2106
+ if (this.historyStack[this.historyStack.length - 1] === normalized) {
2107
+ this.historyStack[this.historyStack.length - 1] = completedUrl;
2108
+ }
2109
+ else if (this.historyStack[this.historyStack.length - 1] !== completedUrl) {
2110
+ this.historyStack.push(completedUrl);
2111
+ }
1383
2112
  this.currentPageLoadingStatus = null;
1384
2113
  if (!this.human_current_page) {
1385
2114
  this.human_current_page = page;
1386
2115
  }
2116
+ this._recordRecentEvent('tab_created', {
2117
+ url: completedUrl,
2118
+ page_id: newTab.page_id,
2119
+ tab_id: newTab.tab_id,
2120
+ });
1387
2121
  this._recordRecentEvent('tab_ready', {
1388
- url: normalized,
2122
+ url: completedUrl,
1389
2123
  page_id: newTab.page_id,
1390
2124
  tab_id: newTab.tab_id,
1391
2125
  });
1392
2126
  await this.event_bus.dispatch(new TabCreatedEvent({
1393
2127
  target_id: newTab.target_id ?? newTab.tab_id ?? 'unknown_target',
1394
- url: normalized,
2128
+ url: completedUrl,
1395
2129
  }));
1396
2130
  this.cachedBrowserState = null;
1397
2131
  return this.agent_current_page;
@@ -1434,6 +2168,7 @@ export class BrowserSession {
1434
2168
  async switch_to_tab(identifier, options = {}) {
1435
2169
  const signal = options.signal ?? null;
1436
2170
  this._throwIfAborted(signal);
2171
+ this._syncTabsWithBrowserPages();
1437
2172
  const index = this._resolveTabIndex(identifier);
1438
2173
  const tab = index >= 0 ? (this._tabs[index] ?? null) : null;
1439
2174
  if (!tab) {
@@ -1473,6 +2208,7 @@ export class BrowserSession {
1473
2208
  return page;
1474
2209
  }
1475
2210
  async close_tab(identifier) {
2211
+ this._syncTabsWithBrowserPages();
1476
2212
  const index = this._resolveTabIndex(identifier);
1477
2213
  if (index < 0 || index >= this._tabs.length) {
1478
2214
  throw new Error(`Tab '${identifier}' does not exist`);
@@ -1887,31 +2623,34 @@ export class BrowserSession {
1887
2623
  async go_back(options = {}) {
1888
2624
  const signal = options.signal ?? null;
1889
2625
  this._throwIfAborted(signal);
1890
- if (this.historyStack.length <= 1) {
2626
+ const page = await this._withAbort(this.get_current_page(), signal);
2627
+ if (!page?.goBack) {
1891
2628
  return;
1892
2629
  }
1893
- const page = await this._withAbort(this.get_current_page(), signal);
1894
- if (page?.goBack) {
1895
- try {
1896
- await this._withAbort(page.goBack(), signal);
1897
- }
1898
- catch (error) {
1899
- if (this._isAbortError(error)) {
1900
- throw error;
1901
- }
1902
- this.logger.debug(`Failed to navigate back: ${error.message}`);
2630
+ const previousUrl = this.currentUrl;
2631
+ try {
2632
+ await this._withAbort(page.goBack(), signal);
2633
+ }
2634
+ catch (error) {
2635
+ if (this._isAbortError(error)) {
2636
+ throw error;
1903
2637
  }
2638
+ this.logger.debug(`Failed to navigate back: ${error.message}`);
1904
2639
  }
1905
2640
  this._throwIfAborted(signal);
1906
- this.historyStack.pop();
1907
- const previous = this.historyStack[this.historyStack.length - 1];
1908
- this.currentUrl = previous;
1909
- this.currentTitle = previous;
1910
- if (this._tabs[this.currentTabIndex]) {
1911
- this._tabs[this.currentTabIndex].url = previous;
1912
- this._tabs[this.currentTabIndex].title = previous;
2641
+ await this._syncCurrentTabFromPage(page);
2642
+ const currentUrl = this.currentUrl;
2643
+ if (currentUrl && currentUrl !== previousUrl) {
2644
+ const existingIndex = this.historyStack.lastIndexOf(currentUrl);
2645
+ if (existingIndex !== -1) {
2646
+ this.historyStack = this.historyStack.slice(0, existingIndex + 1);
2647
+ }
2648
+ else if (this.historyStack[this.historyStack.length - 1] !== currentUrl) {
2649
+ this.historyStack.push(currentUrl);
2650
+ }
1913
2651
  }
1914
- this._recordRecentEvent('navigation_back', { url: previous });
2652
+ this.cachedBrowserState = null;
2653
+ this._recordRecentEvent('navigation_back', { url: currentUrl });
1915
2654
  }
1916
2655
  async get_dom_element_by_index(_index, options = {}) {
1917
2656
  const selectorMap = await this.get_selector_map(options);
@@ -2123,6 +2862,13 @@ export class BrowserSession {
2123
2862
  await performClick();
2124
2863
  }
2125
2864
  await this._waitForLoad(page, 5000, signal);
2865
+ if (page) {
2866
+ await this._syncCurrentTabFromPage(page);
2867
+ if (this.historyStack[this.historyStack.length - 1] !== this.currentUrl) {
2868
+ this.historyStack.push(this.currentUrl);
2869
+ }
2870
+ }
2871
+ this.cachedBrowserState = null;
2126
2872
  return null;
2127
2873
  }
2128
2874
  async _waitForLoad(page, timeout = 5000, signal = null) {
@@ -2435,11 +3181,12 @@ export class BrowserSession {
2435
3181
  }
2436
3182
  // ==================== Screenshots ====================
2437
3183
  /**
2438
- * Take a screenshot of the current page
3184
+ * Take a screenshot of the current page.
2439
3185
  * @param full_page Whether to capture the full scrollable page
3186
+ * @param clip Optional clip region for partial screenshots
2440
3187
  * @returns Base64 encoded PNG screenshot
2441
3188
  */
2442
- async take_screenshot(full_page = false) {
3189
+ async take_screenshot(full_page = false, clip = null) {
2443
3190
  const page = await this.get_current_page();
2444
3191
  if (!page) {
2445
3192
  throw new Error('No page available for screenshot');
@@ -2470,11 +3217,21 @@ export class BrowserSession {
2470
3217
  // Create CDP session for the screenshot
2471
3218
  cdp_session = await this.get_or_create_cdp_session(page);
2472
3219
  // Capture screenshot via CDP
2473
- const screenshot_response = await cdp_session.send('Page.captureScreenshot', {
2474
- captureBeyondViewport: false,
3220
+ const screenshotParams = {
3221
+ captureBeyondViewport: full_page,
2475
3222
  fromSurface: true,
2476
3223
  format: 'png',
2477
- });
3224
+ };
3225
+ if (clip) {
3226
+ screenshotParams.clip = {
3227
+ x: clip.x,
3228
+ y: clip.y,
3229
+ width: clip.width,
3230
+ height: clip.height,
3231
+ scale: 1,
3232
+ };
3233
+ }
3234
+ const screenshot_response = await cdp_session.send('Page.captureScreenshot', screenshotParams);
2478
3235
  const screenshot_b64 = screenshot_response.data;
2479
3236
  if (!screenshot_b64) {
2480
3237
  throw new Error(`CDP returned empty screenshot data for page ${url}`);
@@ -2548,22 +3305,34 @@ export class BrowserSession {
2548
3305
  if (!this.browser_context) {
2549
3306
  return [];
2550
3307
  }
3308
+ this._syncTabsWithBrowserPages();
2551
3309
  const tabs_info = [];
2552
- const pages = this.browser_context.pages();
2553
- for (let page_id = 0; page_id < pages.length; page_id++) {
2554
- const page = pages[page_id];
2555
- const tab_id = this._tabs.find((tab) => tab.page_id === page_id)?.tab_id ??
2556
- this._formatTabId(page_id);
2557
- this._attachDialogHandler(page ?? null);
3310
+ for (const tab of this._tabs) {
3311
+ const page_id = tab.page_id;
3312
+ const page = this.tabPages.get(page_id) ?? null;
3313
+ const tab_id = tab.tab_id || this._formatTabId(page_id);
3314
+ if (!tab.tab_id) {
3315
+ tab.tab_id = tab_id;
3316
+ }
3317
+ this._attachDialogHandler(page);
3318
+ let currentUrl = tab.url;
3319
+ if (page?.url) {
3320
+ try {
3321
+ currentUrl = normalize_url(page.url());
3322
+ }
3323
+ catch {
3324
+ // Keep tab url fallback when page url is unavailable.
3325
+ }
3326
+ }
2558
3327
  // Skip chrome:// pages and new tab pages
2559
- const isNewTab = page.url() === 'about:blank' ||
2560
- page.url().startsWith('chrome://newtab');
2561
- if (isNewTab || page.url().startsWith('chrome://')) {
3328
+ const isNewTab = currentUrl === 'about:blank' ||
3329
+ currentUrl.startsWith('chrome://newtab');
3330
+ if (isNewTab || currentUrl.startsWith('chrome://')) {
2562
3331
  if (isNewTab) {
2563
3332
  tabs_info.push({
2564
3333
  page_id,
2565
3334
  tab_id,
2566
- url: page.url(),
3335
+ url: currentUrl,
2567
3336
  title: 'ignore this tab and do not use it',
2568
3337
  });
2569
3338
  }
@@ -2571,28 +3340,31 @@ export class BrowserSession {
2571
3340
  tabs_info.push({
2572
3341
  page_id,
2573
3342
  tab_id,
2574
- url: page.url(),
2575
- title: page.url(),
3343
+ url: currentUrl,
3344
+ title: currentUrl,
2576
3345
  });
2577
3346
  }
2578
3347
  continue;
2579
3348
  }
2580
3349
  // Normal pages - try to get title with timeout
2581
3350
  try {
3351
+ if (!page?.title) {
3352
+ throw new Error('page_title_unavailable');
3353
+ }
2582
3354
  const titlePromise = page.title();
2583
3355
  const timeoutPromise = new Promise((_, reject) => {
2584
3356
  setTimeout(() => reject(new Error('timeout')), 2000);
2585
3357
  });
2586
3358
  const title = await Promise.race([titlePromise, timeoutPromise]);
2587
- tabs_info.push({ page_id, tab_id, url: page.url(), title });
3359
+ tabs_info.push({ page_id, tab_id, url: currentUrl, title });
2588
3360
  }
2589
3361
  catch (error) {
2590
- this.logger.debug(`⚠️ Failed to get tab info for tab #${page_id}: ${page.url()} (using fallback title)`);
3362
+ this.logger.debug(`⚠️ Failed to get tab info for tab #${page_id}: ${currentUrl} (using fallback title)`);
2591
3363
  if (isNewTab) {
2592
3364
  tabs_info.push({
2593
3365
  page_id,
2594
3366
  tab_id,
2595
- url: page.url(),
3367
+ url: currentUrl,
2596
3368
  title: 'ignore this tab and do not use it',
2597
3369
  });
2598
3370
  }
@@ -2600,8 +3372,8 @@ export class BrowserSession {
2600
3372
  tabs_info.push({
2601
3373
  page_id,
2602
3374
  tab_id,
2603
- url: page.url(),
2604
- title: page.url(), // Use URL as fallback title
3375
+ url: currentUrl,
3376
+ title: tab.title || currentUrl,
2605
3377
  });
2606
3378
  }
2607
3379
  }
@@ -2966,6 +3738,15 @@ export class BrowserSession {
2966
3738
  }
2967
3739
  return [host, `www.${host}`];
2968
3740
  }
3741
+ _setEntryMatchesUrl(domains, hostVariant, hostAlt, protocol) {
3742
+ const matchedHost = domains.has(hostVariant) || domains.has(hostAlt);
3743
+ if (!matchedHost) {
3744
+ return false;
3745
+ }
3746
+ // Set-optimized entries are exact hostnames without explicit schemes,
3747
+ // so keep parity with pattern matching default: https-only.
3748
+ return protocol.toLowerCase() === 'https:';
3749
+ }
2969
3750
  /**
2970
3751
  * Check if page is displaying a PDF
2971
3752
  */
@@ -3258,7 +4039,7 @@ export class BrowserSession {
3258
4039
  ((Array.isArray(allowedDomains) && allowedDomains.length > 0) ||
3259
4040
  (allowedDomains instanceof Set && allowedDomains.size > 0))) {
3260
4041
  if (allowedDomains instanceof Set) {
3261
- if (allowedDomains.has(hostVariant) || allowedDomains.has(hostAlt)) {
4042
+ if (this._setEntryMatchesUrl(allowedDomains, hostVariant, hostAlt, parsed.protocol)) {
3262
4043
  return null;
3263
4044
  }
3264
4045
  }
@@ -3281,8 +4062,7 @@ export class BrowserSession {
3281
4062
  ((Array.isArray(prohibitedDomains) && prohibitedDomains.length > 0) ||
3282
4063
  (prohibitedDomains instanceof Set && prohibitedDomains.size > 0))) {
3283
4064
  if (prohibitedDomains instanceof Set) {
3284
- if (prohibitedDomains.has(hostVariant) ||
3285
- prohibitedDomains.has(hostAlt)) {
4065
+ if (this._setEntryMatchesUrl(prohibitedDomains, hostVariant, hostAlt, parsed.protocol)) {
3286
4066
  return 'in_prohibited_domains';
3287
4067
  }
3288
4068
  }
@@ -3405,57 +4185,31 @@ export class BrowserSession {
3405
4185
  // Check if downloads are enabled
3406
4186
  const downloads_path = this.browser_profile.downloads_path;
3407
4187
  if (downloads_path) {
4188
+ fs.mkdirSync(downloads_path, { recursive: true });
4189
+ // Try to detect file download.
4190
+ const download_promise = page.waitForEvent('download', {
4191
+ timeout: 5000,
4192
+ });
4193
+ // Click failures should bubble to the caller.
3408
4194
  try {
3409
- // Try to detect file download
3410
- const download_promise = page.waitForEvent('download', {
3411
- timeout: 5000,
3412
- });
3413
- // Perform the click
3414
4195
  await element_handle.click();
3415
- // Wait for download or timeout
3416
- const download = await download_promise;
3417
- // Save the downloaded file
3418
- const suggested_filename = download.suggestedFilename();
3419
- const unique_filename = await BrowserSession.get_unique_filename(downloads_path, suggested_filename);
3420
- const download_path = path.join(downloads_path, unique_filename);
3421
- const download_guid = uuid7str();
3422
- const download_url = typeof download.url === 'function'
3423
- ? download.url()
3424
- : (this.currentUrl ?? '');
3425
- await this.event_bus.dispatch(new DownloadStartedEvent({
3426
- guid: download_guid,
3427
- url: download_url,
3428
- suggested_filename,
3429
- auto_download: false,
3430
- }));
3431
- await download.saveAs(download_path);
3432
- this.logger.info(`⬇️ Downloaded file to: ${download_path}`);
3433
- const stats = fs.existsSync(download_path)
3434
- ? fs.statSync(download_path)
3435
- : null;
3436
- await this.event_bus.dispatch(new DownloadProgressEvent({
3437
- guid: download_guid,
3438
- received_bytes: stats?.size ?? 0,
3439
- total_bytes: stats?.size ?? 0,
3440
- state: 'completed',
3441
- }));
3442
- const fileDownloadedResult = await this.event_bus.dispatch(new FileDownloadedEvent({
3443
- guid: download_guid,
3444
- url: download_url,
3445
- path: download_path,
3446
- file_name: unique_filename,
3447
- file_size: stats?.size ?? 0,
3448
- file_type: path.extname(unique_filename).replace('.', '') || null,
3449
- mime_type: null,
3450
- auto_download: false,
3451
- }));
3452
- if (fileDownloadedResult.handler_results.length === 0) {
3453
- this.add_downloaded_file(download_path);
3454
- }
3455
- return download_path;
3456
4196
  }
3457
4197
  catch (error) {
3458
- // No download triggered, treat as normal click
4198
+ void download_promise.catch(() => undefined);
4199
+ throw error;
4200
+ }
4201
+ let download;
4202
+ try {
4203
+ download = await download_promise;
4204
+ }
4205
+ catch (error) {
4206
+ const message = error instanceof Error ? error.message : String(error);
4207
+ const isDownloadTimeout = error instanceof Error &&
4208
+ (error.name === 'TimeoutError' ||
4209
+ message.toLowerCase().includes('timeout'));
4210
+ if (!isDownloadTimeout) {
4211
+ throw error;
4212
+ }
3459
4213
  this.logger.debug('No download triggered within timeout. Checking navigation...');
3460
4214
  try {
3461
4215
  await page.waitForLoadState();
@@ -3463,7 +4217,47 @@ export class BrowserSession {
3463
4217
  catch (e) {
3464
4218
  this.logger.warning(`Navigation check failed: ${e.message}`);
3465
4219
  }
4220
+ return null;
3466
4221
  }
4222
+ // Save the downloaded file.
4223
+ const suggested_filename = download.suggestedFilename();
4224
+ const unique_filename = await BrowserSession.get_unique_filename(downloads_path, suggested_filename);
4225
+ const download_path = path.join(downloads_path, unique_filename);
4226
+ const download_guid = uuid7str();
4227
+ const download_url = typeof download.url === 'function'
4228
+ ? download.url()
4229
+ : (this.currentUrl ?? '');
4230
+ await this.event_bus.dispatch(new DownloadStartedEvent({
4231
+ guid: download_guid,
4232
+ url: download_url,
4233
+ suggested_filename,
4234
+ auto_download: false,
4235
+ }));
4236
+ await download.saveAs(download_path);
4237
+ this.logger.info(`⬇️ Downloaded file to: ${download_path}`);
4238
+ const stats = fs.existsSync(download_path)
4239
+ ? fs.statSync(download_path)
4240
+ : null;
4241
+ await this.event_bus.dispatch(new DownloadProgressEvent({
4242
+ guid: download_guid,
4243
+ received_bytes: stats?.size ?? 0,
4244
+ total_bytes: stats?.size ?? 0,
4245
+ state: 'completed',
4246
+ }));
4247
+ const fileDownloadedResult = await this.event_bus.dispatch(new FileDownloadedEvent({
4248
+ guid: download_guid,
4249
+ url: download_url,
4250
+ path: download_path,
4251
+ file_name: unique_filename,
4252
+ file_size: stats?.size ?? 0,
4253
+ file_type: path.extname(unique_filename).replace('.', '') || null,
4254
+ mime_type: null,
4255
+ auto_download: false,
4256
+ }));
4257
+ if (fileDownloadedResult.handler_results.length === 0) {
4258
+ this.add_downloaded_file(download_path);
4259
+ }
4260
+ return download_path;
3467
4261
  }
3468
4262
  else {
3469
4263
  // No downloads path configured, just click
@@ -3481,15 +4275,33 @@ export class BrowserSession {
3481
4275
  }
3482
4276
  try {
3483
4277
  await page.evaluate(() => {
3484
- // Remove all elements with browser-use highlight class
4278
+ const pageWindow = window;
4279
+ const cleanupFunctions = Array.isArray(pageWindow._highlightCleanupFunctions)
4280
+ ? pageWindow._highlightCleanupFunctions
4281
+ : [];
4282
+ for (const cleanupFn of cleanupFunctions) {
4283
+ try {
4284
+ if (typeof cleanupFn === 'function') {
4285
+ cleanupFn();
4286
+ }
4287
+ }
4288
+ catch {
4289
+ // Ignore callback cleanup failures.
4290
+ }
4291
+ }
4292
+ pageWindow._highlightCleanupFunctions = [];
4293
+ const containers = document.querySelectorAll('#playwright-highlight-container');
4294
+ containers.forEach((element) => element.remove());
4295
+ const labels = document.querySelectorAll('.playwright-highlight-label');
4296
+ labels.forEach((element) => element.remove());
4297
+ // Backward compatibility with legacy selectors.
3485
4298
  const highlights = document.querySelectorAll('.browser-use-highlight');
3486
- highlights.forEach((el) => el.remove());
3487
- // Remove inline highlight styles
4299
+ highlights.forEach((element) => element.remove());
3488
4300
  const styled = document.querySelectorAll('[style*="browser-use"]');
3489
- styled.forEach((el) => {
3490
- if (el.style) {
3491
- el.style.outline = '';
3492
- el.style.border = '';
4301
+ styled.forEach((element) => {
4302
+ if (element.style) {
4303
+ element.style.outline = '';
4304
+ element.style.border = '';
3493
4305
  }
3494
4306
  });
3495
4307
  });