browser-use 0.4.0 → 0.5.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.
@@ -360,10 +360,17 @@ const DEFAULT_BROWSER_PROFILE_OPTIONS = {
360
360
  profile_directory: 'Default',
361
361
  cookies_file: null,
362
362
  };
363
+ const splitArgOnce = (arg) => {
364
+ const separatorIndex = arg.indexOf('=');
365
+ if (separatorIndex === -1) {
366
+ return [arg, ''];
367
+ }
368
+ return [arg.slice(0, separatorIndex), arg.slice(separatorIndex + 1)];
369
+ };
363
370
  const argsAsDict = (args) => {
364
371
  const result = {};
365
372
  for (const arg of args) {
366
- const [keyPart, valuePart = ''] = arg.split('=', 1);
373
+ const [keyPart, valuePart = ''] = splitArgOnce(arg);
367
374
  const key = keyPart.trim().replace(/^-+/, '');
368
375
  result[key] = valuePart.trim();
369
376
  }
@@ -376,9 +383,22 @@ const cloneDefaultOptions = () => JSON.parse(JSON.stringify(DEFAULT_BROWSER_PROF
376
383
  const normalizeDomainEntry = (entry) => String(entry ?? '')
377
384
  .trim()
378
385
  .toLowerCase();
386
+ const isExactHostDomainEntry = (entry) => {
387
+ if (!entry) {
388
+ return false;
389
+ }
390
+ if (entry.includes('*') || entry.includes('://') || entry.includes('/')) {
391
+ return false;
392
+ }
393
+ // Keep set optimization for plain hostnames only. Entries with ports/pattern-like
394
+ // delimiters must stay as arrays to preserve wildcard/scheme matching semantics.
395
+ return !entry.includes(':');
396
+ };
379
397
  const optimizeDomainList = (value) => {
380
398
  const cleaned = value.map(normalizeDomainEntry).filter(Boolean);
381
- if (cleaned.length >= DOMAIN_OPTIMIZATION_THRESHOLD) {
399
+ const canOptimizeToSet = cleaned.length >= DOMAIN_OPTIMIZATION_THRESHOLD &&
400
+ cleaned.every(isExactHostDomainEntry);
401
+ if (canOptimizeToSet) {
382
402
  logger.warning(`Optimizing domain list with ${cleaned.length} entries to a Set for O(1) matching`);
383
403
  return new Set(cleaned);
384
404
  }
@@ -407,16 +427,12 @@ export class BrowserProfile {
407
427
  allowed_domains: Array.isArray(init.allowed_domains)
408
428
  ? optimizeDomainList(init.allowed_domains)
409
429
  : init.allowed_domains instanceof Set
410
- ? new Set(Array.from(init.allowed_domains)
411
- .map(normalizeDomainEntry)
412
- .filter(Boolean))
430
+ ? optimizeDomainList(Array.from(init.allowed_domains))
413
431
  : defaults.allowed_domains,
414
432
  prohibited_domains: Array.isArray(init.prohibited_domains)
415
433
  ? optimizeDomainList(init.prohibited_domains)
416
434
  : init.prohibited_domains instanceof Set
417
- ? new Set(Array.from(init.prohibited_domains)
418
- .map(normalizeDomainEntry)
419
- .filter(Boolean))
435
+ ? optimizeDomainList(Array.from(init.prohibited_domains))
420
436
  : defaults.prohibited_domains,
421
437
  window_position: init.window_position ?? defaults.window_position,
422
438
  };
@@ -96,6 +96,8 @@ export declare class BrowserSession {
96
96
  get_or_create_cdp_session(page?: Page | null): Promise<any>;
97
97
  private _waitForStableNetwork;
98
98
  private _setActivePage;
99
+ private _syncCurrentTabFromPage;
100
+ private _syncTabsWithBrowserPages;
99
101
  private _captureClosedPopupMessage;
100
102
  private _getClosedPopupMessagesSnapshot;
101
103
  private _recordRecentEvent;
@@ -327,6 +329,7 @@ export declare class BrowserSession {
327
329
  private _is_new_tab_page;
328
330
  private _is_ip_address_host;
329
331
  private _get_domain_variants;
332
+ private _setEntryMatchesUrl;
330
333
  /**
331
334
  * Check if page is displaying a PDF
332
335
  */
@@ -400,6 +400,117 @@ export class BrowserSession {
400
400
  this._attachDialogHandler(page);
401
401
  this.agent_current_page = page ?? null;
402
402
  }
403
+ async _syncCurrentTabFromPage(page) {
404
+ if (!page) {
405
+ return;
406
+ }
407
+ let resolvedUrl = null;
408
+ try {
409
+ const rawUrl = page.url();
410
+ if (typeof rawUrl === 'string' && rawUrl.trim()) {
411
+ resolvedUrl = normalize_url(rawUrl);
412
+ this.currentUrl = resolvedUrl;
413
+ }
414
+ }
415
+ catch {
416
+ // Ignore transient URL read failures.
417
+ }
418
+ let resolvedTitle = null;
419
+ if (typeof page.title === 'function') {
420
+ try {
421
+ const title = await page.title();
422
+ if (typeof title === 'string' && title.trim()) {
423
+ resolvedTitle = title;
424
+ }
425
+ }
426
+ catch {
427
+ // Ignore transient title read failures.
428
+ }
429
+ }
430
+ if (!resolvedTitle) {
431
+ resolvedTitle = resolvedUrl ?? this.currentTitle ?? this.currentUrl;
432
+ }
433
+ this.currentTitle = resolvedTitle;
434
+ const currentTab = this._tabs[this.currentTabIndex];
435
+ if (currentTab) {
436
+ if (resolvedUrl) {
437
+ currentTab.url = resolvedUrl;
438
+ }
439
+ currentTab.title = resolvedTitle;
440
+ this._syncSessionManagerFromTabs();
441
+ }
442
+ }
443
+ _syncTabsWithBrowserPages() {
444
+ const pages = this.browser_context?.pages?.() ?? [];
445
+ if (!pages.length) {
446
+ return;
447
+ }
448
+ const nextTabs = [];
449
+ const nextTabPages = new Map();
450
+ const usedPageIds = new Set();
451
+ const knownPageMappings = Array.from(this.tabPages.entries());
452
+ for (const page of pages) {
453
+ this._attachDialogHandler(page ?? null);
454
+ let pageId = null;
455
+ for (const [candidateId, candidatePage] of knownPageMappings) {
456
+ if (candidatePage === page && !usedPageIds.has(candidateId)) {
457
+ pageId = candidateId;
458
+ break;
459
+ }
460
+ }
461
+ if (pageId === null) {
462
+ pageId = this._tabCounter++;
463
+ }
464
+ usedPageIds.add(pageId);
465
+ const existingTab = this._tabs.find((tab) => tab.page_id === pageId);
466
+ const tab = existingTab
467
+ ? { ...existingTab }
468
+ : this._createTabInfo({
469
+ page_id: pageId,
470
+ url: 'about:blank',
471
+ title: 'about:blank',
472
+ });
473
+ try {
474
+ const rawUrl = page.url();
475
+ if (typeof rawUrl === 'string' && rawUrl.trim()) {
476
+ tab.url = normalize_url(rawUrl);
477
+ }
478
+ }
479
+ catch {
480
+ // Keep existing tab url when page url is not readable.
481
+ }
482
+ if (!existingTab || !tab.title || tab.title === 'about:blank') {
483
+ tab.title = tab.url;
484
+ }
485
+ nextTabs.push(tab);
486
+ nextTabPages.set(pageId, page);
487
+ }
488
+ if (!nextTabs.length) {
489
+ return;
490
+ }
491
+ this._tabs = nextTabs;
492
+ this.tabPages = nextTabPages;
493
+ const activePage = this.agent_current_page && pages.includes(this.agent_current_page)
494
+ ? this.agent_current_page
495
+ : pages[0] ?? null;
496
+ if (activePage) {
497
+ const activeIndex = this._tabs.findIndex((tab) => this.tabPages.get(tab.page_id) === activePage);
498
+ if (activeIndex !== -1) {
499
+ this.currentTabIndex = activeIndex;
500
+ }
501
+ }
502
+ if (this.currentTabIndex < 0 || this.currentTabIndex >= this._tabs.length) {
503
+ this.currentTabIndex = Math.max(0, this._tabs.length - 1);
504
+ }
505
+ const activeTab = this._tabs[this.currentTabIndex] ?? null;
506
+ if (activeTab) {
507
+ this.currentUrl = activeTab.url;
508
+ this.currentTitle = activeTab.title || activeTab.url;
509
+ this.agent_current_page = this.tabPages.get(activeTab.page_id) ?? null;
510
+ this.human_current_page = this.human_current_page ?? this.agent_current_page;
511
+ }
512
+ this._syncSessionManagerFromTabs();
513
+ }
403
514
  _captureClosedPopupMessage(dialogType, message) {
404
515
  const normalizedType = String(dialogType || 'alert').trim() || 'alert';
405
516
  const normalizedMessage = String(message || '').trim();
@@ -1101,7 +1212,7 @@ export class BrowserSession {
1101
1212
  else {
1102
1213
  try {
1103
1214
  const domService = new DomService(page, this.logger);
1104
- domState = await this._withAbort(domService.get_clickable_elements(), signal);
1215
+ domState = await this._withAbort(domService.get_clickable_elements(this.browser_profile.highlight_elements, -1, this.browser_profile.viewport_expansion), signal);
1105
1216
  }
1106
1217
  catch (error) {
1107
1218
  if (this._isAbortError(error)) {
@@ -1176,6 +1287,9 @@ export class BrowserSession {
1176
1287
  }
1177
1288
  }
1178
1289
  const pendingNetworkRequests = await this._getPendingNetworkRequests(page);
1290
+ if (page) {
1291
+ await this._syncCurrentTabFromPage(page);
1292
+ }
1179
1293
  if (pageInfo &&
1180
1294
  Number.isFinite(pageInfo.viewport_width) &&
1181
1295
  Number.isFinite(pageInfo.viewport_height)) {
@@ -1225,6 +1339,7 @@ export class BrowserSession {
1225
1339
  return summary;
1226
1340
  }
1227
1341
  async get_current_page() {
1342
+ this._syncTabsWithBrowserPages();
1228
1343
  if (this.agent_current_page) {
1229
1344
  return this.agent_current_page;
1230
1345
  }
@@ -1275,6 +1390,7 @@ export class BrowserSession {
1275
1390
  this._throwIfAborted(signal);
1276
1391
  this._assert_url_allowed(url);
1277
1392
  const normalized = normalize_url(url);
1393
+ let completedUrl = normalized;
1278
1394
  const waitUntil = options.wait_until ?? 'domcontentloaded';
1279
1395
  const timeoutMs = typeof options.timeout_ms === 'number' &&
1280
1396
  Number.isFinite(options.timeout_ms)
@@ -1294,6 +1410,7 @@ export class BrowserSession {
1294
1410
  await this._withAbort(page.goto(normalized, gotoOptions), signal);
1295
1411
  const finalUrl = page.url();
1296
1412
  this._assert_url_allowed(finalUrl);
1413
+ completedUrl = normalize_url(finalUrl);
1297
1414
  await this._waitForStableNetwork(page, signal);
1298
1415
  }
1299
1416
  catch (error) {
@@ -1309,16 +1426,24 @@ export class BrowserSession {
1309
1426
  }
1310
1427
  }
1311
1428
  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;
1429
+ if (page) {
1430
+ await this._syncCurrentTabFromPage(page);
1431
+ completedUrl = this.currentUrl || completedUrl;
1432
+ }
1433
+ else {
1434
+ this.currentUrl = normalized;
1435
+ this.currentTitle = normalized;
1436
+ if (this._tabs[this.currentTabIndex]) {
1437
+ this._tabs[this.currentTabIndex].url = normalized;
1438
+ this._tabs[this.currentTabIndex].title = normalized;
1439
+ }
1440
+ this._syncSessionManagerFromTabs();
1441
+ }
1442
+ if (this.historyStack[this.historyStack.length - 1] !== completedUrl) {
1443
+ this.historyStack.push(completedUrl);
1318
1444
  }
1319
- this._syncSessionManagerFromTabs();
1320
1445
  this._setActivePage(page ?? null);
1321
- this._recordRecentEvent('navigation_completed', { url: normalized });
1446
+ this._recordRecentEvent('navigation_completed', { url: completedUrl });
1322
1447
  this.cachedBrowserState = null;
1323
1448
  return this.agent_current_page;
1324
1449
  }
@@ -1327,11 +1452,14 @@ export class BrowserSession {
1327
1452
  this._throwIfAborted(signal);
1328
1453
  this._assert_url_allowed(url);
1329
1454
  const normalized = normalize_url(url);
1455
+ let completedUrl = normalized;
1330
1456
  const waitUntil = options.wait_until ?? 'domcontentloaded';
1331
1457
  const timeoutMs = typeof options.timeout_ms === 'number' &&
1332
1458
  Number.isFinite(options.timeout_ms)
1333
1459
  ? Math.max(0, options.timeout_ms)
1334
1460
  : null;
1461
+ const previousTabIndex = this.currentTabIndex;
1462
+ const previousTab = this._tabs[this.currentTabIndex] ?? null;
1335
1463
  const newTab = this._createTabInfo({
1336
1464
  page_id: this._tabCounter++,
1337
1465
  url: normalized,
@@ -1342,11 +1470,6 @@ export class BrowserSession {
1342
1470
  this.currentUrl = normalized;
1343
1471
  this.currentTitle = normalized;
1344
1472
  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
1473
  let page = null;
1351
1474
  try {
1352
1475
  page =
@@ -1362,6 +1485,7 @@ export class BrowserSession {
1362
1485
  await this._withAbort(page.goto(normalized, gotoOptions), signal);
1363
1486
  const finalUrl = page.url();
1364
1487
  this._assert_url_allowed(finalUrl);
1488
+ completedUrl = normalize_url(finalUrl);
1365
1489
  await this._waitForStableNetwork(page, signal);
1366
1490
  }
1367
1491
  }
@@ -1369,29 +1493,82 @@ export class BrowserSession {
1369
1493
  if (this._isAbortError(error)) {
1370
1494
  throw error;
1371
1495
  }
1496
+ const message = error.message ?? 'Failed to open new tab';
1372
1497
  this._recordRecentEvent('tab_navigation_failed', {
1373
1498
  url: normalized,
1374
1499
  page_id: newTab.page_id,
1375
1500
  tab_id: newTab.tab_id,
1376
- error_message: error.message ?? 'Failed to open new tab',
1501
+ error_message: message,
1377
1502
  });
1378
- this.logger.debug(`Failed to open new tab via Playwright: ${error.message}`);
1503
+ this.logger.debug(`Failed to open new tab via Playwright: ${message}`);
1504
+ if (page?.close) {
1505
+ try {
1506
+ await page.close();
1507
+ }
1508
+ catch {
1509
+ // Ignore best-effort tab close failures during rollback.
1510
+ }
1511
+ }
1512
+ this._tabs = this._tabs.filter((tab) => tab.page_id !== newTab.page_id);
1513
+ this.tabPages.delete(newTab.page_id);
1514
+ if (this.historyStack[this.historyStack.length - 1] === normalized) {
1515
+ this.historyStack.pop();
1516
+ }
1517
+ if (this._tabs.length > 0) {
1518
+ let restoredIndex = previousTab
1519
+ ? this._tabs.findIndex((tab) => tab.page_id === previousTab.page_id)
1520
+ : -1;
1521
+ if (restoredIndex === -1) {
1522
+ restoredIndex = Math.min(previousTabIndex, this._tabs.length - 1);
1523
+ }
1524
+ this.currentTabIndex = Math.max(0, restoredIndex);
1525
+ const restoredTab = this._tabs[this.currentTabIndex];
1526
+ this.currentUrl = restoredTab.url;
1527
+ this.currentTitle = restoredTab.title;
1528
+ const restoredPage = this.tabPages.get(restoredTab.page_id) ?? null;
1529
+ this._setActivePage(restoredPage);
1530
+ await this._syncCurrentTabFromPage(restoredPage);
1531
+ }
1532
+ else {
1533
+ this.currentTabIndex = 0;
1534
+ this.currentUrl = 'about:blank';
1535
+ this.currentTitle = 'about:blank';
1536
+ this._setActivePage(null);
1537
+ }
1538
+ this._syncSessionManagerFromTabs();
1539
+ this.cachedBrowserState = null;
1540
+ throw new BrowserError(message);
1379
1541
  }
1380
1542
  this.tabPages.set(newTab.page_id, page);
1381
1543
  this._syncSessionManagerFromTabs();
1382
1544
  this._setActivePage(page);
1545
+ if (page) {
1546
+ await this._syncCurrentTabFromPage(page);
1547
+ completedUrl = this.currentUrl || completedUrl;
1548
+ }
1549
+ if (this.historyStack[this.historyStack.length - 1] === normalized) {
1550
+ this.historyStack[this.historyStack.length - 1] = completedUrl;
1551
+ }
1552
+ else if (this.historyStack[this.historyStack.length - 1] !== completedUrl) {
1553
+ this.historyStack.push(completedUrl);
1554
+ }
1383
1555
  this.currentPageLoadingStatus = null;
1384
1556
  if (!this.human_current_page) {
1385
1557
  this.human_current_page = page;
1386
1558
  }
1559
+ this._recordRecentEvent('tab_created', {
1560
+ url: completedUrl,
1561
+ page_id: newTab.page_id,
1562
+ tab_id: newTab.tab_id,
1563
+ });
1387
1564
  this._recordRecentEvent('tab_ready', {
1388
- url: normalized,
1565
+ url: completedUrl,
1389
1566
  page_id: newTab.page_id,
1390
1567
  tab_id: newTab.tab_id,
1391
1568
  });
1392
1569
  await this.event_bus.dispatch(new TabCreatedEvent({
1393
1570
  target_id: newTab.target_id ?? newTab.tab_id ?? 'unknown_target',
1394
- url: normalized,
1571
+ url: completedUrl,
1395
1572
  }));
1396
1573
  this.cachedBrowserState = null;
1397
1574
  return this.agent_current_page;
@@ -1434,6 +1611,7 @@ export class BrowserSession {
1434
1611
  async switch_to_tab(identifier, options = {}) {
1435
1612
  const signal = options.signal ?? null;
1436
1613
  this._throwIfAborted(signal);
1614
+ this._syncTabsWithBrowserPages();
1437
1615
  const index = this._resolveTabIndex(identifier);
1438
1616
  const tab = index >= 0 ? (this._tabs[index] ?? null) : null;
1439
1617
  if (!tab) {
@@ -1473,6 +1651,7 @@ export class BrowserSession {
1473
1651
  return page;
1474
1652
  }
1475
1653
  async close_tab(identifier) {
1654
+ this._syncTabsWithBrowserPages();
1476
1655
  const index = this._resolveTabIndex(identifier);
1477
1656
  if (index < 0 || index >= this._tabs.length) {
1478
1657
  throw new Error(`Tab '${identifier}' does not exist`);
@@ -1887,31 +2066,34 @@ export class BrowserSession {
1887
2066
  async go_back(options = {}) {
1888
2067
  const signal = options.signal ?? null;
1889
2068
  this._throwIfAborted(signal);
1890
- if (this.historyStack.length <= 1) {
2069
+ const page = await this._withAbort(this.get_current_page(), signal);
2070
+ if (!page?.goBack) {
1891
2071
  return;
1892
2072
  }
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}`);
2073
+ const previousUrl = this.currentUrl;
2074
+ try {
2075
+ await this._withAbort(page.goBack(), signal);
2076
+ }
2077
+ catch (error) {
2078
+ if (this._isAbortError(error)) {
2079
+ throw error;
1903
2080
  }
2081
+ this.logger.debug(`Failed to navigate back: ${error.message}`);
1904
2082
  }
1905
2083
  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;
2084
+ await this._syncCurrentTabFromPage(page);
2085
+ const currentUrl = this.currentUrl;
2086
+ if (currentUrl && currentUrl !== previousUrl) {
2087
+ const existingIndex = this.historyStack.lastIndexOf(currentUrl);
2088
+ if (existingIndex !== -1) {
2089
+ this.historyStack = this.historyStack.slice(0, existingIndex + 1);
2090
+ }
2091
+ else if (this.historyStack[this.historyStack.length - 1] !== currentUrl) {
2092
+ this.historyStack.push(currentUrl);
2093
+ }
1913
2094
  }
1914
- this._recordRecentEvent('navigation_back', { url: previous });
2095
+ this.cachedBrowserState = null;
2096
+ this._recordRecentEvent('navigation_back', { url: currentUrl });
1915
2097
  }
1916
2098
  async get_dom_element_by_index(_index, options = {}) {
1917
2099
  const selectorMap = await this.get_selector_map(options);
@@ -2123,6 +2305,13 @@ export class BrowserSession {
2123
2305
  await performClick();
2124
2306
  }
2125
2307
  await this._waitForLoad(page, 5000, signal);
2308
+ if (page) {
2309
+ await this._syncCurrentTabFromPage(page);
2310
+ if (this.historyStack[this.historyStack.length - 1] !== this.currentUrl) {
2311
+ this.historyStack.push(this.currentUrl);
2312
+ }
2313
+ }
2314
+ this.cachedBrowserState = null;
2126
2315
  return null;
2127
2316
  }
2128
2317
  async _waitForLoad(page, timeout = 5000, signal = null) {
@@ -2471,7 +2660,7 @@ export class BrowserSession {
2471
2660
  cdp_session = await this.get_or_create_cdp_session(page);
2472
2661
  // Capture screenshot via CDP
2473
2662
  const screenshot_response = await cdp_session.send('Page.captureScreenshot', {
2474
- captureBeyondViewport: false,
2663
+ captureBeyondViewport: full_page,
2475
2664
  fromSurface: true,
2476
2665
  format: 'png',
2477
2666
  });
@@ -2548,22 +2737,33 @@ export class BrowserSession {
2548
2737
  if (!this.browser_context) {
2549
2738
  return [];
2550
2739
  }
2740
+ this._syncTabsWithBrowserPages();
2551
2741
  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);
2742
+ for (const tab of this._tabs) {
2743
+ const page_id = tab.page_id;
2744
+ const page = this.tabPages.get(page_id) ?? null;
2745
+ const tab_id = tab.tab_id || this._formatTabId(page_id);
2746
+ if (!tab.tab_id) {
2747
+ tab.tab_id = tab_id;
2748
+ }
2749
+ this._attachDialogHandler(page);
2750
+ let currentUrl = tab.url;
2751
+ if (page?.url) {
2752
+ try {
2753
+ currentUrl = normalize_url(page.url());
2754
+ }
2755
+ catch {
2756
+ // Keep tab url fallback when page url is unavailable.
2757
+ }
2758
+ }
2558
2759
  // 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://')) {
2760
+ const isNewTab = currentUrl === 'about:blank' || currentUrl.startsWith('chrome://newtab');
2761
+ if (isNewTab || currentUrl.startsWith('chrome://')) {
2562
2762
  if (isNewTab) {
2563
2763
  tabs_info.push({
2564
2764
  page_id,
2565
2765
  tab_id,
2566
- url: page.url(),
2766
+ url: currentUrl,
2567
2767
  title: 'ignore this tab and do not use it',
2568
2768
  });
2569
2769
  }
@@ -2571,28 +2771,31 @@ export class BrowserSession {
2571
2771
  tabs_info.push({
2572
2772
  page_id,
2573
2773
  tab_id,
2574
- url: page.url(),
2575
- title: page.url(),
2774
+ url: currentUrl,
2775
+ title: currentUrl,
2576
2776
  });
2577
2777
  }
2578
2778
  continue;
2579
2779
  }
2580
2780
  // Normal pages - try to get title with timeout
2581
2781
  try {
2782
+ if (!page?.title) {
2783
+ throw new Error('page_title_unavailable');
2784
+ }
2582
2785
  const titlePromise = page.title();
2583
2786
  const timeoutPromise = new Promise((_, reject) => {
2584
2787
  setTimeout(() => reject(new Error('timeout')), 2000);
2585
2788
  });
2586
2789
  const title = await Promise.race([titlePromise, timeoutPromise]);
2587
- tabs_info.push({ page_id, tab_id, url: page.url(), title });
2790
+ tabs_info.push({ page_id, tab_id, url: currentUrl, title });
2588
2791
  }
2589
2792
  catch (error) {
2590
- this.logger.debug(`⚠️ Failed to get tab info for tab #${page_id}: ${page.url()} (using fallback title)`);
2793
+ this.logger.debug(`⚠️ Failed to get tab info for tab #${page_id}: ${currentUrl} (using fallback title)`);
2591
2794
  if (isNewTab) {
2592
2795
  tabs_info.push({
2593
2796
  page_id,
2594
2797
  tab_id,
2595
- url: page.url(),
2798
+ url: currentUrl,
2596
2799
  title: 'ignore this tab and do not use it',
2597
2800
  });
2598
2801
  }
@@ -2600,8 +2803,8 @@ export class BrowserSession {
2600
2803
  tabs_info.push({
2601
2804
  page_id,
2602
2805
  tab_id,
2603
- url: page.url(),
2604
- title: page.url(), // Use URL as fallback title
2806
+ url: currentUrl,
2807
+ title: tab.title || currentUrl,
2605
2808
  });
2606
2809
  }
2607
2810
  }
@@ -2966,6 +3169,15 @@ export class BrowserSession {
2966
3169
  }
2967
3170
  return [host, `www.${host}`];
2968
3171
  }
3172
+ _setEntryMatchesUrl(domains, hostVariant, hostAlt, protocol) {
3173
+ const matchedHost = domains.has(hostVariant) || domains.has(hostAlt);
3174
+ if (!matchedHost) {
3175
+ return false;
3176
+ }
3177
+ // Set-optimized entries are exact hostnames without explicit schemes,
3178
+ // so keep parity with pattern matching default: https-only.
3179
+ return protocol.toLowerCase() === 'https:';
3180
+ }
2969
3181
  /**
2970
3182
  * Check if page is displaying a PDF
2971
3183
  */
@@ -3258,7 +3470,7 @@ export class BrowserSession {
3258
3470
  ((Array.isArray(allowedDomains) && allowedDomains.length > 0) ||
3259
3471
  (allowedDomains instanceof Set && allowedDomains.size > 0))) {
3260
3472
  if (allowedDomains instanceof Set) {
3261
- if (allowedDomains.has(hostVariant) || allowedDomains.has(hostAlt)) {
3473
+ if (this._setEntryMatchesUrl(allowedDomains, hostVariant, hostAlt, parsed.protocol)) {
3262
3474
  return null;
3263
3475
  }
3264
3476
  }
@@ -3281,8 +3493,7 @@ export class BrowserSession {
3281
3493
  ((Array.isArray(prohibitedDomains) && prohibitedDomains.length > 0) ||
3282
3494
  (prohibitedDomains instanceof Set && prohibitedDomains.size > 0))) {
3283
3495
  if (prohibitedDomains instanceof Set) {
3284
- if (prohibitedDomains.has(hostVariant) ||
3285
- prohibitedDomains.has(hostAlt)) {
3496
+ if (this._setEntryMatchesUrl(prohibitedDomains, hostVariant, hostAlt, parsed.protocol)) {
3286
3497
  return 'in_prohibited_domains';
3287
3498
  }
3288
3499
  }
@@ -3405,57 +3616,30 @@ export class BrowserSession {
3405
3616
  // Check if downloads are enabled
3406
3617
  const downloads_path = this.browser_profile.downloads_path;
3407
3618
  if (downloads_path) {
3619
+ fs.mkdirSync(downloads_path, { recursive: true });
3620
+ // Try to detect file download.
3621
+ const download_promise = page.waitForEvent('download', {
3622
+ timeout: 5000,
3623
+ });
3624
+ // Click failures should bubble to the caller.
3408
3625
  try {
3409
- // Try to detect file download
3410
- const download_promise = page.waitForEvent('download', {
3411
- timeout: 5000,
3412
- });
3413
- // Perform the click
3414
3626
  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
3627
  }
3457
3628
  catch (error) {
3458
- // No download triggered, treat as normal click
3629
+ void download_promise.catch(() => undefined);
3630
+ throw error;
3631
+ }
3632
+ let download;
3633
+ try {
3634
+ download = await download_promise;
3635
+ }
3636
+ catch (error) {
3637
+ const message = error instanceof Error ? error.message : String(error);
3638
+ const isDownloadTimeout = error instanceof Error &&
3639
+ (error.name === 'TimeoutError' || message.toLowerCase().includes('timeout'));
3640
+ if (!isDownloadTimeout) {
3641
+ throw error;
3642
+ }
3459
3643
  this.logger.debug('No download triggered within timeout. Checking navigation...');
3460
3644
  try {
3461
3645
  await page.waitForLoadState();
@@ -3463,7 +3647,45 @@ export class BrowserSession {
3463
3647
  catch (e) {
3464
3648
  this.logger.warning(`Navigation check failed: ${e.message}`);
3465
3649
  }
3650
+ return null;
3651
+ }
3652
+ // Save the downloaded file.
3653
+ const suggested_filename = download.suggestedFilename();
3654
+ const unique_filename = await BrowserSession.get_unique_filename(downloads_path, suggested_filename);
3655
+ const download_path = path.join(downloads_path, unique_filename);
3656
+ const download_guid = uuid7str();
3657
+ const download_url = typeof download.url === 'function' ? download.url() : (this.currentUrl ?? '');
3658
+ await this.event_bus.dispatch(new DownloadStartedEvent({
3659
+ guid: download_guid,
3660
+ url: download_url,
3661
+ suggested_filename,
3662
+ auto_download: false,
3663
+ }));
3664
+ await download.saveAs(download_path);
3665
+ this.logger.info(`⬇️ Downloaded file to: ${download_path}`);
3666
+ const stats = fs.existsSync(download_path)
3667
+ ? fs.statSync(download_path)
3668
+ : null;
3669
+ await this.event_bus.dispatch(new DownloadProgressEvent({
3670
+ guid: download_guid,
3671
+ received_bytes: stats?.size ?? 0,
3672
+ total_bytes: stats?.size ?? 0,
3673
+ state: 'completed',
3674
+ }));
3675
+ const fileDownloadedResult = await this.event_bus.dispatch(new FileDownloadedEvent({
3676
+ guid: download_guid,
3677
+ url: download_url,
3678
+ path: download_path,
3679
+ file_name: unique_filename,
3680
+ file_size: stats?.size ?? 0,
3681
+ file_type: path.extname(unique_filename).replace('.', '') || null,
3682
+ mime_type: null,
3683
+ auto_download: false,
3684
+ }));
3685
+ if (fileDownloadedResult.handler_results.length === 0) {
3686
+ this.add_downloaded_file(download_path);
3466
3687
  }
3688
+ return download_path;
3467
3689
  }
3468
3690
  else {
3469
3691
  // No downloads path configured, just click
@@ -3481,15 +3703,33 @@ export class BrowserSession {
3481
3703
  }
3482
3704
  try {
3483
3705
  await page.evaluate(() => {
3484
- // Remove all elements with browser-use highlight class
3706
+ const pageWindow = window;
3707
+ const cleanupFunctions = Array.isArray(pageWindow._highlightCleanupFunctions)
3708
+ ? pageWindow._highlightCleanupFunctions
3709
+ : [];
3710
+ for (const cleanupFn of cleanupFunctions) {
3711
+ try {
3712
+ if (typeof cleanupFn === 'function') {
3713
+ cleanupFn();
3714
+ }
3715
+ }
3716
+ catch {
3717
+ // Ignore callback cleanup failures.
3718
+ }
3719
+ }
3720
+ pageWindow._highlightCleanupFunctions = [];
3721
+ const containers = document.querySelectorAll('#playwright-highlight-container');
3722
+ containers.forEach((element) => element.remove());
3723
+ const labels = document.querySelectorAll('.playwright-highlight-label');
3724
+ labels.forEach((element) => element.remove());
3725
+ // Backward compatibility with legacy selectors.
3485
3726
  const highlights = document.querySelectorAll('.browser-use-highlight');
3486
- highlights.forEach((el) => el.remove());
3487
- // Remove inline highlight styles
3727
+ highlights.forEach((element) => element.remove());
3488
3728
  const styled = document.querySelectorAll('[style*="browser-use"]');
3489
- styled.forEach((el) => {
3490
- if (el.style) {
3491
- el.style.outline = '';
3492
- el.style.border = '';
3729
+ styled.forEach((element) => {
3730
+ if (element.style) {
3731
+ element.style.outline = '';
3732
+ element.style.border = '';
3493
3733
  }
3494
3734
  });
3495
3735
  });
@@ -343,17 +343,29 @@
343
343
  }
344
344
  }
345
345
 
346
- // // Add this function to perform cleanup when needed
347
- // function cleanupHighlights() {
348
- // if (window._highlightCleanupFunctions && window._highlightCleanupFunctions.length) {
349
- // window._highlightCleanupFunctions.forEach(fn => fn());
350
- // window._highlightCleanupFunctions = [];
351
- // }
352
-
353
- // // Also remove the container
354
- // const container = document.getElementById(HIGHLIGHT_CONTAINER_ID);
355
- // if (container) container.remove();
356
- // }
346
+ function cleanupHighlights() {
347
+ try {
348
+ const cleanupFns = Array.isArray(window._highlightCleanupFunctions)
349
+ ? window._highlightCleanupFunctions
350
+ : [];
351
+ for (const fn of cleanupFns) {
352
+ try {
353
+ if (typeof fn === 'function') {
354
+ fn();
355
+ }
356
+ } catch (error) {
357
+ // Ignore cleanup callback failures to keep extraction resilient.
358
+ }
359
+ }
360
+ window._highlightCleanupFunctions = [];
361
+ const container = document.getElementById(HIGHLIGHT_CONTAINER_ID);
362
+ if (container) {
363
+ container.remove();
364
+ }
365
+ } catch (error) {
366
+ // Ignore cleanup failures and continue with DOM extraction.
367
+ }
368
+ }
357
369
 
358
370
  /**
359
371
  * Gets the position of an element in its parent.
@@ -1391,6 +1403,7 @@
1391
1403
  return id;
1392
1404
  }
1393
1405
 
1406
+ cleanupHighlights();
1394
1407
  const rootId = buildDomTree(document.body);
1395
1408
 
1396
1409
  // Clear the cache before starting
package/dist/utils.d.ts CHANGED
@@ -116,7 +116,7 @@ export declare function createSemaphore(maxConcurrent: number): {
116
116
  getQueueLength(): number;
117
117
  };
118
118
  /**
119
- * Check if a URL is a new tab page (about:blank, chrome://new-tab-page, or chrome://newtab).
119
+ * Check if a URL is a new tab page (about:blank/about:newtab/chrome://new-tab-page/chrome://newtab).
120
120
  */
121
121
  export declare function is_new_tab_page(url: string): boolean;
122
122
  /**
package/dist/utils.js CHANGED
@@ -488,10 +488,11 @@ export function createSemaphore(maxConcurrent) {
488
488
  };
489
489
  }
490
490
  /**
491
- * Check if a URL is a new tab page (about:blank, chrome://new-tab-page, or chrome://newtab).
491
+ * Check if a URL is a new tab page (about:blank/about:newtab/chrome://new-tab-page/chrome://newtab).
492
492
  */
493
493
  export function is_new_tab_page(url) {
494
494
  return (url === 'about:blank' ||
495
+ url === 'about:newtab' ||
495
496
  url === 'chrome://new-tab-page/' ||
496
497
  url === 'chrome://new-tab-page' ||
497
498
  url === 'chrome://newtab/' ||
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "browser-use",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "A TypeScript-first library for programmatic browser control, designed for building AI-powered web agents.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",