browser-use 0.0.1 → 0.1.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 (200) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +761 -0
  3. package/dist/agent/cloud-events.d.ts +264 -0
  4. package/dist/agent/cloud-events.js +318 -0
  5. package/dist/agent/gif.d.ts +15 -0
  6. package/dist/agent/gif.js +215 -0
  7. package/dist/agent/index.d.ts +8 -0
  8. package/dist/agent/index.js +8 -0
  9. package/dist/agent/message-manager/service.d.ts +30 -0
  10. package/dist/agent/message-manager/service.js +208 -0
  11. package/dist/agent/message-manager/utils.d.ts +2 -0
  12. package/dist/agent/message-manager/utils.js +41 -0
  13. package/dist/agent/message-manager/views.d.ts +26 -0
  14. package/dist/agent/message-manager/views.js +73 -0
  15. package/dist/agent/prompts.d.ts +52 -0
  16. package/dist/agent/prompts.js +259 -0
  17. package/dist/agent/service.d.ts +290 -0
  18. package/dist/agent/service.js +2200 -0
  19. package/dist/agent/views.d.ts +741 -0
  20. package/dist/agent/views.js +537 -0
  21. package/dist/browser/browser.d.ts +7 -0
  22. package/dist/browser/browser.js +5 -0
  23. package/dist/browser/context.d.ts +8 -0
  24. package/dist/browser/context.js +4 -0
  25. package/dist/browser/dvd-screensaver.d.ts +101 -0
  26. package/dist/browser/dvd-screensaver.js +270 -0
  27. package/dist/browser/extensions.d.ts +63 -0
  28. package/dist/browser/extensions.js +359 -0
  29. package/dist/browser/index.d.ts +10 -0
  30. package/dist/browser/index.js +9 -0
  31. package/dist/browser/playwright-manager.d.ts +47 -0
  32. package/dist/browser/playwright-manager.js +146 -0
  33. package/dist/browser/profile.d.ts +196 -0
  34. package/dist/browser/profile.js +815 -0
  35. package/dist/browser/session.d.ts +505 -0
  36. package/dist/browser/session.js +3409 -0
  37. package/dist/browser/types.d.ts +1184 -0
  38. package/dist/browser/types.js +1 -0
  39. package/dist/browser/utils.d.ts +1 -0
  40. package/dist/browser/utils.js +19 -0
  41. package/dist/browser/views.d.ts +78 -0
  42. package/dist/browser/views.js +72 -0
  43. package/dist/cli.d.ts +2 -0
  44. package/dist/cli.js +44 -0
  45. package/dist/config.d.ts +108 -0
  46. package/dist/config.js +430 -0
  47. package/dist/controller/index.d.ts +3 -0
  48. package/dist/controller/index.js +3 -0
  49. package/dist/controller/registry/index.d.ts +2 -0
  50. package/dist/controller/registry/index.js +2 -0
  51. package/dist/controller/registry/service.d.ts +45 -0
  52. package/dist/controller/registry/service.js +184 -0
  53. package/dist/controller/registry/views.d.ts +55 -0
  54. package/dist/controller/registry/views.js +174 -0
  55. package/dist/controller/service.d.ts +49 -0
  56. package/dist/controller/service.js +1176 -0
  57. package/dist/controller/views.d.ts +241 -0
  58. package/dist/controller/views.js +88 -0
  59. package/dist/dom/clickable-element-processor/service.d.ts +11 -0
  60. package/dist/dom/clickable-element-processor/service.js +60 -0
  61. package/dist/dom/dom_tree/index.js +1400 -0
  62. package/dist/dom/history-tree-processor/service.d.ts +14 -0
  63. package/dist/dom/history-tree-processor/service.js +75 -0
  64. package/dist/dom/history-tree-processor/view.d.ts +54 -0
  65. package/dist/dom/history-tree-processor/view.js +56 -0
  66. package/dist/dom/playground/extraction.d.ts +19 -0
  67. package/dist/dom/playground/extraction.js +187 -0
  68. package/dist/dom/playground/process-dom.d.ts +1 -0
  69. package/dist/dom/playground/process-dom.js +5 -0
  70. package/dist/dom/playground/test-accessibility.d.ts +44 -0
  71. package/dist/dom/playground/test-accessibility.js +111 -0
  72. package/dist/dom/service.d.ts +19 -0
  73. package/dist/dom/service.js +227 -0
  74. package/dist/dom/utils.d.ts +1 -0
  75. package/dist/dom/utils.js +6 -0
  76. package/dist/dom/views.d.ts +61 -0
  77. package/dist/dom/views.js +247 -0
  78. package/dist/event-bus.d.ts +11 -0
  79. package/dist/event-bus.js +19 -0
  80. package/dist/exceptions.d.ts +10 -0
  81. package/dist/exceptions.js +22 -0
  82. package/dist/filesystem/file-system.d.ts +68 -0
  83. package/dist/filesystem/file-system.js +412 -0
  84. package/dist/filesystem/index.d.ts +1 -0
  85. package/dist/filesystem/index.js +1 -0
  86. package/dist/index.d.ts +31 -0
  87. package/dist/index.js +33 -0
  88. package/dist/integrations/gmail/actions.d.ts +12 -0
  89. package/dist/integrations/gmail/actions.js +113 -0
  90. package/dist/integrations/gmail/index.d.ts +2 -0
  91. package/dist/integrations/gmail/index.js +2 -0
  92. package/dist/integrations/gmail/service.d.ts +61 -0
  93. package/dist/integrations/gmail/service.js +260 -0
  94. package/dist/llm/anthropic/chat.d.ts +28 -0
  95. package/dist/llm/anthropic/chat.js +126 -0
  96. package/dist/llm/anthropic/index.d.ts +2 -0
  97. package/dist/llm/anthropic/index.js +2 -0
  98. package/dist/llm/anthropic/serializer.d.ts +68 -0
  99. package/dist/llm/anthropic/serializer.js +285 -0
  100. package/dist/llm/aws/chat-anthropic.d.ts +61 -0
  101. package/dist/llm/aws/chat-anthropic.js +176 -0
  102. package/dist/llm/aws/chat-bedrock.d.ts +15 -0
  103. package/dist/llm/aws/chat-bedrock.js +80 -0
  104. package/dist/llm/aws/index.d.ts +3 -0
  105. package/dist/llm/aws/index.js +3 -0
  106. package/dist/llm/aws/serializer.d.ts +5 -0
  107. package/dist/llm/aws/serializer.js +68 -0
  108. package/dist/llm/azure/chat.d.ts +15 -0
  109. package/dist/llm/azure/chat.js +83 -0
  110. package/dist/llm/azure/index.d.ts +1 -0
  111. package/dist/llm/azure/index.js +1 -0
  112. package/dist/llm/base.d.ts +16 -0
  113. package/dist/llm/base.js +1 -0
  114. package/dist/llm/deepseek/chat.d.ts +15 -0
  115. package/dist/llm/deepseek/chat.js +51 -0
  116. package/dist/llm/deepseek/index.d.ts +2 -0
  117. package/dist/llm/deepseek/index.js +2 -0
  118. package/dist/llm/deepseek/serializer.d.ts +6 -0
  119. package/dist/llm/deepseek/serializer.js +57 -0
  120. package/dist/llm/exceptions.d.ts +10 -0
  121. package/dist/llm/exceptions.js +18 -0
  122. package/dist/llm/google/chat.d.ts +20 -0
  123. package/dist/llm/google/chat.js +144 -0
  124. package/dist/llm/google/index.d.ts +2 -0
  125. package/dist/llm/google/index.js +2 -0
  126. package/dist/llm/google/serializer.d.ts +6 -0
  127. package/dist/llm/google/serializer.js +64 -0
  128. package/dist/llm/groq/chat.d.ts +15 -0
  129. package/dist/llm/groq/chat.js +52 -0
  130. package/dist/llm/groq/index.d.ts +3 -0
  131. package/dist/llm/groq/index.js +3 -0
  132. package/dist/llm/groq/parser.d.ts +32 -0
  133. package/dist/llm/groq/parser.js +189 -0
  134. package/dist/llm/groq/serializer.d.ts +6 -0
  135. package/dist/llm/groq/serializer.js +56 -0
  136. package/dist/llm/messages.d.ts +77 -0
  137. package/dist/llm/messages.js +157 -0
  138. package/dist/llm/ollama/chat.d.ts +15 -0
  139. package/dist/llm/ollama/chat.js +77 -0
  140. package/dist/llm/ollama/index.d.ts +2 -0
  141. package/dist/llm/ollama/index.js +2 -0
  142. package/dist/llm/ollama/serializer.d.ts +6 -0
  143. package/dist/llm/ollama/serializer.js +53 -0
  144. package/dist/llm/openai/chat.d.ts +38 -0
  145. package/dist/llm/openai/chat.js +174 -0
  146. package/dist/llm/openai/index.d.ts +3 -0
  147. package/dist/llm/openai/index.js +3 -0
  148. package/dist/llm/openai/like.d.ts +17 -0
  149. package/dist/llm/openai/like.js +19 -0
  150. package/dist/llm/openai/serializer.d.ts +6 -0
  151. package/dist/llm/openai/serializer.js +57 -0
  152. package/dist/llm/openrouter/chat.d.ts +15 -0
  153. package/dist/llm/openrouter/chat.js +74 -0
  154. package/dist/llm/openrouter/index.d.ts +2 -0
  155. package/dist/llm/openrouter/index.js +2 -0
  156. package/dist/llm/openrouter/serializer.d.ts +3 -0
  157. package/dist/llm/openrouter/serializer.js +3 -0
  158. package/dist/llm/schema.d.ts +6 -0
  159. package/dist/llm/schema.js +77 -0
  160. package/dist/llm/views.d.ts +15 -0
  161. package/dist/llm/views.js +12 -0
  162. package/dist/logging-config.d.ts +25 -0
  163. package/dist/logging-config.js +89 -0
  164. package/dist/mcp/client.d.ts +142 -0
  165. package/dist/mcp/client.js +638 -0
  166. package/dist/mcp/controller.d.ts +6 -0
  167. package/dist/mcp/controller.js +38 -0
  168. package/dist/mcp/index.d.ts +3 -0
  169. package/dist/mcp/index.js +3 -0
  170. package/dist/mcp/server.d.ts +134 -0
  171. package/dist/mcp/server.js +759 -0
  172. package/dist/observability-decorators.d.ts +158 -0
  173. package/dist/observability-decorators.js +286 -0
  174. package/dist/observability.d.ts +23 -0
  175. package/dist/observability.js +58 -0
  176. package/dist/screenshots/index.d.ts +1 -0
  177. package/dist/screenshots/index.js +1 -0
  178. package/dist/screenshots/service.d.ts +6 -0
  179. package/dist/screenshots/service.js +28 -0
  180. package/dist/sync/auth.d.ts +27 -0
  181. package/dist/sync/auth.js +205 -0
  182. package/dist/sync/index.d.ts +2 -0
  183. package/dist/sync/index.js +2 -0
  184. package/dist/sync/service.d.ts +21 -0
  185. package/dist/sync/service.js +146 -0
  186. package/dist/telemetry/index.d.ts +2 -0
  187. package/dist/telemetry/index.js +2 -0
  188. package/dist/telemetry/service.d.ts +12 -0
  189. package/dist/telemetry/service.js +85 -0
  190. package/dist/telemetry/views.d.ts +112 -0
  191. package/dist/telemetry/views.js +112 -0
  192. package/dist/tokens/index.d.ts +2 -0
  193. package/dist/tokens/index.js +2 -0
  194. package/dist/tokens/service.d.ts +35 -0
  195. package/dist/tokens/service.js +423 -0
  196. package/dist/tokens/views.d.ts +58 -0
  197. package/dist/tokens/views.js +1 -0
  198. package/dist/utils.d.ts +128 -0
  199. package/dist/utils.js +529 -0
  200. package/package.json +94 -5
@@ -0,0 +1,3409 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { exec } from 'node:child_process';
4
+ import { promisify } from 'node:util';
5
+ import { createLogger } from '../logging-config.js';
6
+ import { match_url_with_domain_pattern, uuid7str } from '../utils.js';
7
+ import { async_playwright, } from './types.js';
8
+ import { BrowserProfile, CHROME_DOCKER_ARGS, DEFAULT_BROWSER_PROFILE, } from './profile.js';
9
+ import { BrowserStateSummary, BrowserError } from './views.js';
10
+ import { DOMElementNode, DOMState } from '../dom/views.js';
11
+ import { normalize_url } from './utils.js';
12
+ import { DomService } from '../dom/service.js';
13
+ import { showDVDScreensaver, showSpinner, withDVDScreensaver, } from './dvd-screensaver.js';
14
+ const execAsync = promisify(exec);
15
+ const createEmptyDomState = () => {
16
+ const root = new DOMElementNode(true, null, 'html', '/html[1]', {}, []);
17
+ return new DOMState(root, {});
18
+ };
19
+ export class BrowserSession {
20
+ id;
21
+ browser_profile;
22
+ browser;
23
+ browser_context;
24
+ agent_current_page;
25
+ human_current_page;
26
+ initialized = false;
27
+ wss_url;
28
+ cdp_url;
29
+ browser_pid;
30
+ playwright;
31
+ cachedBrowserState = null;
32
+ _cachedClickableElementHashes = null;
33
+ currentUrl;
34
+ currentTitle;
35
+ _logger = null;
36
+ _tabCounter = 0;
37
+ _tabs = [];
38
+ currentTabIndex = 0;
39
+ historyStack = [];
40
+ downloaded_files = [];
41
+ ownsBrowserResources = true;
42
+ _autoDownloadPdfs = true;
43
+ tabPages = new Map();
44
+ currentPageLoadingStatus = null;
45
+ _subprocess = null;
46
+ _childProcesses = new Set();
47
+ attachedAgentId = null;
48
+ attachedSharedAgentIds = new Set();
49
+ _stoppingPromise = null;
50
+ constructor(init = {}) {
51
+ const sourceProfileConfig = init.browser_profile
52
+ ? typeof structuredClone === 'function'
53
+ ? structuredClone(init.browser_profile.config)
54
+ : JSON.parse(JSON.stringify(init.browser_profile.config))
55
+ : init.profile ?? {};
56
+ this.browser_profile = new BrowserProfile(sourceProfileConfig);
57
+ this.id = init.id ?? uuid7str();
58
+ this.browser = init.browser ?? null;
59
+ this.browser_context = init.browser_context ?? null;
60
+ this.agent_current_page = init.page ?? null;
61
+ this.human_current_page = init.page ?? null;
62
+ this.currentUrl = normalize_url(init.url ?? 'about:blank');
63
+ this.currentTitle = init.title ?? '';
64
+ this.wss_url = init.wss_url ?? null;
65
+ this.cdp_url = init.cdp_url ?? null;
66
+ this.browser_pid = init.browser_pid ?? null;
67
+ this.playwright = init.playwright ?? null;
68
+ this.downloaded_files = Array.isArray(init.downloaded_files)
69
+ ? [...init.downloaded_files]
70
+ : [];
71
+ if (typeof init?.auto_download_pdfs === 'boolean') {
72
+ this._autoDownloadPdfs = Boolean(init.auto_download_pdfs);
73
+ }
74
+ this._tabs = [
75
+ {
76
+ page_id: this._tabCounter++,
77
+ url: this.currentUrl,
78
+ title: this.currentTitle || this.currentUrl,
79
+ parent_page_id: null,
80
+ },
81
+ ];
82
+ this.historyStack.push(this.currentUrl);
83
+ this.ownsBrowserResources = this._determineOwnership();
84
+ this.tabPages.set(this._tabs[0].page_id, this.agent_current_page ?? null);
85
+ }
86
+ async _waitForStableNetwork(page, signal = null) {
87
+ const pendingRequests = new Set();
88
+ let lastActivity = Date.now() / 1000;
89
+ // Relevant resource types that indicate page loading progress
90
+ const relevantResourceTypes = new Set([
91
+ 'document',
92
+ 'stylesheet',
93
+ 'image',
94
+ 'font',
95
+ 'script',
96
+ 'iframe',
97
+ ]);
98
+ const ignoredResourceTypes = new Set([
99
+ 'websocket',
100
+ 'media',
101
+ 'eventsource',
102
+ 'manifest',
103
+ 'other',
104
+ ]);
105
+ // Expanded URL pattern filters - more comprehensive blocking
106
+ const ignoredUrlPatterns = [
107
+ 'analytics',
108
+ 'tracking',
109
+ 'telemetry',
110
+ 'beacon',
111
+ 'metrics',
112
+ 'doubleclick',
113
+ 'adsystem',
114
+ 'adserver',
115
+ 'advertising',
116
+ 'facebook.com/plugins',
117
+ 'platform.twitter',
118
+ 'linkedin.com/embed',
119
+ 'livechat',
120
+ 'zendesk',
121
+ 'intercom',
122
+ 'crisp.chat',
123
+ 'hotjar',
124
+ 'push-notifications',
125
+ 'onesignal',
126
+ 'pushwoosh',
127
+ 'heartbeat',
128
+ 'ping',
129
+ 'alive',
130
+ 'webrtc',
131
+ 'rtmp://',
132
+ 'wss://',
133
+ 'cloudfront.net/assets',
134
+ 'fastly.net',
135
+ ];
136
+ // Content types that should be filtered
137
+ const relevantContentTypes = new Set([
138
+ 'text/html',
139
+ 'text/css',
140
+ 'application/javascript',
141
+ 'application/x-javascript',
142
+ 'text/javascript',
143
+ 'image/png',
144
+ 'image/jpeg',
145
+ 'image/gif',
146
+ 'image/webp',
147
+ 'image/svg+xml',
148
+ 'font/woff',
149
+ 'font/woff2',
150
+ 'application/font-woff',
151
+ 'application/font-woff2',
152
+ ]);
153
+ // Streaming media content types to ignore
154
+ const streamingContentTypes = new Set([
155
+ 'video/',
156
+ 'audio/',
157
+ 'application/octet-stream',
158
+ 'application/x-mpegurl',
159
+ 'application/vnd.apple.mpegurl',
160
+ ]);
161
+ // Max response size to track (5MB)
162
+ const maxResponseSize = 5 * 1024 * 1024;
163
+ const onRequest = (request) => {
164
+ const resourceType = request.resourceType?.() ?? request.resourceType;
165
+ if (!resourceType || !relevantResourceTypes.has(resourceType)) {
166
+ return;
167
+ }
168
+ if (ignoredResourceTypes.has(resourceType)) {
169
+ return;
170
+ }
171
+ const url = request.url?.().toLowerCase?.() ?? request.url?.toLowerCase?.() ?? '';
172
+ // Filter data URLs and blob URLs
173
+ if (url.startsWith('data:') || url.startsWith('blob:')) {
174
+ return;
175
+ }
176
+ // Filter by URL patterns
177
+ if (ignoredUrlPatterns.some((pattern) => url.includes(pattern.toLowerCase()))) {
178
+ return;
179
+ }
180
+ // Filter prefetch requests
181
+ const headers = request.headers?.() ?? request.headers ?? {};
182
+ const purpose = headers['purpose'] || headers['sec-fetch-dest'];
183
+ if (purpose === 'prefetch' || headers['x-moz'] === 'prefetch') {
184
+ return;
185
+ }
186
+ pendingRequests.add(request);
187
+ lastActivity = Date.now() / 1000;
188
+ };
189
+ const onResponse = async (response) => {
190
+ const request = response.request?.() ?? response.request;
191
+ if (!pendingRequests.has(request)) {
192
+ return;
193
+ }
194
+ try {
195
+ // Check Content-Type header
196
+ const headers = response.headers?.() ?? response.headers ?? {};
197
+ const contentType = headers['content-type'] || headers['Content-Type'] || '';
198
+ // Filter streaming media
199
+ if (streamingContentTypes.has(contentType.split(';')[0].trim())) {
200
+ pendingRequests.delete(request);
201
+ return;
202
+ }
203
+ // Check if content type is relevant
204
+ const baseContentType = contentType.split(';')[0].trim();
205
+ const isRelevant = Array.from(relevantContentTypes).some((ct) => baseContentType.startsWith(ct) || ct.startsWith(baseContentType));
206
+ if (contentType && !isRelevant) {
207
+ // Unknown content type, still track but log it
208
+ this.logger.debug(`Tracking unknown content type: ${baseContentType}`);
209
+ }
210
+ // Check response size (if available)
211
+ const contentLength = headers['content-length'] || headers['Content-Length'];
212
+ if (contentLength && parseInt(contentLength, 10) > maxResponseSize) {
213
+ this.logger.debug(`Skipping large response (${contentLength} bytes): ${request.url?.().substring?.(0, 50) ?? ''}`);
214
+ pendingRequests.delete(request);
215
+ return;
216
+ }
217
+ }
218
+ catch (error) {
219
+ // If header inspection fails, still process the response
220
+ this.logger.debug(`Error inspecting response headers: ${error.message}`);
221
+ }
222
+ pendingRequests.delete(request);
223
+ lastActivity = Date.now() / 1000;
224
+ };
225
+ const waitForIdle = async () => {
226
+ const startTime = Date.now() / 1000;
227
+ while (true) {
228
+ this._throwIfAborted(signal);
229
+ await this._waitWithAbort(100, signal);
230
+ this._throwIfAborted(signal);
231
+ const now = Date.now() / 1000;
232
+ if (pendingRequests.size === 0 &&
233
+ now - lastActivity >=
234
+ (this.browser_profile.wait_for_network_idle_page_load_time ?? 0.5)) {
235
+ this.currentPageLoadingStatus = null;
236
+ break;
237
+ }
238
+ if (now - startTime >
239
+ (this.browser_profile.maximum_wait_page_load_time ?? 5)) {
240
+ this.currentPageLoadingStatus = `Page loading was aborted after ${this.browser_profile.maximum_wait_page_load_time ?? 5}s with ${pendingRequests.size} pending network requests. You may want to use the wait action to allow more time for the page to fully load.`;
241
+ break;
242
+ }
243
+ }
244
+ };
245
+ if (typeof page?.on === 'function' && typeof page?.off === 'function') {
246
+ page.on('request', onRequest);
247
+ page.on('response', onResponse);
248
+ try {
249
+ await waitForIdle();
250
+ }
251
+ finally {
252
+ page.off('request', onRequest);
253
+ page.off('response', onResponse);
254
+ }
255
+ }
256
+ else {
257
+ this.currentPageLoadingStatus = null;
258
+ }
259
+ }
260
+ _setActivePage(page) {
261
+ const currentTab = this._tabs[this.currentTabIndex];
262
+ if (currentTab) {
263
+ this.tabPages.set(currentTab.page_id, page ?? null);
264
+ }
265
+ this.agent_current_page = page ?? null;
266
+ }
267
+ get tabs() {
268
+ return this._tabs.slice();
269
+ }
270
+ get active_tab_index() {
271
+ return this.currentTabIndex;
272
+ }
273
+ get active_tab() {
274
+ return this._tabs[this.currentTabIndex] ?? null;
275
+ }
276
+ describe() {
277
+ return this.toString();
278
+ }
279
+ get _owns_browser_resources() {
280
+ return this.ownsBrowserResources;
281
+ }
282
+ claim_agent(agentId, mode = 'exclusive') {
283
+ if (!agentId) {
284
+ return false;
285
+ }
286
+ if (mode === 'shared') {
287
+ if (this.attachedAgentId &&
288
+ this.attachedAgentId !== agentId &&
289
+ this.attachedSharedAgentIds.size === 0) {
290
+ return false;
291
+ }
292
+ if (this.attachedSharedAgentIds.size === 0 && this.attachedAgentId) {
293
+ this.attachedSharedAgentIds.add(this.attachedAgentId);
294
+ }
295
+ this.attachedSharedAgentIds.add(agentId);
296
+ this.attachedAgentId = this.attachedAgentId ?? agentId;
297
+ return true;
298
+ }
299
+ if (this.attachedSharedAgentIds.size > 0) {
300
+ if (this.attachedSharedAgentIds.size === 1 &&
301
+ this.attachedSharedAgentIds.has(agentId)) {
302
+ this.attachedSharedAgentIds.clear();
303
+ this.attachedAgentId = agentId;
304
+ return true;
305
+ }
306
+ return false;
307
+ }
308
+ if (this.attachedAgentId && this.attachedAgentId !== agentId) {
309
+ return false;
310
+ }
311
+ this.attachedAgentId = agentId;
312
+ return true;
313
+ }
314
+ claimAgent(agentId, mode = 'exclusive') {
315
+ return this.claim_agent(agentId, mode);
316
+ }
317
+ release_agent(agentId) {
318
+ if (this.attachedSharedAgentIds.size > 0) {
319
+ if (!agentId) {
320
+ this.attachedSharedAgentIds.clear();
321
+ this.attachedAgentId = null;
322
+ return true;
323
+ }
324
+ if (!this.attachedSharedAgentIds.has(agentId)) {
325
+ return false;
326
+ }
327
+ this.attachedSharedAgentIds.delete(agentId);
328
+ if (this.attachedSharedAgentIds.size === 0) {
329
+ this.attachedAgentId = null;
330
+ }
331
+ else if (this.attachedAgentId === agentId) {
332
+ const [nextOwner] = this.attachedSharedAgentIds;
333
+ this.attachedAgentId = nextOwner ?? null;
334
+ }
335
+ return true;
336
+ }
337
+ if (!this.attachedAgentId) {
338
+ return true;
339
+ }
340
+ if (agentId && this.attachedAgentId !== agentId) {
341
+ return false;
342
+ }
343
+ this.attachedAgentId = null;
344
+ return true;
345
+ }
346
+ releaseAgent(agentId) {
347
+ return this.release_agent(agentId);
348
+ }
349
+ get_attached_agent_id() {
350
+ return this.attachedAgentId;
351
+ }
352
+ getAttachedAgentId() {
353
+ return this.get_attached_agent_id();
354
+ }
355
+ get_attached_agent_ids() {
356
+ if (this.attachedSharedAgentIds.size > 0) {
357
+ return Array.from(this.attachedSharedAgentIds);
358
+ }
359
+ return this.attachedAgentId ? [this.attachedAgentId] : [];
360
+ }
361
+ getAttachedAgentIds() {
362
+ return this.get_attached_agent_ids();
363
+ }
364
+ _determineOwnership() {
365
+ if (this.cdp_url || this.wss_url || this.browser || this.browser_context) {
366
+ return false;
367
+ }
368
+ return true;
369
+ }
370
+ _createAbortError(reason) {
371
+ if (reason instanceof Error) {
372
+ return reason;
373
+ }
374
+ const error = new Error('Operation aborted');
375
+ error.name = 'AbortError';
376
+ return error;
377
+ }
378
+ _isAbortError(error) {
379
+ if (!(error instanceof Error)) {
380
+ return false;
381
+ }
382
+ return (error.name === 'AbortError' ||
383
+ /abort|aborted|interrupted/i.test(error.message));
384
+ }
385
+ _throwIfAborted(signal = null) {
386
+ if (signal?.aborted) {
387
+ throw this._createAbortError(signal.reason);
388
+ }
389
+ }
390
+ async _waitWithAbort(timeoutMs, signal = null) {
391
+ if (timeoutMs <= 0) {
392
+ this._throwIfAborted(signal);
393
+ return;
394
+ }
395
+ await new Promise((resolve, reject) => {
396
+ const timeout = setTimeout(() => {
397
+ cleanup();
398
+ resolve();
399
+ }, timeoutMs);
400
+ const onAbort = () => {
401
+ clearTimeout(timeout);
402
+ cleanup();
403
+ reject(this._createAbortError(signal?.reason));
404
+ };
405
+ const cleanup = () => {
406
+ signal?.removeEventListener('abort', onAbort);
407
+ };
408
+ if (signal) {
409
+ if (signal.aborted) {
410
+ onAbort();
411
+ return;
412
+ }
413
+ signal.addEventListener('abort', onAbort, { once: true });
414
+ }
415
+ });
416
+ }
417
+ async _withAbort(promise, signal = null) {
418
+ if (!signal) {
419
+ return promise;
420
+ }
421
+ this._throwIfAborted(signal);
422
+ return await new Promise((resolve, reject) => {
423
+ const onAbort = () => {
424
+ cleanup();
425
+ reject(this._createAbortError(signal.reason));
426
+ };
427
+ const cleanup = () => {
428
+ signal.removeEventListener('abort', onAbort);
429
+ };
430
+ signal.addEventListener('abort', onAbort, { once: true });
431
+ promise
432
+ .then((result) => {
433
+ cleanup();
434
+ resolve(result);
435
+ })
436
+ .catch((error) => {
437
+ cleanup();
438
+ reject(error);
439
+ });
440
+ });
441
+ }
442
+ _toPlaywrightOptions(value) {
443
+ if (value === null || value === undefined) {
444
+ return undefined;
445
+ }
446
+ if (Array.isArray(value)) {
447
+ const converted = value
448
+ .map((item) => this._toPlaywrightOptions(item))
449
+ .filter((item) => item !== undefined);
450
+ return converted;
451
+ }
452
+ if (typeof value !== 'object' ||
453
+ value instanceof Date ||
454
+ Buffer.isBuffer(value)) {
455
+ return value;
456
+ }
457
+ const result = {};
458
+ for (const [rawKey, rawVal] of Object.entries(value)) {
459
+ const convertedValue = this._toPlaywrightOptions(rawVal);
460
+ if (convertedValue === undefined) {
461
+ continue;
462
+ }
463
+ const normalizedKey = rawKey.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
464
+ result[normalizedKey] = convertedValue;
465
+ }
466
+ return result;
467
+ }
468
+ _isSandboxLaunchError(error) {
469
+ const message = error instanceof Error ? error.message : String(error);
470
+ return (/no usable sandbox/i.test(message) ||
471
+ /chromium sandboxing failed/i.test(message) ||
472
+ /zygote_host_impl_linux\.cc/i.test(message));
473
+ }
474
+ _createNoSandboxLaunchOptions(launchOptions) {
475
+ const rawArgs = Array.isArray(launchOptions.args)
476
+ ? launchOptions.args.filter((arg) => typeof arg === 'string')
477
+ : [];
478
+ const mergedArgs = [...rawArgs];
479
+ for (const arg of CHROME_DOCKER_ARGS) {
480
+ if (!mergedArgs.includes(arg)) {
481
+ mergedArgs.push(arg);
482
+ }
483
+ }
484
+ return {
485
+ ...launchOptions,
486
+ chromiumSandbox: false,
487
+ args: mergedArgs,
488
+ };
489
+ }
490
+ async _launchChromiumWithSandboxFallback(playwright, launchOptions) {
491
+ try {
492
+ return await playwright.chromium.launch(launchOptions);
493
+ }
494
+ catch (error) {
495
+ const sandboxEnabled = this.browser_profile.config.chromium_sandbox;
496
+ if (!sandboxEnabled ||
497
+ !this._isSandboxLaunchError(error)) {
498
+ throw error;
499
+ }
500
+ this.logger.warning('Chromium sandbox is unavailable in this environment. Retrying launch with chromium_sandbox=false (--no-sandbox).');
501
+ const fallbackOptions = this._createNoSandboxLaunchOptions(launchOptions);
502
+ return await playwright.chromium.launch(fallbackOptions);
503
+ }
504
+ }
505
+ _connectionDescriptor() {
506
+ const source = this.cdp_url ||
507
+ this.wss_url ||
508
+ (this.browser_pid ? String(this.browser_pid) : 'playwright');
509
+ const tail = source.split('/').pop() ?? source;
510
+ const port = tail.includes(':') ? tail.split(':').pop() : tail;
511
+ return `${this.id.slice(-4)}:${port}`;
512
+ }
513
+ toString() {
514
+ const ownershipFlag = this.ownsBrowserResources ? '#' : '©';
515
+ return `BrowserSession🆂 ${this._connectionDescriptor()} ${ownershipFlag}${String(this.id).slice(-2)}`;
516
+ }
517
+ get logger() {
518
+ if (!this._logger) {
519
+ this._logger = createLogger(`browser_use.browser.session.${this.id.slice(-4)}`);
520
+ }
521
+ return this._logger;
522
+ }
523
+ async start() {
524
+ if (this.initialized) {
525
+ return this;
526
+ }
527
+ const ensurePage = async () => {
528
+ const current = this.agent_current_page;
529
+ if (current && !current.isClosed?.()) {
530
+ this._setActivePage(current);
531
+ return;
532
+ }
533
+ const existingPages = (typeof this.browser_context?.pages === 'function'
534
+ ? this.browser_context.pages()
535
+ : []) ?? [];
536
+ const firstOpenPage = existingPages.find((page) => !page.isClosed?.()) ?? null;
537
+ if (firstOpenPage) {
538
+ this._setActivePage(firstOpenPage);
539
+ return;
540
+ }
541
+ if (typeof this.browser_context?.newPage === 'function') {
542
+ const created = await this.browser_context.newPage();
543
+ this._setActivePage(created ?? null);
544
+ return;
545
+ }
546
+ this._setActivePage(null);
547
+ };
548
+ if (!this.browser_context) {
549
+ if (!this.browser) {
550
+ const playwright = this.playwright ?? (await async_playwright());
551
+ this.playwright = playwright;
552
+ if (this.cdp_url) {
553
+ this.browser = await playwright.chromium.connectOverCDP(this.cdp_url);
554
+ this.ownsBrowserResources = false;
555
+ }
556
+ else if (this.wss_url) {
557
+ const connectOptions = this._toPlaywrightOptions(this.browser_profile.kwargs_for_connect());
558
+ this.browser = await playwright.chromium.connect(this.wss_url, connectOptions ?? {});
559
+ this.ownsBrowserResources = false;
560
+ }
561
+ else {
562
+ const launchOptions = this._toPlaywrightOptions(await this.browser_profile.kwargs_for_launch());
563
+ this.browser = await this._launchChromiumWithSandboxFallback(playwright, launchOptions ?? {});
564
+ this.ownsBrowserResources = true;
565
+ const processGetter = this.browser?.process;
566
+ if (typeof processGetter === 'function') {
567
+ const processRef = processGetter.call(this.browser);
568
+ if (typeof processRef?.pid === 'number') {
569
+ this.browser_pid = processRef.pid;
570
+ }
571
+ }
572
+ }
573
+ }
574
+ const existingContexts = (typeof this.browser?.contexts === 'function'
575
+ ? this.browser.contexts()
576
+ : []) ?? [];
577
+ if (existingContexts.length > 0) {
578
+ this.browser_context = existingContexts[0] ?? null;
579
+ }
580
+ else if (typeof this.browser?.newContext === 'function') {
581
+ const contextOptions = this._toPlaywrightOptions(this.browser_profile.kwargs_for_new_context());
582
+ this.browser_context = await this.browser.newContext(contextOptions ?? {});
583
+ }
584
+ else {
585
+ this.browser_context = null;
586
+ }
587
+ }
588
+ await ensurePage();
589
+ if (!this.human_current_page ||
590
+ this.human_current_page.isClosed?.()) {
591
+ this.human_current_page = this.agent_current_page;
592
+ }
593
+ const activePage = await this.get_current_page();
594
+ if (activePage) {
595
+ try {
596
+ this.currentUrl = normalize_url(activePage.url());
597
+ }
598
+ catch {
599
+ // Ignore url read errors from transient pages.
600
+ }
601
+ if (typeof activePage.title === 'function') {
602
+ try {
603
+ this.currentTitle = await activePage.title();
604
+ }
605
+ catch {
606
+ // Ignore title read errors from transient pages.
607
+ }
608
+ }
609
+ }
610
+ this.initialized = true;
611
+ this.logger.debug(`Started ${this.describe()} with profile ${this.browser_profile.toString()}`);
612
+ return this;
613
+ }
614
+ /**
615
+ * Setup browser session by connecting to an existing browser process via PID
616
+ * Useful for debugging or connecting to manually launched browsers
617
+ * @param browserPid - Process ID of the browser to connect to
618
+ * @param cdpUrl - Optional CDP URL (will be discovered if not provided)
619
+ */
620
+ async setupBrowserViaBrowserPid(browserPid, cdpUrl) {
621
+ this.logger.info(`Connecting to existing browser with PID ${browserPid}`);
622
+ this.browser_pid = browserPid;
623
+ // If CDP URL not provided, try to discover it
624
+ if (!cdpUrl) {
625
+ cdpUrl = (await this._discoverCdpUrl(browserPid)) ?? undefined;
626
+ }
627
+ if (!cdpUrl) {
628
+ throw new Error(`Could not discover CDP URL for browser PID ${browserPid}`);
629
+ }
630
+ this.cdp_url = cdpUrl;
631
+ this.logger.info(`Discovered CDP URL: ${cdpUrl}`);
632
+ // Connect to browser via CDP
633
+ try {
634
+ const playwright = await import('playwright');
635
+ const browser = await playwright.chromium.connectOverCDP(cdpUrl);
636
+ this.browser = browser;
637
+ this.playwright = playwright;
638
+ // Get or create context
639
+ const contexts = browser.contexts();
640
+ if (contexts.length > 0) {
641
+ this.browser_context = contexts[0];
642
+ }
643
+ else {
644
+ this.browser_context = (await browser.newContext());
645
+ }
646
+ // Get or create page
647
+ if (!this.browser_context) {
648
+ throw new Error('Browser context not available');
649
+ }
650
+ const pages = this.browser_context.pages();
651
+ if (pages.length > 0) {
652
+ this.agent_current_page = pages[0];
653
+ this.human_current_page = pages[0];
654
+ }
655
+ else {
656
+ const page = await this.browser_context.newPage();
657
+ this.agent_current_page = page;
658
+ this.human_current_page = page;
659
+ }
660
+ // We don't own this browser since we're connecting to existing one
661
+ this.ownsBrowserResources = false;
662
+ this.initialized = true;
663
+ this.logger.info(`Successfully connected to browser PID ${browserPid}`);
664
+ }
665
+ catch (error) {
666
+ throw new Error(`Failed to connect to browser PID ${browserPid}: ${error.message}`);
667
+ }
668
+ }
669
+ /**
670
+ * Discover CDP URL from browser PID
671
+ * Tries common ports and checks for debugging endpoints
672
+ */
673
+ async _discoverCdpUrl(browserPid) {
674
+ const commonPorts = [9222, 9223, 9224, 9225];
675
+ for (const port of commonPorts) {
676
+ try {
677
+ const response = await fetch(`http://localhost:${port}/json/version`);
678
+ if (response.ok) {
679
+ const data = await response.json();
680
+ if (data.webSocketDebuggerUrl) {
681
+ this.logger.debug(`Found CDP endpoint on port ${port}`);
682
+ return data.webSocketDebuggerUrl;
683
+ }
684
+ }
685
+ }
686
+ catch {
687
+ // Port not accessible, try next
688
+ continue;
689
+ }
690
+ }
691
+ this.logger.warning(`Could not discover CDP URL for PID ${browserPid} on common ports`);
692
+ return null;
693
+ }
694
+ async _shutdown_browser_session() {
695
+ this.initialized = false;
696
+ this.attachedAgentId = null;
697
+ this.attachedSharedAgentIds.clear();
698
+ const closeWithTimeout = async (label, operation, timeoutMs = 3000) => {
699
+ let timeoutHandle = null;
700
+ const timeoutPromise = new Promise((_, reject) => {
701
+ timeoutHandle = setTimeout(() => reject(new Error(`${label} timed out after ${timeoutMs}ms`)), timeoutMs);
702
+ });
703
+ try {
704
+ await Promise.race([operation, timeoutPromise]);
705
+ }
706
+ finally {
707
+ if (timeoutHandle) {
708
+ clearTimeout(timeoutHandle);
709
+ }
710
+ }
711
+ };
712
+ if (this.ownsBrowserResources) {
713
+ if (typeof this.browser_context?.close === 'function') {
714
+ try {
715
+ await closeWithTimeout('Closing browser context', this.browser_context.close());
716
+ }
717
+ catch (error) {
718
+ this.logger.debug(`Failed to close browser context: ${error.message}`);
719
+ }
720
+ }
721
+ if (typeof this.browser?.close === 'function') {
722
+ try {
723
+ await closeWithTimeout('Closing browser instance', this.browser.close());
724
+ }
725
+ catch (error) {
726
+ this.logger.debug(`Failed to close browser instance: ${error.message}`);
727
+ }
728
+ }
729
+ }
730
+ // Kill child processes first
731
+ await this._killChildProcesses();
732
+ // If we own the browser resources, terminate the browser process
733
+ if (this.ownsBrowserResources && this.browser_pid) {
734
+ await this._terminateBrowserProcess();
735
+ }
736
+ this.browser = null;
737
+ this.browser_context = null;
738
+ this.agent_current_page = null;
739
+ this.human_current_page = null;
740
+ this.browser_pid = null;
741
+ this.cdp_url = null;
742
+ this.wss_url = null;
743
+ this.playwright = null;
744
+ this.cachedBrowserState = null;
745
+ this._tabs = [];
746
+ this.downloaded_files = [];
747
+ }
748
+ async close() {
749
+ await this.stop();
750
+ }
751
+ async get_browser_state_with_recovery(options = {}) {
752
+ const signal = options.signal ?? null;
753
+ this._throwIfAborted(signal);
754
+ if (!this.initialized) {
755
+ await this._withAbort(this.start(), signal);
756
+ }
757
+ const page = await this._withAbort(this.get_current_page(), signal);
758
+ this._throwIfAborted(signal);
759
+ this.cachedBrowserState = null;
760
+ let domState;
761
+ if (!page) {
762
+ domState = createEmptyDomState();
763
+ }
764
+ else {
765
+ try {
766
+ const domService = new DomService(page, this.logger);
767
+ domState = await this._withAbort(domService.get_clickable_elements(), signal);
768
+ }
769
+ catch (error) {
770
+ if (this._isAbortError(error)) {
771
+ throw error;
772
+ }
773
+ this.logger.debug(`Failed to build DOM tree: ${error.message}`);
774
+ domState = createEmptyDomState();
775
+ }
776
+ }
777
+ let screenshot = null;
778
+ if (options.include_screenshot && page?.screenshot) {
779
+ try {
780
+ const image = await this._withAbort(page.screenshot({
781
+ type: 'png',
782
+ fullPage: true,
783
+ }), signal);
784
+ screenshot =
785
+ typeof image === 'string'
786
+ ? image
787
+ : Buffer.from(image).toString('base64');
788
+ }
789
+ catch (error) {
790
+ if (this._isAbortError(error)) {
791
+ throw error;
792
+ }
793
+ this.logger.debug(`Failed to capture screenshot: ${error.message}`);
794
+ }
795
+ }
796
+ let pageInfo = null;
797
+ let pixelsAbove = 0;
798
+ let pixelsBelow = 0;
799
+ let pixelsLeft = 0;
800
+ let pixelsRight = 0;
801
+ if (page) {
802
+ try {
803
+ const metrics = await this._withAbort(page.evaluate(() => {
804
+ const doc = document.documentElement;
805
+ const body = document.body;
806
+ const width = Math.max(doc?.scrollWidth ?? 0, body?.scrollWidth ?? 0, doc?.clientWidth ?? 0);
807
+ const height = Math.max(doc?.scrollHeight ?? 0, body?.scrollHeight ?? 0, doc?.clientHeight ?? 0);
808
+ return {
809
+ viewportWidth: window.innerWidth,
810
+ viewportHeight: window.innerHeight,
811
+ scrollX: window.scrollX,
812
+ scrollY: window.scrollY,
813
+ pageWidth: width,
814
+ pageHeight: height,
815
+ };
816
+ }), signal);
817
+ pixelsAbove = Math.max(metrics.scrollY ?? 0, 0);
818
+ const viewportHeight = metrics.viewportHeight ?? 0;
819
+ const viewportWidth = metrics.viewportWidth ?? 0;
820
+ pixelsBelow = Math.max((metrics.pageHeight ?? 0) - (metrics.scrollY + viewportHeight), 0);
821
+ pixelsLeft = Math.max(metrics.scrollX ?? 0, 0);
822
+ pixelsRight = Math.max((metrics.pageWidth ?? 0) - (metrics.scrollX + viewportWidth), 0);
823
+ pageInfo = {
824
+ viewport_width: viewportWidth,
825
+ viewport_height: viewportHeight,
826
+ page_width: metrics.pageWidth ?? viewportWidth,
827
+ page_height: metrics.pageHeight ?? viewportHeight,
828
+ scroll_x: metrics.scrollX ?? 0,
829
+ scroll_y: metrics.scrollY ?? 0,
830
+ pixels_above: pixelsAbove,
831
+ pixels_below: pixelsBelow,
832
+ pixels_left: pixelsLeft,
833
+ pixels_right: pixelsRight,
834
+ };
835
+ }
836
+ catch (error) {
837
+ if (this._isAbortError(error)) {
838
+ throw error;
839
+ }
840
+ this.logger.debug(`Failed to compute page metrics: ${error.message}`);
841
+ }
842
+ }
843
+ const summary = new BrowserStateSummary(domState, {
844
+ url: this.currentUrl,
845
+ title: this.currentTitle || this.currentUrl,
846
+ tabs: this._buildTabs(),
847
+ screenshot,
848
+ page_info: pageInfo,
849
+ pixels_above: pixelsAbove,
850
+ pixels_below: pixelsBelow,
851
+ browser_errors: this.currentPageLoadingStatus
852
+ ? [this.currentPageLoadingStatus]
853
+ : [],
854
+ is_pdf_viewer: Boolean(this.currentUrl?.toLowerCase().endsWith('.pdf')),
855
+ loading_status: this.currentPageLoadingStatus,
856
+ });
857
+ // Implement clickable element hash caching to detect new elements
858
+ if (options.cache_clickable_elements_hashes && page) {
859
+ const currentUrl = page.url();
860
+ const currentHashes = this._computeElementHashes(domState.selector_map);
861
+ // Mark new elements if we have cached hashes for this URL
862
+ if (this._cachedClickableElementHashes &&
863
+ this._cachedClickableElementHashes.url === currentUrl) {
864
+ this._markNewElements(domState.selector_map, this._cachedClickableElementHashes.hashes);
865
+ }
866
+ // Update cache with current hashes
867
+ this._cachedClickableElementHashes = {
868
+ url: currentUrl,
869
+ hashes: currentHashes,
870
+ };
871
+ }
872
+ this._throwIfAborted(signal);
873
+ this.cachedBrowserState = summary;
874
+ return summary;
875
+ }
876
+ async get_current_page() {
877
+ if (this.agent_current_page) {
878
+ return this.agent_current_page;
879
+ }
880
+ const currentTab = this._tabs[this.currentTabIndex];
881
+ if (currentTab) {
882
+ const tabPage = this.tabPages.get(currentTab.page_id) ?? null;
883
+ if (tabPage) {
884
+ this._setActivePage(tabPage);
885
+ return tabPage;
886
+ }
887
+ }
888
+ const fallback = this.browser_context?.pages()?.[0] ?? null;
889
+ this._setActivePage(fallback ?? null);
890
+ return fallback;
891
+ }
892
+ update_current_page(page, title, url) {
893
+ this._setActivePage(page);
894
+ this.human_current_page = this.human_current_page ?? page;
895
+ if (url) {
896
+ this.currentUrl = normalize_url(url);
897
+ }
898
+ if (title) {
899
+ this.currentTitle = title;
900
+ }
901
+ }
902
+ _buildTabs() {
903
+ if (!this._tabs.length) {
904
+ this._tabs.push({
905
+ page_id: this._tabCounter++,
906
+ url: this.currentUrl,
907
+ title: this.currentTitle || this.currentUrl,
908
+ parent_page_id: null,
909
+ });
910
+ }
911
+ else {
912
+ const tab = this._tabs[this.currentTabIndex];
913
+ tab.url = this.currentUrl;
914
+ tab.title = this.currentTitle || this.currentUrl;
915
+ }
916
+ return this._tabs.slice();
917
+ }
918
+ async navigate_to(url, options = {}) {
919
+ const signal = options.signal ?? null;
920
+ this._throwIfAborted(signal);
921
+ const normalized = normalize_url(url);
922
+ const page = await this._withAbort(this.get_current_page(), signal);
923
+ if (page?.goto) {
924
+ try {
925
+ this.currentPageLoadingStatus = null;
926
+ await this._withAbort(page.goto(normalized, { waitUntil: 'domcontentloaded' }), signal);
927
+ await this._waitForStableNetwork(page, signal);
928
+ }
929
+ catch (error) {
930
+ if (this._isAbortError(error)) {
931
+ throw error;
932
+ }
933
+ const message = error.message ?? 'Navigation failed';
934
+ throw new BrowserError(message);
935
+ }
936
+ }
937
+ this._throwIfAborted(signal);
938
+ this.currentUrl = normalized;
939
+ this.currentTitle = normalized;
940
+ this.historyStack.push(normalized);
941
+ if (this._tabs[this.currentTabIndex]) {
942
+ this._tabs[this.currentTabIndex].url = normalized;
943
+ this._tabs[this.currentTabIndex].title = normalized;
944
+ }
945
+ this._setActivePage(page ?? null);
946
+ this.cachedBrowserState = null;
947
+ return this.agent_current_page;
948
+ }
949
+ async create_new_tab(url, options = {}) {
950
+ const signal = options.signal ?? null;
951
+ this._throwIfAborted(signal);
952
+ const normalized = normalize_url(url);
953
+ const newTab = {
954
+ page_id: this._tabCounter++,
955
+ url: normalized,
956
+ title: normalized,
957
+ parent_page_id: null,
958
+ };
959
+ this._tabs.push(newTab);
960
+ this.currentTabIndex = this._tabs.length - 1;
961
+ this.currentUrl = normalized;
962
+ this.currentTitle = normalized;
963
+ this.historyStack.push(normalized);
964
+ let page = null;
965
+ try {
966
+ page =
967
+ (await this._withAbort(this.browser_context?.newPage?.() ?? Promise.resolve(null), signal)) ?? null;
968
+ if (page) {
969
+ this.currentPageLoadingStatus = null;
970
+ await this._withAbort(page.goto(normalized, { waitUntil: 'domcontentloaded' }), signal);
971
+ await this._waitForStableNetwork(page, signal);
972
+ }
973
+ }
974
+ catch (error) {
975
+ if (this._isAbortError(error)) {
976
+ throw error;
977
+ }
978
+ this.logger.debug(`Failed to open new tab via Playwright: ${error.message}`);
979
+ }
980
+ this.tabPages.set(newTab.page_id, page);
981
+ this._setActivePage(page);
982
+ this.currentPageLoadingStatus = null;
983
+ if (!this.human_current_page) {
984
+ this.human_current_page = page;
985
+ }
986
+ this.cachedBrowserState = null;
987
+ return this.agent_current_page;
988
+ }
989
+ _resolveTabIndex(identifier) {
990
+ if (identifier === -1) {
991
+ return Math.max(0, this._tabs.length - 1);
992
+ }
993
+ const byId = this._tabs.findIndex((tab) => tab.page_id === identifier);
994
+ if (byId !== -1) {
995
+ return byId;
996
+ }
997
+ if (identifier >= 0 && identifier < this._tabs.length) {
998
+ return identifier;
999
+ }
1000
+ return -1;
1001
+ }
1002
+ async switch_to_tab(identifier, options = {}) {
1003
+ const signal = options.signal ?? null;
1004
+ this._throwIfAborted(signal);
1005
+ const index = this._resolveTabIndex(identifier);
1006
+ const tab = index >= 0 ? (this._tabs[index] ?? null) : null;
1007
+ if (!tab) {
1008
+ throw new Error(`Tab index ${identifier} does not exist`);
1009
+ }
1010
+ this.currentTabIndex = index;
1011
+ this.currentUrl = tab.url;
1012
+ this.currentTitle = tab.title;
1013
+ const page = this.tabPages.get(tab.page_id) ?? null;
1014
+ this._setActivePage(page);
1015
+ if (page?.bringToFront) {
1016
+ try {
1017
+ await this._withAbort(page.bringToFront(), signal);
1018
+ }
1019
+ catch (error) {
1020
+ if (this._isAbortError(error)) {
1021
+ throw error;
1022
+ }
1023
+ this.logger.debug(`Failed to focus tab: ${error.message}`);
1024
+ }
1025
+ }
1026
+ await this._waitForLoad(page, 5000, signal);
1027
+ this.cachedBrowserState = null;
1028
+ return page;
1029
+ }
1030
+ async close_tab(identifier) {
1031
+ const index = this._resolveTabIndex(identifier);
1032
+ if (index < 0 || index >= this._tabs.length) {
1033
+ throw new Error(`Tab index ${identifier} does not exist`);
1034
+ }
1035
+ const closingTab = this._tabs[index];
1036
+ const closingPage = this.tabPages.get(closingTab.page_id) ?? null;
1037
+ if (closingPage?.close) {
1038
+ try {
1039
+ await closingPage.close();
1040
+ }
1041
+ catch (error) {
1042
+ this.logger.debug(`Failed to close page: ${error.message}`);
1043
+ }
1044
+ }
1045
+ this.tabPages.delete(closingTab.page_id);
1046
+ this._tabs.splice(index, 1);
1047
+ if (this.currentTabIndex >= this._tabs.length) {
1048
+ this.currentTabIndex = Math.max(0, this._tabs.length - 1);
1049
+ }
1050
+ const tab = this._tabs[this.currentTabIndex] ?? null;
1051
+ const current = tab ? (this.tabPages.get(tab.page_id) ?? null) : null;
1052
+ this._setActivePage(current);
1053
+ this.currentPageLoadingStatus = null;
1054
+ this.cachedBrowserState = null;
1055
+ if (this._tabs.length) {
1056
+ const tab = this._tabs[this.currentTabIndex];
1057
+ this.currentUrl = tab.url;
1058
+ this.currentTitle = tab.title;
1059
+ }
1060
+ else {
1061
+ this.currentUrl = 'about:blank';
1062
+ this.currentTitle = 'about:blank';
1063
+ this._setActivePage(null);
1064
+ }
1065
+ }
1066
+ async go_back(options = {}) {
1067
+ const signal = options.signal ?? null;
1068
+ this._throwIfAborted(signal);
1069
+ if (this.historyStack.length <= 1) {
1070
+ return;
1071
+ }
1072
+ const page = await this._withAbort(this.get_current_page(), signal);
1073
+ if (page?.goBack) {
1074
+ try {
1075
+ await this._withAbort(page.goBack(), signal);
1076
+ }
1077
+ catch (error) {
1078
+ if (this._isAbortError(error)) {
1079
+ throw error;
1080
+ }
1081
+ this.logger.debug(`Failed to navigate back: ${error.message}`);
1082
+ }
1083
+ }
1084
+ this._throwIfAborted(signal);
1085
+ this.historyStack.pop();
1086
+ const previous = this.historyStack[this.historyStack.length - 1];
1087
+ this.currentUrl = previous;
1088
+ this.currentTitle = previous;
1089
+ if (this._tabs[this.currentTabIndex]) {
1090
+ this._tabs[this.currentTabIndex].url = previous;
1091
+ this._tabs[this.currentTabIndex].title = previous;
1092
+ }
1093
+ }
1094
+ async get_dom_element_by_index(_index, options = {}) {
1095
+ const selectorMap = await this.get_selector_map(options);
1096
+ return selectorMap?.[_index] ?? null;
1097
+ }
1098
+ set_downloaded_files(files) {
1099
+ if (!Array.isArray(files)) {
1100
+ return;
1101
+ }
1102
+ this.downloaded_files = [...files];
1103
+ }
1104
+ add_downloaded_file(filePath) {
1105
+ if (!filePath) {
1106
+ return;
1107
+ }
1108
+ if (!this.downloaded_files.includes(filePath)) {
1109
+ this.downloaded_files = [...this.downloaded_files, filePath];
1110
+ this.logger.info(`📁 Added download to session tracking (total: ${this.downloaded_files.length} files)`);
1111
+ }
1112
+ }
1113
+ get_downloaded_files() {
1114
+ this.logger.debug(`📁 Retrieved ${this.downloaded_files.length} downloaded files from session tracking`);
1115
+ return [...this.downloaded_files];
1116
+ }
1117
+ set_auto_download_pdfs(enabled) {
1118
+ this._autoDownloadPdfs = Boolean(enabled);
1119
+ this.logger.info(`📄 PDF auto-download ${this._autoDownloadPdfs ? 'enabled' : 'disabled'}`);
1120
+ }
1121
+ auto_download_pdfs() {
1122
+ return this._autoDownloadPdfs;
1123
+ }
1124
+ static async get_unique_filename(directory, filename) {
1125
+ const resolvedDir = path.resolve(directory);
1126
+ const parsed = path.parse(filename);
1127
+ let candidate = filename;
1128
+ let counter = 1;
1129
+ while (fs.existsSync(path.join(resolvedDir, candidate))) {
1130
+ candidate = `${parsed.name} (${counter})${parsed.ext}`;
1131
+ counter += 1;
1132
+ }
1133
+ return candidate;
1134
+ }
1135
+ async get_selector_map(options = {}) {
1136
+ if (!this.cachedBrowserState) {
1137
+ await this.get_browser_state_with_recovery({
1138
+ cache_clickable_elements_hashes: true,
1139
+ include_screenshot: false,
1140
+ signal: options.signal ?? null,
1141
+ });
1142
+ }
1143
+ return this.cachedBrowserState?.selector_map ?? {};
1144
+ }
1145
+ static is_file_input(node) {
1146
+ if (!node) {
1147
+ return false;
1148
+ }
1149
+ return (node.tag_name?.toLowerCase() === 'input' &&
1150
+ (node.attributes?.type ?? '').toLowerCase() === 'file');
1151
+ }
1152
+ is_file_input(node) {
1153
+ return BrowserSession.is_file_input(node);
1154
+ }
1155
+ async find_file_upload_element_by_index(index, maxHeight = 3, maxDescendantDepth = 3, options = {}) {
1156
+ const selectorMap = await this.get_selector_map(options);
1157
+ const root = selectorMap[index];
1158
+ if (!root) {
1159
+ return null;
1160
+ }
1161
+ const findInDescendants = (node, depth) => {
1162
+ if (depth < 0) {
1163
+ return null;
1164
+ }
1165
+ if (BrowserSession.is_file_input(node)) {
1166
+ return node;
1167
+ }
1168
+ for (const child of node.children) {
1169
+ if (child instanceof DOMElementNode) {
1170
+ const found = findInDescendants(child, depth - 1);
1171
+ if (found) {
1172
+ return found;
1173
+ }
1174
+ }
1175
+ }
1176
+ return null;
1177
+ };
1178
+ let current = root;
1179
+ let remainingHeight = maxHeight;
1180
+ while (current && remainingHeight >= 0) {
1181
+ const direct = findInDescendants(current, maxDescendantDepth);
1182
+ if (direct) {
1183
+ return direct;
1184
+ }
1185
+ if (current.parent) {
1186
+ for (const sibling of current.parent.children) {
1187
+ if (sibling instanceof DOMElementNode && sibling !== current) {
1188
+ const fromSibling = findInDescendants(sibling, maxDescendantDepth);
1189
+ if (fromSibling) {
1190
+ return fromSibling;
1191
+ }
1192
+ }
1193
+ }
1194
+ }
1195
+ current = current.parent;
1196
+ remainingHeight -= 1;
1197
+ }
1198
+ return null;
1199
+ }
1200
+ async get_locate_element(node) {
1201
+ const page = await this.get_current_page();
1202
+ if (!page || !node?.xpath) {
1203
+ return null;
1204
+ }
1205
+ try {
1206
+ const locator = page.locator(`xpath=${node.xpath}`);
1207
+ const count = await locator.count();
1208
+ if (count === 0) {
1209
+ return null;
1210
+ }
1211
+ return locator;
1212
+ }
1213
+ catch (error) {
1214
+ this.logger.debug(`Failed to locate element via xpath ${node.xpath}: ${error.message}`);
1215
+ return null;
1216
+ }
1217
+ }
1218
+ async _input_text_element_node(node, text, options = {}) {
1219
+ const signal = options.signal ?? null;
1220
+ this._throwIfAborted(signal);
1221
+ const locator = await this.get_locate_element(node);
1222
+ if (!locator) {
1223
+ throw new Error('Element not found');
1224
+ }
1225
+ await this._withAbort(locator.click({ timeout: 5000 }), signal);
1226
+ await this._withAbort(locator.fill(text, { timeout: 5000 }), signal);
1227
+ }
1228
+ async _click_element_node(node, options = {}) {
1229
+ const signal = options.signal ?? null;
1230
+ this._throwIfAborted(signal);
1231
+ const locator = await this.get_locate_element(node);
1232
+ if (!locator) {
1233
+ throw new Error('Element not found');
1234
+ }
1235
+ const page = await this._withAbort(this.get_current_page(), signal);
1236
+ const performClick = async () => {
1237
+ await this._withAbort(locator.click({ timeout: 5000 }), signal);
1238
+ };
1239
+ const downloadsDir = this.browser_profile.downloads_path;
1240
+ if (downloadsDir && page?.waitForEvent) {
1241
+ const downloadPromise = page.waitForEvent('download', { timeout: 5000 });
1242
+ await performClick();
1243
+ try {
1244
+ const download = await this._withAbort(downloadPromise, signal);
1245
+ const suggested = typeof download.suggestedFilename === 'function'
1246
+ ? download.suggestedFilename()
1247
+ : 'download';
1248
+ const uniqueFilename = await BrowserSession.get_unique_filename(downloadsDir, suggested);
1249
+ const downloadPath = path.join(downloadsDir, uniqueFilename);
1250
+ if (typeof download.saveAs === 'function') {
1251
+ await download.saveAs(downloadPath);
1252
+ }
1253
+ this.add_downloaded_file(downloadPath);
1254
+ return downloadPath;
1255
+ }
1256
+ catch (error) {
1257
+ if (this._isAbortError(error)) {
1258
+ throw error;
1259
+ }
1260
+ this.logger.debug(`No download triggered within timeout: ${error.message}`);
1261
+ }
1262
+ }
1263
+ else {
1264
+ await performClick();
1265
+ }
1266
+ await this._waitForLoad(page, 5000, signal);
1267
+ return null;
1268
+ }
1269
+ async _waitForLoad(page, timeout = 5000, signal = null) {
1270
+ if (!page || typeof page.waitForLoadState !== 'function') {
1271
+ return;
1272
+ }
1273
+ try {
1274
+ await this._withAbort(page.waitForLoadState('domcontentloaded', { timeout }), signal);
1275
+ }
1276
+ catch (error) {
1277
+ if (this._isAbortError(error)) {
1278
+ throw error;
1279
+ }
1280
+ this.logger.debug(`waitForLoadState failed: ${error.message}`);
1281
+ }
1282
+ }
1283
+ // ==================== Cookie Management ====================
1284
+ /**
1285
+ * Get all cookies from the current browser context
1286
+ */
1287
+ async get_cookies() {
1288
+ if (this.browser_context?.cookies) {
1289
+ return await this.browser_context.cookies();
1290
+ }
1291
+ return [];
1292
+ }
1293
+ /**
1294
+ * Save cookies to a file (deprecated, use save_storage_state instead)
1295
+ * @deprecated Use save_storage_state() instead
1296
+ */
1297
+ async save_cookies(...args) {
1298
+ return this.save_storage_state(...args);
1299
+ }
1300
+ /**
1301
+ * Load cookies from a file (deprecated, use load_storage_state instead)
1302
+ * @deprecated Use load_storage_state() instead
1303
+ */
1304
+ async load_cookies_from_file(...args) {
1305
+ return this.load_storage_state(...args);
1306
+ }
1307
+ /**
1308
+ * Save the current storage state (cookies, localStorage, sessionStorage) to a file
1309
+ */
1310
+ async save_storage_state(filePath) {
1311
+ if (!this.browser_context) {
1312
+ this.logger.warning('Cannot save storage state: browser context not initialized');
1313
+ return;
1314
+ }
1315
+ const targetPath = filePath || this.browser_profile.cookies_file;
1316
+ if (!targetPath) {
1317
+ return;
1318
+ }
1319
+ try {
1320
+ const resolvedPath = path.resolve(targetPath);
1321
+ const dirPath = path.dirname(resolvedPath);
1322
+ // Create directory if it doesn't exist
1323
+ if (!fs.existsSync(dirPath)) {
1324
+ fs.mkdirSync(dirPath, { recursive: true });
1325
+ }
1326
+ // Get storage state from browser context
1327
+ const storageState = await this.browser_context.storageState();
1328
+ // Write to temporary file first
1329
+ const tempPath = `${resolvedPath}.tmp`;
1330
+ fs.writeFileSync(tempPath, JSON.stringify(storageState, null, 2));
1331
+ // Backup existing file if present
1332
+ if (fs.existsSync(resolvedPath)) {
1333
+ const backupPath = `${resolvedPath}.bak`;
1334
+ try {
1335
+ fs.renameSync(resolvedPath, backupPath);
1336
+ }
1337
+ catch (error) {
1338
+ // Ignore backup errors
1339
+ }
1340
+ }
1341
+ // Move temp file to target
1342
+ fs.renameSync(tempPath, resolvedPath);
1343
+ const cookieCount = storageState.cookies?.length || 0;
1344
+ this.logger.info(`🍪 Saved ${cookieCount} cookies to ${path.basename(resolvedPath)}`);
1345
+ }
1346
+ catch (error) {
1347
+ this.logger.warning(`❌ Failed to save storage state: ${error.message}`);
1348
+ }
1349
+ }
1350
+ /**
1351
+ * Load storage state (cookies, localStorage, sessionStorage) from a file
1352
+ */
1353
+ async load_storage_state(filePath) {
1354
+ const targetPath = filePath || this.browser_profile.cookies_file;
1355
+ if (!targetPath) {
1356
+ return;
1357
+ }
1358
+ try {
1359
+ const resolvedPath = path.resolve(targetPath);
1360
+ if (!fs.existsSync(resolvedPath)) {
1361
+ this.logger.warning(`Storage state file not found: ${resolvedPath}`);
1362
+ return;
1363
+ }
1364
+ const storageStateContent = fs.readFileSync(resolvedPath, 'utf-8');
1365
+ const storageState = JSON.parse(storageStateContent);
1366
+ if (this.browser_context?.addCookies) {
1367
+ // Add cookies to context
1368
+ if (storageState.cookies && Array.isArray(storageState.cookies)) {
1369
+ await this.browser_context.addCookies(storageState.cookies);
1370
+ this.logger.info(`🍪 Loaded ${storageState.cookies.length} cookies from ${path.basename(resolvedPath)}`);
1371
+ }
1372
+ }
1373
+ }
1374
+ catch (error) {
1375
+ this.logger.warning(`❌ Failed to load storage state: ${error.message}`);
1376
+ }
1377
+ }
1378
+ // ==================== JavaScript Execution ====================
1379
+ /**
1380
+ * Execute JavaScript in the current page context
1381
+ */
1382
+ async execute_javascript(script) {
1383
+ const page = await this.get_current_page();
1384
+ if (!page) {
1385
+ throw new Error('No page available to execute JavaScript');
1386
+ }
1387
+ return await page.evaluate(script);
1388
+ }
1389
+ // ==================== Page Information ====================
1390
+ /**
1391
+ * Get comprehensive page information (size, scroll position, etc.)
1392
+ */
1393
+ async get_page_info(page) {
1394
+ const targetPage = page || (await this.get_current_page());
1395
+ if (!targetPage) {
1396
+ return null;
1397
+ }
1398
+ const pageData = await targetPage.evaluate(() => {
1399
+ return {
1400
+ // Current viewport dimensions
1401
+ viewport_width: window.innerWidth,
1402
+ viewport_height: window.innerHeight,
1403
+ // Total page dimensions
1404
+ page_width: Math.max(document.documentElement.scrollWidth, document.body.scrollWidth || 0),
1405
+ page_height: Math.max(document.documentElement.scrollHeight, document.body.scrollHeight || 0),
1406
+ // Current scroll position
1407
+ scroll_x: window.scrollX ||
1408
+ window.pageXOffset ||
1409
+ document.documentElement.scrollLeft ||
1410
+ 0,
1411
+ scroll_y: window.scrollY ||
1412
+ window.pageYOffset ||
1413
+ document.documentElement.scrollTop ||
1414
+ 0,
1415
+ };
1416
+ });
1417
+ // Calculate derived values
1418
+ const viewport_width = Math.floor(pageData.viewport_width);
1419
+ const viewport_height = Math.floor(pageData.viewport_height);
1420
+ const page_width = Math.floor(pageData.page_width);
1421
+ const page_height = Math.floor(pageData.page_height);
1422
+ const scroll_x = Math.floor(pageData.scroll_x);
1423
+ const scroll_y = Math.floor(pageData.scroll_y);
1424
+ // Calculate scroll information
1425
+ const pixels_above = scroll_y;
1426
+ const pixels_below = Math.max(0, page_height - (scroll_y + viewport_height));
1427
+ const pixels_left = scroll_x;
1428
+ const pixels_right = Math.max(0, page_width - (scroll_x + viewport_width));
1429
+ return {
1430
+ viewport_width,
1431
+ viewport_height,
1432
+ page_width,
1433
+ page_height,
1434
+ scroll_x,
1435
+ scroll_y,
1436
+ pixels_above,
1437
+ pixels_below,
1438
+ pixels_left,
1439
+ pixels_right,
1440
+ };
1441
+ }
1442
+ /**
1443
+ * Get the HTML content of the current page
1444
+ */
1445
+ async get_page_html() {
1446
+ const page = await this.get_current_page();
1447
+ if (!page) {
1448
+ return '';
1449
+ }
1450
+ return await page.content();
1451
+ }
1452
+ /**
1453
+ * Get a debug view of the page structure including iframes
1454
+ */
1455
+ async get_page_structure() {
1456
+ const page = await this.get_current_page();
1457
+ if (!page) {
1458
+ return '';
1459
+ }
1460
+ const debug_script = `(() => {
1461
+ function getPageStructure(element = document, depth = 0, maxDepth = 10) {
1462
+ if (depth >= maxDepth) return '';
1463
+
1464
+ const indent = ' '.repeat(depth);
1465
+ let structure = '';
1466
+
1467
+ // Skip certain elements that clutter the output
1468
+ const skipTags = new Set(['script', 'style', 'link', 'meta', 'noscript']);
1469
+
1470
+ // Add current element info if it's not the document
1471
+ if (element !== document) {
1472
+ const tagName = element.tagName.toLowerCase();
1473
+
1474
+ // Skip uninteresting elements
1475
+ if (skipTags.has(tagName)) return '';
1476
+
1477
+ const id = element.id ? \`#\${element.id}\` : '';
1478
+ const classes = element.className && typeof element.className === 'string' ?
1479
+ \`.\${element.className.split(' ').filter(c => c).join('.')}\` : '';
1480
+
1481
+ // Get additional useful attributes
1482
+ const attrs = [];
1483
+ if (element.getAttribute('role')) attrs.push(\`role="\${element.getAttribute('role')}"\`);
1484
+ if (element.getAttribute('aria-label')) attrs.push(\`aria-label="\${element.getAttribute('aria-label')}"\`);
1485
+ if (element.getAttribute('type')) attrs.push(\`type="\${element.getAttribute('type')}"\`);
1486
+ if (element.getAttribute('name')) attrs.push(\`name="\${element.getAttribute('name')}"\`);
1487
+ if (element.getAttribute('src')) {
1488
+ const src = element.getAttribute('src');
1489
+ attrs.push(\`src="\${src.substring(0, 50)}\${src.length > 50 ? '...' : ''}"\`);
1490
+ }
1491
+
1492
+ // Add element info
1493
+ structure += \`\${indent}\${tagName}\${id}\${classes}\${attrs.length ? ' [' + attrs.join(', ') + ']' : ''}\\n\`;
1494
+
1495
+ // Handle iframes specially
1496
+ if (tagName === 'iframe') {
1497
+ try {
1498
+ const iframeDoc = element.contentDocument || element.contentWindow?.document;
1499
+ if (iframeDoc) {
1500
+ structure += \`\${indent} [IFRAME CONTENT]:\\n\`;
1501
+ structure += getPageStructure(iframeDoc, depth + 2, maxDepth);
1502
+ } else {
1503
+ structure += \`\${indent} [CROSS-ORIGIN IFRAME - Cannot access]\\n\`;
1504
+ }
1505
+ } catch (e) {
1506
+ structure += \`\${indent} [IFRAME - Access denied]\\n\`;
1507
+ }
1508
+ return structure;
1509
+ }
1510
+ }
1511
+
1512
+ // Process children
1513
+ const children = element.children || element.documentElement?.children || [];
1514
+ for (let i = 0; i < children.length; i++) {
1515
+ structure += getPageStructure(children[i], depth + 1, maxDepth);
1516
+ }
1517
+
1518
+ return structure;
1519
+ }
1520
+
1521
+ return getPageStructure();
1522
+ })()`;
1523
+ return await page.evaluate(debug_script);
1524
+ }
1525
+ // ==================== Navigation & History ====================
1526
+ /**
1527
+ * Navigate forward in browser history
1528
+ */
1529
+ async go_forward() {
1530
+ try {
1531
+ const page = await this.get_current_page();
1532
+ if (page?.goForward) {
1533
+ await page.goForward({ timeout: 10000, waitUntil: 'load' });
1534
+ }
1535
+ }
1536
+ catch (error) {
1537
+ this.logger.debug(`⏭️ Error during go_forward: ${error.message}`);
1538
+ // Verify page is still usable after navigation error
1539
+ if (error.message.toLowerCase().includes('timeout')) {
1540
+ const page = await this.get_current_page();
1541
+ try {
1542
+ await page?.evaluate('1');
1543
+ }
1544
+ catch (evalError) {
1545
+ this.logger.error(`❌ Page crashed after go_forward timeout: ${evalError.message}`);
1546
+ }
1547
+ }
1548
+ }
1549
+ }
1550
+ /**
1551
+ * Refresh the current page
1552
+ */
1553
+ async refresh() {
1554
+ try {
1555
+ const page = await this.get_current_page();
1556
+ if (page?.reload) {
1557
+ this.currentPageLoadingStatus = null;
1558
+ await page.reload({ waitUntil: 'domcontentloaded' });
1559
+ await this._waitForStableNetwork(page);
1560
+ }
1561
+ }
1562
+ catch (error) {
1563
+ this.logger.debug(`🔄 Error during refresh: ${error.message}`);
1564
+ }
1565
+ }
1566
+ // ==================== Element Waiting ====================
1567
+ /**
1568
+ * Wait for an element to appear on the page
1569
+ */
1570
+ async wait_for_element(selector, timeout = 10000) {
1571
+ const page = await this.get_current_page();
1572
+ if (!page) {
1573
+ throw new Error('No page available');
1574
+ }
1575
+ await page.waitForSelector(selector, { state: 'visible', timeout });
1576
+ }
1577
+ // ==================== Screenshots ====================
1578
+ /**
1579
+ * Take a screenshot of the current page
1580
+ * @param full_page Whether to capture the full scrollable page
1581
+ * @returns Base64 encoded PNG screenshot
1582
+ */
1583
+ async take_screenshot(full_page = false) {
1584
+ const page = await this.get_current_page();
1585
+ if (!page) {
1586
+ throw new Error('No page available for screenshot');
1587
+ }
1588
+ if (!this.browser_context) {
1589
+ throw new Error('Browser context is not set');
1590
+ }
1591
+ // Check if it's a new tab page
1592
+ const url = page.url();
1593
+ if (url === 'about:blank' ||
1594
+ url === 'chrome://newtab/' ||
1595
+ url === 'edge://newtab/') {
1596
+ this.logger.warning(`▫️ Skipping screenshot of empty page: ${url}`);
1597
+ // Return a 4px placeholder
1598
+ return 'iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAYAAACp8Z5+AAAAD0lEQVQIHWP8//8/AxYMACgtBP9g8jqYAAAAAElFTkSuQmCC';
1599
+ }
1600
+ // Bring page to front before rendering
1601
+ try {
1602
+ await page.bringToFront();
1603
+ }
1604
+ catch (error) {
1605
+ // Ignore errors
1606
+ }
1607
+ // Take screenshot using CDP for better performance
1608
+ let cdp_session = null;
1609
+ try {
1610
+ this.logger.debug(`📸 Taking ${full_page ? 'full-page' : 'viewport'} PNG screenshot via CDP: ${url}`);
1611
+ // Create CDP session for the screenshot
1612
+ cdp_session = await this.browser_context.newCDPSession(page);
1613
+ // Capture screenshot via CDP
1614
+ const screenshot_response = await cdp_session.send('Page.captureScreenshot', {
1615
+ captureBeyondViewport: false,
1616
+ fromSurface: true,
1617
+ format: 'png',
1618
+ });
1619
+ const screenshot_b64 = screenshot_response.data;
1620
+ if (!screenshot_b64) {
1621
+ throw new Error(`CDP returned empty screenshot data for page ${url}`);
1622
+ }
1623
+ return screenshot_b64;
1624
+ }
1625
+ catch (error) {
1626
+ const error_str = error.message || String(error);
1627
+ if (error_str.toLowerCase().includes('timeout')) {
1628
+ this.logger.warning(`⏱️ Screenshot timed out on page ${url}: ${error_str}`);
1629
+ }
1630
+ else {
1631
+ this.logger.error(`❌ Screenshot failed on page ${url}: ${error_str}`);
1632
+ }
1633
+ throw error;
1634
+ }
1635
+ finally {
1636
+ if (cdp_session) {
1637
+ try {
1638
+ await cdp_session.detach();
1639
+ }
1640
+ catch (error) {
1641
+ // Ignore detach errors
1642
+ }
1643
+ }
1644
+ }
1645
+ }
1646
+ // ==================== Event Listeners ====================
1647
+ /**
1648
+ * Add a request event listener to the current page
1649
+ */
1650
+ async on_request(callback) {
1651
+ const page = await this.get_current_page();
1652
+ if (page && typeof page.on === 'function') {
1653
+ page.on('request', callback);
1654
+ }
1655
+ }
1656
+ /**
1657
+ * Add a response event listener to the current page
1658
+ */
1659
+ async on_response(callback) {
1660
+ const page = await this.get_current_page();
1661
+ if (page && typeof page.on === 'function') {
1662
+ page.on('response', callback);
1663
+ }
1664
+ }
1665
+ /**
1666
+ * Remove a request event listener from the current page
1667
+ */
1668
+ async off_request(callback) {
1669
+ const page = await this.get_current_page();
1670
+ if (page && typeof page.off === 'function') {
1671
+ page.off('request', callback);
1672
+ }
1673
+ }
1674
+ /**
1675
+ * Remove a response event listener from the current page
1676
+ */
1677
+ async off_response(callback) {
1678
+ const page = await this.get_current_page();
1679
+ if (page && typeof page.off === 'function') {
1680
+ page.off('response', callback);
1681
+ }
1682
+ }
1683
+ // ==================== P2 Additional Functions ====================
1684
+ /**
1685
+ * Get information about all open tabs
1686
+ * @returns Array of tab information including page_id, url, and title
1687
+ */
1688
+ async get_tabs_info() {
1689
+ if (!this.browser_context) {
1690
+ return [];
1691
+ }
1692
+ const tabs_info = [];
1693
+ const pages = this.browser_context.pages();
1694
+ for (let page_id = 0; page_id < pages.length; page_id++) {
1695
+ const page = pages[page_id];
1696
+ // Skip chrome:// pages and new tab pages
1697
+ const isNewTab = page.url() === 'about:blank' ||
1698
+ page.url().startsWith('chrome://newtab');
1699
+ if (isNewTab || page.url().startsWith('chrome://')) {
1700
+ if (isNewTab) {
1701
+ tabs_info.push({
1702
+ page_id,
1703
+ url: page.url(),
1704
+ title: 'ignore this tab and do not use it',
1705
+ });
1706
+ }
1707
+ else {
1708
+ tabs_info.push({
1709
+ page_id,
1710
+ url: page.url(),
1711
+ title: page.url(),
1712
+ });
1713
+ }
1714
+ continue;
1715
+ }
1716
+ // Normal pages - try to get title with timeout
1717
+ try {
1718
+ const titlePromise = page.title();
1719
+ const timeoutPromise = new Promise((_, reject) => {
1720
+ setTimeout(() => reject(new Error('timeout')), 2000);
1721
+ });
1722
+ const title = await Promise.race([titlePromise, timeoutPromise]);
1723
+ tabs_info.push({ page_id, url: page.url(), title });
1724
+ }
1725
+ catch (error) {
1726
+ this.logger.debug(`⚠️ Failed to get tab info for tab #${page_id}: ${page.url()} (using fallback title)`);
1727
+ if (isNewTab) {
1728
+ tabs_info.push({
1729
+ page_id,
1730
+ url: page.url(),
1731
+ title: 'ignore this tab and do not use it',
1732
+ });
1733
+ }
1734
+ else {
1735
+ tabs_info.push({
1736
+ page_id,
1737
+ url: page.url(),
1738
+ title: page.url(), // Use URL as fallback title
1739
+ });
1740
+ }
1741
+ }
1742
+ }
1743
+ return tabs_info;
1744
+ }
1745
+ /**
1746
+ * Check if a page is responsive by trying to evaluate simple JavaScript
1747
+ * @param page - The page to check
1748
+ * @param timeout - Timeout in seconds (default: 5)
1749
+ * @returns True if page is responsive, false otherwise
1750
+ */
1751
+ async _is_page_responsive(page, timeout = 5.0) {
1752
+ try {
1753
+ const evalPromise = page.evaluate('1');
1754
+ const timeoutPromise = new Promise((_, reject) => {
1755
+ setTimeout(() => reject(new Error('timeout')), timeout * 1000);
1756
+ });
1757
+ await Promise.race([evalPromise, timeoutPromise]);
1758
+ return true;
1759
+ }
1760
+ catch (error) {
1761
+ return false;
1762
+ }
1763
+ }
1764
+ /**
1765
+ * Get scroll information for the current page
1766
+ * @returns Object with scroll position and page dimensions
1767
+ */
1768
+ async get_scroll_info() {
1769
+ const page = await this.get_current_page();
1770
+ if (!page) {
1771
+ return {
1772
+ scroll_x: 0,
1773
+ scroll_y: 0,
1774
+ page_width: 0,
1775
+ page_height: 0,
1776
+ viewport_width: 0,
1777
+ viewport_height: 0,
1778
+ };
1779
+ }
1780
+ return await page.evaluate(() => {
1781
+ return {
1782
+ scroll_x: window.scrollX ||
1783
+ window.pageXOffset ||
1784
+ document.documentElement.scrollLeft ||
1785
+ 0,
1786
+ scroll_y: window.scrollY ||
1787
+ window.pageYOffset ||
1788
+ document.documentElement.scrollTop ||
1789
+ 0,
1790
+ page_width: Math.max(document.documentElement.scrollWidth, document.body.scrollWidth || 0),
1791
+ page_height: Math.max(document.documentElement.scrollHeight, document.body.scrollHeight || 0),
1792
+ viewport_width: window.innerWidth,
1793
+ viewport_height: window.innerHeight,
1794
+ };
1795
+ });
1796
+ }
1797
+ /**
1798
+ * Get a summary of the current browser state
1799
+ * @param cache_clickable_elements_hashes - Cache clickable element hashes to detect new elements
1800
+ * @param include_screenshot - Include screenshot in state summary
1801
+ * @returns BrowserStateSummary with current page state
1802
+ */
1803
+ async get_state_summary(cache_clickable_elements_hashes = true, include_screenshot = true) {
1804
+ this.logger.debug('🔄 Starting get_state_summary...');
1805
+ const updated_state = await this._get_updated_state(-1, include_screenshot);
1806
+ // Implement clickable element hash caching to detect new elements
1807
+ if (cache_clickable_elements_hashes) {
1808
+ const page = await this.get_current_page();
1809
+ if (page) {
1810
+ const currentUrl = page.url();
1811
+ const currentHashes = this._computeElementHashes(updated_state.selector_map);
1812
+ // Mark new elements if we have cached hashes for this URL
1813
+ if (this._cachedClickableElementHashes &&
1814
+ this._cachedClickableElementHashes.url === currentUrl) {
1815
+ this._markNewElements(updated_state.selector_map, this._cachedClickableElementHashes.hashes);
1816
+ }
1817
+ // Update cache with current hashes
1818
+ this._cachedClickableElementHashes = {
1819
+ url: currentUrl,
1820
+ hashes: currentHashes,
1821
+ };
1822
+ }
1823
+ }
1824
+ this.cachedBrowserState = updated_state;
1825
+ return this.cachedBrowserState;
1826
+ }
1827
+ /**
1828
+ * Get minimal state summary without DOM processing, but with screenshot
1829
+ * Used when page is in error state or unresponsive
1830
+ */
1831
+ async get_minimal_state_summary() {
1832
+ try {
1833
+ const page = await this.get_current_page();
1834
+ const url = page ? page.url() : 'unknown';
1835
+ // Try to get title safely
1836
+ let title = 'Page Load Error';
1837
+ try {
1838
+ if (page) {
1839
+ const titlePromise = page.title();
1840
+ const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 2000));
1841
+ title = await Promise.race([titlePromise, timeoutPromise]);
1842
+ }
1843
+ }
1844
+ catch (error) {
1845
+ // Keep default title
1846
+ }
1847
+ // Try to get tabs info safely
1848
+ let tabs_info = [];
1849
+ try {
1850
+ const tabsPromise = this.get_tabs_info();
1851
+ const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 2000));
1852
+ tabs_info = await Promise.race([tabsPromise, timeoutPromise]);
1853
+ }
1854
+ catch (error) {
1855
+ // Keep empty tabs
1856
+ }
1857
+ // Create minimal DOM element for error state
1858
+ const minimal_element_tree = new DOMElementNode(true, null, 'body', '/body', {}, []);
1859
+ // Try to get screenshot
1860
+ let screenshot_b64 = null;
1861
+ try {
1862
+ screenshot_b64 = await this.take_screenshot();
1863
+ }
1864
+ catch (error) {
1865
+ this.logger.debug(`Screenshot failed in minimal state: ${error.message}`);
1866
+ }
1867
+ // Use default viewport dimensions
1868
+ const viewport = this.browser_profile.viewport || {
1869
+ width: 1280,
1870
+ height: 720,
1871
+ };
1872
+ const dom_state = new DOMState(minimal_element_tree, {});
1873
+ return new BrowserStateSummary(dom_state, {
1874
+ url,
1875
+ title,
1876
+ tabs: tabs_info,
1877
+ screenshot: screenshot_b64,
1878
+ page_info: {
1879
+ viewport_width: viewport.width,
1880
+ viewport_height: viewport.height,
1881
+ page_width: viewport.width,
1882
+ page_height: viewport.height,
1883
+ scroll_x: 0,
1884
+ scroll_y: 0,
1885
+ pixels_above: 0,
1886
+ pixels_below: 0,
1887
+ pixels_left: 0,
1888
+ pixels_right: 0,
1889
+ },
1890
+ pixels_above: 0,
1891
+ pixels_below: 0,
1892
+ browser_errors: ['Page in error state - minimal navigation available'],
1893
+ is_pdf_viewer: false,
1894
+ loading_status: this.currentPageLoadingStatus,
1895
+ });
1896
+ }
1897
+ catch (error) {
1898
+ this.logger.error(`Failed to get minimal state summary: ${error.message}`);
1899
+ throw error;
1900
+ }
1901
+ }
1902
+ /**
1903
+ * Internal method to get updated browser state with DOM processing
1904
+ * @param focus_element - Element index to focus on (default: -1)
1905
+ * @param include_screenshot - Whether to include screenshot
1906
+ */
1907
+ async _get_updated_state(focus_element = -1, include_screenshot = true) {
1908
+ const page = await this.get_current_page();
1909
+ if (!page) {
1910
+ throw new Error('No current page available');
1911
+ }
1912
+ const page_url = page.url();
1913
+ // Check for new tab or chrome:// pages - fast path
1914
+ const is_empty_page = this._is_new_tab_page(page_url) || page_url.startsWith('chrome://');
1915
+ if (is_empty_page) {
1916
+ this.logger.debug(`⚡ Fast path for empty page: ${page_url}`);
1917
+ // Create minimal DOM state
1918
+ const minimal_element_tree = new DOMElementNode(false, null, 'body', '', {}, []);
1919
+ const tabs_info = await this.get_tabs_info();
1920
+ const viewport = this.browser_profile.viewport || {
1921
+ width: 1280,
1922
+ height: 720,
1923
+ };
1924
+ const dom_state = new DOMState(minimal_element_tree, {});
1925
+ return new BrowserStateSummary(dom_state, {
1926
+ url: page_url,
1927
+ title: this._is_new_tab_page(page_url) ? 'New Tab' : 'Chrome Page',
1928
+ tabs: tabs_info,
1929
+ screenshot: null,
1930
+ page_info: {
1931
+ viewport_width: viewport.width,
1932
+ viewport_height: viewport.height,
1933
+ page_width: viewport.width,
1934
+ page_height: viewport.height,
1935
+ scroll_x: 0,
1936
+ scroll_y: 0,
1937
+ pixels_above: 0,
1938
+ pixels_below: 0,
1939
+ pixels_left: 0,
1940
+ pixels_right: 0,
1941
+ },
1942
+ pixels_above: 0,
1943
+ pixels_below: 0,
1944
+ browser_errors: [],
1945
+ is_pdf_viewer: false,
1946
+ loading_status: this.currentPageLoadingStatus,
1947
+ });
1948
+ }
1949
+ // Normal path for regular pages
1950
+ this.logger.debug('🧹 Removing highlights...');
1951
+ try {
1952
+ await this.remove_highlights();
1953
+ }
1954
+ catch (error) {
1955
+ this.logger.debug('Timeout removing highlights');
1956
+ }
1957
+ // Check for PDF and auto-download if needed
1958
+ try {
1959
+ const pdf_path = await this._auto_download_pdf_if_needed(page);
1960
+ if (pdf_path) {
1961
+ this.logger.info(`📄 PDF auto-downloaded: ${pdf_path}`);
1962
+ }
1963
+ }
1964
+ catch (error) {
1965
+ this.logger.debug(`PDF auto-download check failed: ${error.message}`);
1966
+ }
1967
+ // DOM processing
1968
+ this.logger.debug('🌳 Starting DOM processing...');
1969
+ const dom_service = new DomService(page, this.logger);
1970
+ let content;
1971
+ try {
1972
+ const domPromise = dom_service.get_clickable_elements(this.browser_profile.highlight_elements, focus_element, this.browser_profile.viewport_expansion);
1973
+ const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('DOM processing timeout')), 45000));
1974
+ content = await Promise.race([domPromise, timeoutPromise]);
1975
+ this.logger.debug('✅ DOM processing completed');
1976
+ }
1977
+ catch (error) {
1978
+ this.logger.warning(`DOM processing timed out for ${page_url}`);
1979
+ this.logger.warning('🔄 Falling back to minimal DOM state...');
1980
+ // Create minimal DOM state for fallback
1981
+ const minimal_element_tree = new DOMElementNode(true, null, 'body', '/body', {}, []);
1982
+ content = new DOMState(minimal_element_tree, {});
1983
+ }
1984
+ // Get tabs info
1985
+ this.logger.debug('📋 Getting tabs info...');
1986
+ const tabs_info = await this.get_tabs_info();
1987
+ this.logger.debug('✅ Tabs info completed');
1988
+ // Screenshot
1989
+ let screenshot_b64 = null;
1990
+ if (include_screenshot) {
1991
+ try {
1992
+ this.logger.debug('📸 Capturing screenshot...');
1993
+ screenshot_b64 = await this.take_screenshot();
1994
+ }
1995
+ catch (error) {
1996
+ this.logger.warning(`❌ Screenshot failed for ${page_url}: ${error.message}`);
1997
+ }
1998
+ }
1999
+ // Get page info and scroll info
2000
+ const page_info = await this.get_page_info(page);
2001
+ let pixels_above = 0;
2002
+ let pixels_below = 0;
2003
+ try {
2004
+ this.logger.debug('📏 Getting scroll info...');
2005
+ const scroll_info = await Promise.race([
2006
+ this.get_scroll_info(),
2007
+ new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 5000)),
2008
+ ]);
2009
+ // Calculate pixels above/below viewport
2010
+ pixels_above = Math.max(0, scroll_info.scroll_y);
2011
+ const viewport_bottom = scroll_info.scroll_y + scroll_info.viewport_height;
2012
+ pixels_below = Math.max(0, scroll_info.page_height - viewport_bottom);
2013
+ this.logger.debug('✅ Scroll info completed');
2014
+ }
2015
+ catch (error) {
2016
+ this.logger.warning(`Failed to get scroll info: ${error.message}`);
2017
+ }
2018
+ // Get title
2019
+ let title = 'Title unavailable';
2020
+ try {
2021
+ const titlePromise = page.title();
2022
+ const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 3000));
2023
+ title = await Promise.race([titlePromise, timeoutPromise]);
2024
+ }
2025
+ catch (error) {
2026
+ // Keep default title
2027
+ }
2028
+ // Check for errors
2029
+ const browser_errors = [];
2030
+ if (Object.keys(content.selector_map).length === 0) {
2031
+ browser_errors.push(`DOM processing timed out for ${page_url} - using minimal state. Basic navigation still available.`);
2032
+ }
2033
+ // Check if PDF viewer
2034
+ const is_pdf_viewer = await this._is_pdf_viewer(page);
2035
+ const browser_state = new BrowserStateSummary(content, {
2036
+ url: page_url,
2037
+ title,
2038
+ tabs: tabs_info,
2039
+ screenshot: screenshot_b64,
2040
+ page_info,
2041
+ pixels_above,
2042
+ pixels_below,
2043
+ browser_errors,
2044
+ is_pdf_viewer,
2045
+ loading_status: this.currentPageLoadingStatus,
2046
+ });
2047
+ this.logger.debug('✅ get_state_summary completed successfully');
2048
+ return browser_state;
2049
+ }
2050
+ /**
2051
+ * Check if a URL is a new tab page
2052
+ */
2053
+ _is_new_tab_page(url) {
2054
+ return (url === 'about:blank' ||
2055
+ url === 'about:newtab' ||
2056
+ url === 'chrome://newtab/');
2057
+ }
2058
+ /**
2059
+ * Check if page is displaying a PDF
2060
+ */
2061
+ async _is_pdf_viewer(page) {
2062
+ try {
2063
+ const url = page.url();
2064
+ if (url.endsWith('.pdf') || url.includes('.pdf?')) {
2065
+ return true;
2066
+ }
2067
+ // Check for PDF viewer in page content
2068
+ const is_pdf = await page.evaluate(() => {
2069
+ return (document.querySelector('embed[type="application/pdf"]') !== null ||
2070
+ document.querySelector('object[type="application/pdf"]') !== null);
2071
+ });
2072
+ return is_pdf;
2073
+ }
2074
+ catch (error) {
2075
+ return false;
2076
+ }
2077
+ }
2078
+ /**
2079
+ * Auto-download PDF if detected and auto-download is enabled
2080
+ */
2081
+ async _auto_download_pdf_if_needed(page) {
2082
+ const downloadsPath = this.browser_profile.downloads_path;
2083
+ if (!downloadsPath || !this._autoDownloadPdfs) {
2084
+ return null;
2085
+ }
2086
+ try {
2087
+ const is_pdf = await this._is_pdf_viewer(page);
2088
+ if (!is_pdf) {
2089
+ return null;
2090
+ }
2091
+ const url = page.url();
2092
+ this.logger.info(`📄 PDF detected: ${url}`);
2093
+ let pdfFilename = path.basename(url.split('?')[0]);
2094
+ if (!pdfFilename || !pdfFilename.toLowerCase().endsWith('.pdf')) {
2095
+ const parsed = new URL(url);
2096
+ pdfFilename = path.basename(parsed.pathname) || 'document.pdf';
2097
+ if (!pdfFilename.toLowerCase().endsWith('.pdf')) {
2098
+ pdfFilename += '.pdf';
2099
+ }
2100
+ }
2101
+ if (this.downloaded_files.some((downloaded) => path.basename(downloaded) === pdfFilename)) {
2102
+ this.logger.debug(`📄 PDF already downloaded: ${pdfFilename}`);
2103
+ return null;
2104
+ }
2105
+ this.logger.info(`📄 Auto-downloading PDF from: ${url}`);
2106
+ const downloadResult = await page.evaluate(async (pdfUrl) => {
2107
+ try {
2108
+ const response = await fetch(pdfUrl, {
2109
+ cache: 'force-cache',
2110
+ });
2111
+ if (!response.ok) {
2112
+ throw new Error(`HTTP error! status: ${response.status}`);
2113
+ }
2114
+ const blob = await response.blob();
2115
+ const arrayBuffer = await blob.arrayBuffer();
2116
+ const uint8Array = new Uint8Array(arrayBuffer);
2117
+ const cacheHeader = response.headers.get('x-cache') || '';
2118
+ const fromCache = response.headers.has('age') ||
2119
+ cacheHeader.toLowerCase().includes('hit');
2120
+ return {
2121
+ data: Array.from(uint8Array),
2122
+ fromCache,
2123
+ responseSize: uint8Array.length,
2124
+ };
2125
+ }
2126
+ catch (error) {
2127
+ return {
2128
+ data: [],
2129
+ fromCache: false,
2130
+ responseSize: 0,
2131
+ error: error instanceof Error ? error.message : 'Unknown fetch error',
2132
+ };
2133
+ }
2134
+ }, url);
2135
+ if (downloadResult?.error) {
2136
+ this.logger.warning(`⚠️ Failed to auto-download PDF from ${url}: ${downloadResult.error}`);
2137
+ return null;
2138
+ }
2139
+ if (!downloadResult ||
2140
+ !Array.isArray(downloadResult.data) ||
2141
+ downloadResult.data.length === 0) {
2142
+ this.logger.warning(`⚠️ No data received when downloading PDF from ${url}`);
2143
+ return null;
2144
+ }
2145
+ await fs.promises.mkdir(downloadsPath, { recursive: true });
2146
+ const uniqueFilename = await BrowserSession.get_unique_filename(downloadsPath, pdfFilename);
2147
+ const downloadPath = path.join(downloadsPath, uniqueFilename);
2148
+ await fs.promises.writeFile(downloadPath, Buffer.from(downloadResult.data));
2149
+ this.add_downloaded_file(downloadPath);
2150
+ const cacheStatus = downloadResult.fromCache
2151
+ ? 'from cache'
2152
+ : 'from network';
2153
+ const responseSize = Number(downloadResult.responseSize || 0);
2154
+ this.logger.info(`📄 Auto-downloaded PDF (${cacheStatus}, ${responseSize.toLocaleString()} bytes): ${downloadPath}`);
2155
+ return downloadPath;
2156
+ }
2157
+ catch (error) {
2158
+ this.logger.debug(`PDF detection failed: ${error.message}`);
2159
+ return null;
2160
+ }
2161
+ }
2162
+ /**
2163
+ * Check if an element is visible on the page
2164
+ */
2165
+ async _is_visible(element) {
2166
+ try {
2167
+ const is_hidden = await element.isHidden();
2168
+ const bbox = await element.boundingBox();
2169
+ return !is_hidden && bbox !== null && bbox.width > 0 && bbox.height > 0;
2170
+ }
2171
+ catch (error) {
2172
+ return false;
2173
+ }
2174
+ }
2175
+ /**
2176
+ * Locate an element by XPath
2177
+ */
2178
+ async get_locate_element_by_xpath(xpath) {
2179
+ const page = await this.get_current_page();
2180
+ if (!page) {
2181
+ return null;
2182
+ }
2183
+ try {
2184
+ // Use XPath to locate the element
2185
+ const element_handle = await page
2186
+ .locator(`xpath=${xpath}`)
2187
+ .elementHandle();
2188
+ if (element_handle) {
2189
+ const is_visible = await this._is_visible(element_handle);
2190
+ if (is_visible) {
2191
+ await element_handle.scrollIntoViewIfNeeded({ timeout: 1000 });
2192
+ }
2193
+ return element_handle;
2194
+ }
2195
+ return null;
2196
+ }
2197
+ catch (error) {
2198
+ this.logger.error(`❌ Failed to locate xpath ${xpath}: ${error.message}`);
2199
+ return null;
2200
+ }
2201
+ }
2202
+ /**
2203
+ * Locate an element by CSS selector
2204
+ */
2205
+ async get_locate_element_by_css_selector(css_selector) {
2206
+ const page = await this.get_current_page();
2207
+ if (!page) {
2208
+ return null;
2209
+ }
2210
+ try {
2211
+ // Use CSS selector to locate the element
2212
+ const element_handle = await page.locator(css_selector).elementHandle();
2213
+ if (element_handle) {
2214
+ const is_visible = await this._is_visible(element_handle);
2215
+ if (is_visible) {
2216
+ await element_handle.scrollIntoViewIfNeeded({ timeout: 1000 });
2217
+ }
2218
+ return element_handle;
2219
+ }
2220
+ return null;
2221
+ }
2222
+ catch (error) {
2223
+ this.logger.error(`❌ Failed to locate element ${css_selector}: ${error.message}`);
2224
+ return null;
2225
+ }
2226
+ }
2227
+ /**
2228
+ * Locate an element by text content
2229
+ * @param text - Text to search for
2230
+ * @param nth - Which matching element to return (0-based index)
2231
+ * @param element_type - Optional tag name to filter by (e.g., 'button', 'span')
2232
+ */
2233
+ async get_locate_element_by_text(text, nth = 0, element_type = null) {
2234
+ const page = await this.get_current_page();
2235
+ if (!page) {
2236
+ return null;
2237
+ }
2238
+ try {
2239
+ // Build selector: filter by element type and text
2240
+ const selector = element_type
2241
+ ? `${element_type}:text("${text}")`
2242
+ : `:text("${text}")`;
2243
+ // Get all matching elements
2244
+ const locator = page.locator(selector);
2245
+ const count = await locator.count();
2246
+ if (count === 0) {
2247
+ this.logger.error(`❌ No element with text '${text}' found`);
2248
+ return null;
2249
+ }
2250
+ // Filter visible elements
2251
+ const visible_elements = [];
2252
+ for (let i = 0; i < count; i++) {
2253
+ const element_handle = await locator.nth(i).elementHandle();
2254
+ if (element_handle && (await this._is_visible(element_handle))) {
2255
+ visible_elements.push(element_handle);
2256
+ }
2257
+ }
2258
+ if (visible_elements.length === 0) {
2259
+ this.logger.error(`❌ No visible element with text '${text}' found`);
2260
+ return null;
2261
+ }
2262
+ if (nth >= visible_elements.length) {
2263
+ this.logger.error(`❌ Element with text '${text}' not found at index #${nth}`);
2264
+ return null;
2265
+ }
2266
+ const element_handle = visible_elements[nth];
2267
+ const is_visible = await this._is_visible(element_handle);
2268
+ if (is_visible) {
2269
+ await element_handle.scrollIntoViewIfNeeded({ timeout: 1000 });
2270
+ }
2271
+ return element_handle;
2272
+ }
2273
+ catch (error) {
2274
+ this.logger.error(`❌ Failed to locate element by text '${text}': ${error.message}`);
2275
+ return null;
2276
+ }
2277
+ }
2278
+ /**
2279
+ * Check if browser session is connected and has valid browser/context objects
2280
+ * @param restart - If true, attempt to create a new tab if no pages exist
2281
+ */
2282
+ async is_connected(restart = true) {
2283
+ if (!this.browser_context) {
2284
+ return false;
2285
+ }
2286
+ try {
2287
+ // Check if browser is connected
2288
+ if (this.browser && !this.browser.isConnected()) {
2289
+ return false;
2290
+ }
2291
+ // Check if browser context's browser is connected (context may reference a different browser object)
2292
+ const context_browser = this.browser_context.browser?.();
2293
+ if (context_browser && !context_browser.isConnected()) {
2294
+ return false;
2295
+ }
2296
+ // Check if context has at least one page
2297
+ const pages = this.browser_context.pages();
2298
+ if (pages.length === 0) {
2299
+ if (restart) {
2300
+ // Try to create a new page to keep context alive
2301
+ try {
2302
+ await this.browser_context.newPage();
2303
+ }
2304
+ catch (error) {
2305
+ return false;
2306
+ }
2307
+ }
2308
+ else {
2309
+ return false;
2310
+ }
2311
+ }
2312
+ return true;
2313
+ }
2314
+ catch (error) {
2315
+ return false;
2316
+ }
2317
+ }
2318
+ /**
2319
+ * Check if a URL is allowed based on allowed_domains configuration
2320
+ * @param url - URL to check
2321
+ */
2322
+ _is_url_allowed(url) {
2323
+ if (!this.browser_profile.allowed_domains ||
2324
+ this.browser_profile.allowed_domains.length === 0) {
2325
+ return true; // No restrictions if allowed_domains not configured
2326
+ }
2327
+ // Always allow new tab pages
2328
+ if (this._is_new_tab_page(url)) {
2329
+ return true;
2330
+ }
2331
+ // Check against allowed domains
2332
+ for (const allowed_domain of this.browser_profile.allowed_domains) {
2333
+ try {
2334
+ if (match_url_with_domain_pattern(url, allowed_domain, true)) {
2335
+ return true;
2336
+ }
2337
+ }
2338
+ catch (error) {
2339
+ this.logger.warning(`Invalid domain pattern: ${allowed_domain}`);
2340
+ }
2341
+ }
2342
+ return false;
2343
+ }
2344
+ /**
2345
+ * Navigate helper with URL validation
2346
+ */
2347
+ async navigate(url) {
2348
+ // Validate URL is allowed
2349
+ if (!this._is_url_allowed(url)) {
2350
+ throw new Error(`URL ${url} is not in allowed_domains. ` +
2351
+ `Current allowed_domains: ${JSON.stringify(this.browser_profile.allowed_domains)}`);
2352
+ }
2353
+ await this.navigate_to(url);
2354
+ }
2355
+ /**
2356
+ * Kill the browser session (force close even if keep_alive=true)
2357
+ */
2358
+ async kill() {
2359
+ this.logger.info('💀 Force killing browser session...');
2360
+ // Temporarily disable keep_alive to ensure browser closes
2361
+ const original_keep_alive = this.browser_profile.keep_alive;
2362
+ this.browser_profile.keep_alive = false;
2363
+ try {
2364
+ await this.close();
2365
+ }
2366
+ finally {
2367
+ // Restore original keep_alive setting
2368
+ this.browser_profile.keep_alive = original_keep_alive;
2369
+ }
2370
+ }
2371
+ /**
2372
+ * Alias for close() to match Python API
2373
+ */
2374
+ async stop() {
2375
+ if (this.browser_profile.keep_alive) {
2376
+ this.logger.info('🕊️ BrowserSession.stop() called but keep_alive=true, leaving browser running. Use .kill() to force close.');
2377
+ return;
2378
+ }
2379
+ if (this._stoppingPromise) {
2380
+ await this._stoppingPromise;
2381
+ return;
2382
+ }
2383
+ const hasActiveResources = this.initialized ||
2384
+ Boolean(this.browser ||
2385
+ this.browser_context ||
2386
+ this.browser_pid ||
2387
+ this._subprocess ||
2388
+ this._childProcesses.size > 0);
2389
+ if (!hasActiveResources) {
2390
+ return;
2391
+ }
2392
+ this._stoppingPromise = this._shutdown_browser_session();
2393
+ try {
2394
+ await this._stoppingPromise;
2395
+ }
2396
+ finally {
2397
+ this._stoppingPromise = null;
2398
+ }
2399
+ }
2400
+ /**
2401
+ * Perform a click action with download and navigation handling
2402
+ * @param element_node - DOM element to click
2403
+ */
2404
+ async perform_click(element_node) {
2405
+ const page = await this.get_current_page();
2406
+ if (!page) {
2407
+ throw new Error('No current page available');
2408
+ }
2409
+ const element_handle = await this.get_locate_element(element_node);
2410
+ if (!element_handle) {
2411
+ throw new Error(`Element not found: ${JSON.stringify(element_node)}`);
2412
+ }
2413
+ // Check if downloads are enabled
2414
+ const downloads_path = this.browser_profile.downloads_path;
2415
+ if (downloads_path) {
2416
+ try {
2417
+ // Try to detect file download
2418
+ const download_promise = page.waitForEvent('download', {
2419
+ timeout: 5000,
2420
+ });
2421
+ // Perform the click
2422
+ await element_handle.click();
2423
+ // Wait for download or timeout
2424
+ const download = await download_promise;
2425
+ // Save the downloaded file
2426
+ const suggested_filename = download.suggestedFilename();
2427
+ const unique_filename = await BrowserSession.get_unique_filename(downloads_path, suggested_filename);
2428
+ const download_path = path.join(downloads_path, unique_filename);
2429
+ await download.saveAs(download_path);
2430
+ this.logger.info(`⬇️ Downloaded file to: ${download_path}`);
2431
+ // Track the downloaded file
2432
+ this.add_downloaded_file(download_path);
2433
+ return download_path;
2434
+ }
2435
+ catch (error) {
2436
+ // No download triggered, treat as normal click
2437
+ this.logger.debug('No download triggered within timeout. Checking navigation...');
2438
+ try {
2439
+ await page.waitForLoadState();
2440
+ }
2441
+ catch (e) {
2442
+ this.logger.warning(`Navigation check failed: ${e.message}`);
2443
+ }
2444
+ }
2445
+ }
2446
+ else {
2447
+ // No downloads path configured, just click
2448
+ await element_handle.click();
2449
+ }
2450
+ return null;
2451
+ }
2452
+ /**
2453
+ * Remove all highlights from the current page
2454
+ */
2455
+ async remove_highlights() {
2456
+ const page = await this.get_current_page();
2457
+ if (!page) {
2458
+ return;
2459
+ }
2460
+ try {
2461
+ await page.evaluate(() => {
2462
+ // Remove all elements with browser-use highlight class
2463
+ const highlights = document.querySelectorAll('.browser-use-highlight');
2464
+ highlights.forEach((el) => el.remove());
2465
+ // Remove inline highlight styles
2466
+ const styled = document.querySelectorAll('[style*="browser-use"]');
2467
+ styled.forEach((el) => {
2468
+ if (el.style) {
2469
+ el.style.outline = '';
2470
+ el.style.border = '';
2471
+ }
2472
+ });
2473
+ });
2474
+ }
2475
+ catch (error) {
2476
+ this.logger.debug(`Failed to remove highlights: ${error.message}`);
2477
+ }
2478
+ }
2479
+ // region - Trace Recording
2480
+ /**
2481
+ * Start tracing on browser context if traces_dir is configured
2482
+ * Note: Currently optional as it may cause performance issues in some cases
2483
+ */
2484
+ async _startContextTracing() {
2485
+ if (this.browser_profile.traces_dir && this.browser_context) {
2486
+ try {
2487
+ this.logger.debug(`📽️ Starting tracing (will save to: ${this.browser_profile.traces_dir})`);
2488
+ await this.browser_context.tracing.start({
2489
+ screenshots: true,
2490
+ snapshots: true,
2491
+ sources: false, // Reduce trace size
2492
+ });
2493
+ }
2494
+ catch (error) {
2495
+ this.logger.warning(`Failed to start tracing: ${error.message}`);
2496
+ }
2497
+ }
2498
+ }
2499
+ /**
2500
+ * Save browser trace recording
2501
+ */
2502
+ async _saveTraceRecording() {
2503
+ if (this.browser_profile.traces_dir && this.browser_context) {
2504
+ try {
2505
+ const tracesPath = this.browser_profile.traces_dir;
2506
+ let finalTracePath;
2507
+ // Check if path has extension
2508
+ if (path.extname(tracesPath)) {
2509
+ // Path has extension, use as-is (user specified exact file path)
2510
+ finalTracePath = tracesPath;
2511
+ }
2512
+ else {
2513
+ // Path has no extension, treat as directory and create filename
2514
+ const traceFilename = `BrowserSession_${this.id}.zip`;
2515
+ finalTracePath = path.join(tracesPath, traceFilename);
2516
+ }
2517
+ this.logger.info(`🎥 Saving browser_context trace to ${finalTracePath}...`);
2518
+ await this.browser_context.tracing.stop({ path: finalTracePath });
2519
+ }
2520
+ catch (error) {
2521
+ this.logger.warning(`Failed to save trace recording: ${error.message}`);
2522
+ }
2523
+ }
2524
+ }
2525
+ // endregion
2526
+ // region - CDP Advanced Integration
2527
+ /**
2528
+ * Scroll using CDP Input.synthesizeScrollGesture for universal compatibility
2529
+ * @param page - The page to scroll
2530
+ * @param pixels - Number of pixels to scroll (positive = up, negative = down)
2531
+ * @returns true if successful, false if failed
2532
+ */
2533
+ async _scrollWithCdpGesture(page, pixels) {
2534
+ try {
2535
+ // Use CDP to synthesize scroll gesture - works in all contexts including PDFs
2536
+ const cdpSession = await this.browser_context.newCDPSession(page);
2537
+ // Get viewport center for scroll origin
2538
+ const viewport = await page.evaluate(() => ({
2539
+ width: window.innerWidth,
2540
+ height: window.innerHeight,
2541
+ }));
2542
+ const centerX = Math.floor(viewport.width / 2);
2543
+ const centerY = Math.floor(viewport.height / 2);
2544
+ await cdpSession.send('Input.synthesizeScrollGesture', {
2545
+ x: centerX,
2546
+ y: centerY,
2547
+ xDistance: 0,
2548
+ yDistance: -pixels, // Negative = scroll down, Positive = scroll up
2549
+ gestureSourceType: 'mouse', // Use mouse gestures for better compatibility
2550
+ speed: 3000, // Pixels per second
2551
+ });
2552
+ try {
2553
+ await Promise.race([
2554
+ cdpSession.detach(),
2555
+ new Promise((resolve) => setTimeout(resolve, 1000)),
2556
+ ]);
2557
+ }
2558
+ catch {
2559
+ // Ignore detach errors
2560
+ }
2561
+ this.logger.debug(`📄 Scrolled via CDP Input.synthesizeScrollGesture: ${pixels}px`);
2562
+ return true;
2563
+ }
2564
+ catch (error) {
2565
+ this.logger.warning(`❌ Scrolling via CDP Input.synthesizeScrollGesture failed: ${error.message}`);
2566
+ return false;
2567
+ }
2568
+ }
2569
+ /**
2570
+ * Scroll the current page container
2571
+ * @param pixels - Number of pixels to scroll (positive = up, negative = down)
2572
+ */
2573
+ async _scrollContainer(pixels) {
2574
+ const page = await this.getCurrentPage();
2575
+ if (!page) {
2576
+ throw new Error('No active page available for scrolling');
2577
+ }
2578
+ // Try CDP scroll gesture first (works universally including PDFs)
2579
+ if (await this._scrollWithCdpGesture(page, pixels)) {
2580
+ return;
2581
+ }
2582
+ // Fallback to JavaScript for older browsers or when CDP fails
2583
+ this.logger.debug('Falling back to JavaScript scrolling');
2584
+ const SMART_SCROLL_JS = `(dy) => {
2585
+ const bigEnough = el => el.clientHeight >= window.innerHeight * 0.5;
2586
+ const canScroll = el =>
2587
+ el &&
2588
+ /(auto|scroll|overlay)/.test(getComputedStyle(el).overflowY) &&
2589
+ el.scrollHeight > el.clientHeight &&
2590
+ bigEnough(el);
2591
+
2592
+ let el = document.activeElement;
2593
+ while (el && !canScroll(el) && el !== document.body) el = el.parentElement;
2594
+
2595
+ el = canScroll(el)
2596
+ ? el
2597
+ : [...document.querySelectorAll('*')].find(canScroll)
2598
+ || document.scrollingElement
2599
+ || document.documentElement;
2600
+
2601
+ if (el === document.scrollingElement ||
2602
+ el === document.documentElement ||
2603
+ el === document.body) {
2604
+ window.scrollBy(0, dy);
2605
+ } else {
2606
+ el.scrollBy(0, dy);
2607
+ }
2608
+ }`;
2609
+ await page.evaluate(SMART_SCROLL_JS, pixels);
2610
+ }
2611
+ /**
2612
+ * Compute hashes for all clickable elements in the selector map
2613
+ * @param selectorMap - Selector map from DOM state
2614
+ * @returns Set of element hashes
2615
+ */
2616
+ _computeElementHashes(selectorMap) {
2617
+ const hashes = new Set();
2618
+ for (const [index, element] of Object.entries(selectorMap)) {
2619
+ if (element instanceof DOMElementNode) {
2620
+ // Create hash from element's xpath and key attributes
2621
+ const hashParts = [
2622
+ element.xpath || '',
2623
+ element.tag_name || '',
2624
+ JSON.stringify(element.attributes || {}),
2625
+ ];
2626
+ const hash = hashParts.join('|');
2627
+ hashes.add(hash);
2628
+ }
2629
+ }
2630
+ return hashes;
2631
+ }
2632
+ /**
2633
+ * Mark elements in the selector map as new if they weren't in the cached hashes
2634
+ * @param selectorMap - Selector map to update
2635
+ * @param cachedHashes - Previously cached element hashes
2636
+ */
2637
+ _markNewElements(selectorMap, cachedHashes) {
2638
+ for (const [index, element] of Object.entries(selectorMap)) {
2639
+ if (element instanceof DOMElementNode) {
2640
+ // Create hash for current element
2641
+ const hashParts = [
2642
+ element.xpath || '',
2643
+ element.tag_name || '',
2644
+ JSON.stringify(element.attributes || {}),
2645
+ ];
2646
+ const hash = hashParts.join('|');
2647
+ // Mark as new if not in cached hashes
2648
+ if (!cachedHashes.has(hash)) {
2649
+ // Add a marker to the element's attributes to indicate it's new
2650
+ element.attributes = element.attributes || {};
2651
+ element.attributes['__browser_use_new_element'] = true;
2652
+ }
2653
+ }
2654
+ }
2655
+ }
2656
+ /**
2657
+ * Helper to get a safe method name from the calling context
2658
+ * Used for recovery error messages
2659
+ */
2660
+ _getCurrentMethodName() {
2661
+ try {
2662
+ const stack = new Error().stack;
2663
+ if (!stack)
2664
+ return 'unknown';
2665
+ const lines = stack.split('\n');
2666
+ // Skip first 3 lines: Error, this method, and the caller
2667
+ const callerLine = lines[3] || '';
2668
+ const match = callerLine.match(/at (?:BrowserSession\.)?(\w+)/);
2669
+ return match ? match[1] : 'unknown';
2670
+ }
2671
+ catch {
2672
+ return 'unknown';
2673
+ }
2674
+ }
2675
+ /**
2676
+ * Get current page with fallback logic
2677
+ * Alias for compatibility with Python API
2678
+ */
2679
+ async getCurrentPage() {
2680
+ return await this.get_current_page();
2681
+ }
2682
+ /**
2683
+ * Log warning about unsafe glob patterns
2684
+ * @param pattern - The glob pattern being used
2685
+ */
2686
+ _logGlobWarning(pattern) {
2687
+ const unsafePatterns = [
2688
+ '**/*',
2689
+ '**/.*',
2690
+ '~/*',
2691
+ '/etc/*',
2692
+ '/sys/*',
2693
+ '/proc/*',
2694
+ ];
2695
+ const isUnsafe = unsafePatterns.some((unsafe) => pattern.includes(unsafe) ||
2696
+ pattern.startsWith(unsafe.replace('**/', '')));
2697
+ if (isUnsafe) {
2698
+ this.logger.warning(`⚠️ Potentially unsafe glob pattern detected: "${pattern}". ` +
2699
+ `This could access system files or expose sensitive data.`);
2700
+ }
2701
+ }
2702
+ /**
2703
+ * Create a shallow copy of the browser session
2704
+ * Note: This doesn't copy the actual browser instance, just the session metadata
2705
+ * @returns A new BrowserSession instance with copied state
2706
+ */
2707
+ modelCopy() {
2708
+ return new BrowserSession({
2709
+ id: this.id,
2710
+ browser_profile: this.browser_profile,
2711
+ browser: this.browser,
2712
+ browser_context: this.browser_context,
2713
+ page: this.agent_current_page,
2714
+ title: this.currentTitle,
2715
+ url: this.currentUrl,
2716
+ wss_url: this.wss_url,
2717
+ cdp_url: this.cdp_url,
2718
+ browser_pid: this.browser_pid,
2719
+ playwright: this.playwright,
2720
+ downloaded_files: [...this.downloaded_files],
2721
+ });
2722
+ }
2723
+ model_copy() {
2724
+ return this.modelCopy();
2725
+ }
2726
+ // endregion
2727
+ // region - Page Health Check and Recovery
2728
+ _inRecovery = false;
2729
+ /**
2730
+ * Check if a page is responsive by trying to evaluate simple JavaScript
2731
+ * @param page - The page to check
2732
+ * @param timeout - Timeout in seconds (default: 5.0)
2733
+ * @returns true if page is responsive, false otherwise
2734
+ */
2735
+ async _isPageResponsive(page, timeout = 5.0) {
2736
+ try {
2737
+ const timeoutMs = timeout * 1000;
2738
+ await Promise.race([
2739
+ page.evaluate('1'),
2740
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), timeoutMs)),
2741
+ ]);
2742
+ return true;
2743
+ }
2744
+ catch (error) {
2745
+ return false;
2746
+ }
2747
+ }
2748
+ /**
2749
+ * Force close a crashed page using CDP from a clean temporary page
2750
+ * @param pageUrl - The URL of the page to force close
2751
+ * @returns true if successful, false otherwise
2752
+ */
2753
+ async _forceClosePageViaCdp(pageUrl) {
2754
+ try {
2755
+ if (!this.browser_context) {
2756
+ throw new Error('Browser context is not set up yet');
2757
+ }
2758
+ // Create a clean page for CDP operations
2759
+ const tempPage = await Promise.race([
2760
+ this.browser_context.newPage(),
2761
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout creating temp page')), 5000)),
2762
+ ]);
2763
+ await Promise.race([
2764
+ tempPage.goto('about:blank'),
2765
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout navigating to blank')), 2000)),
2766
+ ]);
2767
+ try {
2768
+ // Create CDP session from the clean page
2769
+ const cdpSession = await Promise.race([
2770
+ this.browser_context.newCDPSession(tempPage),
2771
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout creating CDP session')), 5000)),
2772
+ ]);
2773
+ try {
2774
+ // Get all browser targets
2775
+ const targets = (await Promise.race([
2776
+ cdpSession.send('Target.getTargets'),
2777
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout getting targets')), 2000)),
2778
+ ]));
2779
+ // Find the crashed page target
2780
+ let blockedTargetId = null;
2781
+ const targetInfos = targets.targetInfos || [];
2782
+ for (const target of targetInfos) {
2783
+ if (target.type === 'page' && target.url === pageUrl) {
2784
+ blockedTargetId = target.targetId;
2785
+ break;
2786
+ }
2787
+ }
2788
+ if (blockedTargetId) {
2789
+ // Force close the target
2790
+ this.logger.warning(`🪓 Force-closing crashed page target_id=${blockedTargetId} via CDP: ${pageUrl.substring(0, 50)}...`);
2791
+ await Promise.race([
2792
+ cdpSession.send('Target.closeTarget', {
2793
+ targetId: blockedTargetId,
2794
+ }),
2795
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout closing target')), 2000)),
2796
+ ]);
2797
+ return true;
2798
+ }
2799
+ else {
2800
+ this.logger.debug(`❌ Could not find CDP page target_id to force-close: ${pageUrl.substring(0, 50)} (concurrency issues?)`);
2801
+ return false;
2802
+ }
2803
+ }
2804
+ finally {
2805
+ try {
2806
+ await Promise.race([
2807
+ cdpSession.detach(),
2808
+ new Promise((resolve) => setTimeout(resolve, 1000)),
2809
+ ]);
2810
+ }
2811
+ catch {
2812
+ // Ignore detach errors
2813
+ }
2814
+ }
2815
+ }
2816
+ finally {
2817
+ await tempPage.close();
2818
+ }
2819
+ }
2820
+ catch (error) {
2821
+ this.logger.error(`❌ Using raw CDP to force-close crashed page failed: ${error.message}`);
2822
+ return false;
2823
+ }
2824
+ }
2825
+ /**
2826
+ * Try to reopen a URL in a new page and check if it's responsive
2827
+ * @param url - The URL to reopen
2828
+ * @param timeoutMs - Navigation timeout in milliseconds
2829
+ * @returns true if successful and responsive, false otherwise
2830
+ */
2831
+ async _tryReopenUrl(url, timeoutMs) {
2832
+ if (!url ||
2833
+ url.startsWith('about:') ||
2834
+ url.startsWith('chrome://') ||
2835
+ url.startsWith('edge://')) {
2836
+ return false;
2837
+ }
2838
+ const timeout = timeoutMs || this.browser_profile.default_navigation_timeout || 6000;
2839
+ try {
2840
+ this.logger.debug(`🔄 Attempting to reload URL that crashed: ${url.substring(0, 50)}`);
2841
+ if (!this.browser_context) {
2842
+ throw new Error('Browser context is not set');
2843
+ }
2844
+ // Create new page directly to avoid circular dependency
2845
+ const newPage = await this.browser_context.newPage();
2846
+ this.agent_current_page = newPage;
2847
+ // Update human tab reference if there is no human tab yet
2848
+ if (!this.human_current_page || this.human_current_page.isClosed()) {
2849
+ this.human_current_page = newPage;
2850
+ }
2851
+ // Set viewport for new tab
2852
+ if (this.browser_profile.window_size) {
2853
+ await newPage.setViewportSize(this.browser_profile.window_size);
2854
+ }
2855
+ // Navigate with timeout
2856
+ try {
2857
+ await Promise.race([
2858
+ newPage.goto(url, { waitUntil: 'load', timeout }),
2859
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Navigation timeout')), timeout + 500)),
2860
+ ]);
2861
+ }
2862
+ catch (error) {
2863
+ this.logger.debug(`⚠️ Attempting to reload previously crashed URL ${url.substring(0, 50)} failed again: ${error.name}`);
2864
+ }
2865
+ // Wait a bit for any transient blocking to resolve
2866
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2867
+ // Check if the reopened page is responsive
2868
+ const isResponsive = await this._isPageResponsive(newPage, 2.0);
2869
+ if (isResponsive) {
2870
+ this.logger.info(`✅ Page recovered and is now responsive after reopening on: ${url.substring(0, 50)}`);
2871
+ return true;
2872
+ }
2873
+ else {
2874
+ this.logger.warning(`⚠️ Reopened page ${url.substring(0, 50)} is still unresponsive`);
2875
+ // Close the unresponsive page before returning
2876
+ try {
2877
+ await this._forceClosePageViaCdp(newPage.url());
2878
+ }
2879
+ catch (error) {
2880
+ this.logger.error(`❌ Failed to close crashed page ${url.substring(0, 50)} via CDP: ${error.message} (something is very wrong or system is extremely overloaded)`);
2881
+ }
2882
+ this.agent_current_page = null; // Clear reference to closed page
2883
+ return false;
2884
+ }
2885
+ }
2886
+ catch (error) {
2887
+ this.logger.error(`❌ Retrying crashed page ${url.substring(0, 50)} failed: ${error.message}`);
2888
+ return false;
2889
+ }
2890
+ }
2891
+ /**
2892
+ * Create a new blank page as a fallback when recovery fails
2893
+ * @param url - The original URL that failed
2894
+ */
2895
+ async _createBlankFallbackPage(url) {
2896
+ this.logger.warning(`⚠️ Resetting to about:blank as fallback because browser is unable to load the original URL without crashing: ${url.substring(0, 50)}`);
2897
+ // Close any existing broken page
2898
+ if (this.agent_current_page && !this.agent_current_page.isClosed()) {
2899
+ try {
2900
+ await this.agent_current_page.close();
2901
+ }
2902
+ catch {
2903
+ // Ignore close errors
2904
+ }
2905
+ }
2906
+ if (!this.browser_context) {
2907
+ throw new Error('Browser context is not set');
2908
+ }
2909
+ // Create fresh page directly (avoid decorated methods to prevent circular dependency)
2910
+ const newPage = await this.browser_context.newPage();
2911
+ this.agent_current_page = newPage;
2912
+ // Update human tab reference if there is no human tab yet
2913
+ if (!this.human_current_page || this.human_current_page.isClosed()) {
2914
+ this.human_current_page = newPage;
2915
+ }
2916
+ // Set viewport for new tab
2917
+ if (this.browser_profile.window_size) {
2918
+ await newPage.setViewportSize(this.browser_profile.window_size);
2919
+ }
2920
+ // Navigate to blank
2921
+ try {
2922
+ await newPage.goto('about:blank', { waitUntil: 'load', timeout: 5000 });
2923
+ }
2924
+ catch (error) {
2925
+ this.logger.error(`❌ Failed to navigate to about:blank: ${error.message} (something is very wrong or system is extremely overloaded)`);
2926
+ throw error;
2927
+ }
2928
+ // Verify it's responsive
2929
+ if (!(await this._isPageResponsive(newPage, 1.0))) {
2930
+ throw new BrowserError('Browser is unable to load any new about:blank pages (something is very wrong or browser is extremely overloaded)');
2931
+ }
2932
+ }
2933
+ /**
2934
+ * Recover from an unresponsive page by closing and reopening it
2935
+ * @param callingMethod - The name of the method that detected the unresponsive page
2936
+ * @param timeoutMs - Navigation timeout in milliseconds
2937
+ */
2938
+ async _recoverUnresponsivePage(callingMethod, timeoutMs) {
2939
+ this.logger.warning(`⚠️ Page JS engine became unresponsive in ${callingMethod}(), attempting recovery...`);
2940
+ const timeout = Math.min(3000, timeoutMs || this.browser_profile.default_navigation_timeout || 5000);
2941
+ // Check if browser connection is still alive
2942
+ if (this.browser && !this.browser.isConnected()) {
2943
+ this.logger.error('❌ Browser connection lost - browser process may have crashed');
2944
+ throw new Error('Browser connection lost - cannot recover unresponsive page');
2945
+ }
2946
+ // Prevent re-entrance
2947
+ if (this._inRecovery) {
2948
+ this.logger.debug('Already in recovery, skipping nested recovery attempt');
2949
+ return;
2950
+ }
2951
+ this._inRecovery = true;
2952
+ try {
2953
+ // Get current URL before recovery
2954
+ if (!this.agent_current_page) {
2955
+ throw new Error('Agent current page is not set');
2956
+ }
2957
+ const currentUrl = this.agent_current_page.url();
2958
+ // Clear page references
2959
+ const blockedPage = this.agent_current_page;
2960
+ this.agent_current_page = null;
2961
+ if (blockedPage === this.human_current_page) {
2962
+ this.human_current_page = null;
2963
+ }
2964
+ // Force-close the crashed page via CDP
2965
+ this.logger.debug('🪓 Page Recovery Step 1/3: Force-closing crashed page via CDP...');
2966
+ await this._forceClosePageViaCdp(currentUrl);
2967
+ // Remove the closed page from browser_context.pages by forcing a refresh
2968
+ if (this.browser_context && this.browser_context.pages()) {
2969
+ for (const page of this.browser_context.pages().slice()) {
2970
+ const pageUrl = page.url();
2971
+ if (pageUrl === currentUrl &&
2972
+ !page.isClosed() &&
2973
+ !pageUrl.startsWith('about:') &&
2974
+ !pageUrl.startsWith('chrome://') &&
2975
+ !pageUrl.startsWith('edge://')) {
2976
+ try {
2977
+ await page.close();
2978
+ this.logger.debug(`🪓 Closed page because it has a known crash-causing URL: ${pageUrl.substring(0, 50)}`);
2979
+ }
2980
+ catch {
2981
+ // Page might already be closed via CDP
2982
+ }
2983
+ }
2984
+ }
2985
+ }
2986
+ // Try to reopen the URL (in case blocking was transient)
2987
+ this.logger.debug('🍼 Page Recovery Step 2/3: Trying to reopen the URL again...');
2988
+ if (await this._tryReopenUrl(currentUrl, timeout)) {
2989
+ this.logger.debug('✅ Page Recovery Step 3/3: Page loading succeeded after 2nd attempt!');
2990
+ return; // Success!
2991
+ }
2992
+ // If that failed, fall back to blank page
2993
+ this.logger.debug('❌ Page Recovery Step 3/3: Loading the page a 2nd time failed as well, browser seems unable to load this URL without getting stuck, retreating to a safe page...');
2994
+ await this._createBlankFallbackPage(currentUrl);
2995
+ }
2996
+ finally {
2997
+ // Always clear recovery flag
2998
+ this._inRecovery = false;
2999
+ }
3000
+ }
3001
+ // endregion
3002
+ // region - Enhanced CSS Selector Generation
3003
+ /**
3004
+ * Generate enhanced CSS selector for an element
3005
+ * Handles special characters and provides fallback strategies
3006
+ * @param xpath - XPath of the element
3007
+ * @param element - Optional element node for additional context
3008
+ * @returns Enhanced CSS selector string
3009
+ */
3010
+ _enhancedCssSelectorForElement(xpath, element) {
3011
+ // Try to convert XPath to CSS selector
3012
+ const cssSelector = this._xpathToCss(xpath);
3013
+ if (cssSelector) {
3014
+ return cssSelector;
3015
+ }
3016
+ // Fallback: use element attributes if available
3017
+ if (element) {
3018
+ const selectors = [];
3019
+ // Try ID first (most specific)
3020
+ if (element.attributes?.id) {
3021
+ const id = this._escapeSelector(element.attributes.id);
3022
+ selectors.push(`#${id}`);
3023
+ }
3024
+ // Try class names
3025
+ if (element.attributes?.class) {
3026
+ const classes = element.attributes.class
3027
+ .split(/\s+/)
3028
+ .filter((c) => c.length > 0)
3029
+ .map((c) => `.${this._escapeSelector(c)}`)
3030
+ .join('');
3031
+ if (classes) {
3032
+ selectors.push(`${element.tag_name}${classes}`);
3033
+ }
3034
+ }
3035
+ // Try name attribute
3036
+ if (element.attributes?.name) {
3037
+ const name = this._escapeSelector(element.attributes.name);
3038
+ selectors.push(`${element.tag_name}[name="${name}"]`);
3039
+ }
3040
+ // Try data attributes
3041
+ for (const [key, value] of Object.entries(element.attributes || {})) {
3042
+ if (key.startsWith('data-')) {
3043
+ const escaped = this._escapeSelector(String(value));
3044
+ selectors.push(`${element.tag_name}[${key}="${escaped}"]`);
3045
+ }
3046
+ }
3047
+ if (selectors.length > 0) {
3048
+ return selectors[0];
3049
+ }
3050
+ // Last resort: just the tag name
3051
+ return element.tag_name || 'div';
3052
+ }
3053
+ // Ultimate fallback
3054
+ return 'body';
3055
+ }
3056
+ /**
3057
+ * Convert XPath to CSS selector
3058
+ * Handles simple XPath expressions
3059
+ */
3060
+ _xpathToCss(xpath) {
3061
+ try {
3062
+ // Remove leading slashes
3063
+ let path = xpath.replace(/^\/+/, '');
3064
+ // Handle simple cases like /html/body/div[1]/span[2]
3065
+ const parts = path.split('/');
3066
+ const cssparts = [];
3067
+ for (const part of parts) {
3068
+ // Extract tag and index: div[1] -> {tag: 'div', index: 1}
3069
+ const match = part.match(/^([a-zA-Z0-9_-]+)(?:\[(\d+)\])?$/);
3070
+ if (match) {
3071
+ const [, tag, index] = match;
3072
+ if (index) {
3073
+ // CSS uses nth-of-type (1-indexed like XPath)
3074
+ cssparts.push(`${tag}:nth-of-type(${index})`);
3075
+ }
3076
+ else {
3077
+ cssparts.push(tag);
3078
+ }
3079
+ }
3080
+ else {
3081
+ // Complex XPath, can't convert
3082
+ return null;
3083
+ }
3084
+ }
3085
+ return cssparts.join(' > ');
3086
+ }
3087
+ catch {
3088
+ return null;
3089
+ }
3090
+ }
3091
+ /**
3092
+ * Escape special characters in CSS selectors
3093
+ * Handles characters that need escaping in CSS
3094
+ */
3095
+ _escapeSelector(selector) {
3096
+ // Escape special CSS characters
3097
+ return selector.replace(/[!"#$%&'()*+,.\/:;<=>?@\[\\\]^`{|}~]/g, '\\$&');
3098
+ }
3099
+ // endregion
3100
+ // region - User Data Directory Management
3101
+ /**
3102
+ * Prepare user data directory for browser profile
3103
+ * Handles singleton lock conflicts and creates temp profiles if needed
3104
+ */
3105
+ async prepareUserDataDir(userDataDir) {
3106
+ if (!userDataDir) {
3107
+ // Use profile's user data dir or create temp one
3108
+ userDataDir =
3109
+ this.browser_profile.user_data_dir ||
3110
+ (await this._createTempUserDataDir());
3111
+ }
3112
+ // Check for singleton lock conflicts
3113
+ const hasConflict = await this._checkForSingletonLockConflict(userDataDir);
3114
+ if (hasConflict) {
3115
+ this.logger.warning(`Singleton lock detected in ${userDataDir}, falling back to temp profile`);
3116
+ userDataDir = await this._fallbackToTempProfile();
3117
+ }
3118
+ // Ensure directory exists
3119
+ if (!fs.existsSync(userDataDir)) {
3120
+ fs.mkdirSync(userDataDir, { recursive: true });
3121
+ this.logger.debug(`Created user data directory: ${userDataDir}`);
3122
+ }
3123
+ return userDataDir;
3124
+ }
3125
+ /**
3126
+ * Check if user data directory has a singleton lock
3127
+ * This happens when another Chrome instance is using the profile
3128
+ */
3129
+ async _checkForSingletonLockConflict(userDataDir) {
3130
+ try {
3131
+ const singletonLockFile = path.join(userDataDir, 'SingletonLock');
3132
+ const singletonSocketFile = path.join(userDataDir, 'SingletonSocket');
3133
+ const singletonCookieFile = path.join(userDataDir, 'SingletonCookie');
3134
+ // Check if any singleton lock files exist
3135
+ if (fs.existsSync(singletonLockFile) ||
3136
+ fs.existsSync(singletonSocketFile) ||
3137
+ fs.existsSync(singletonCookieFile)) {
3138
+ // Try to detect if process is still alive (Unix-like systems)
3139
+ if (process.platform !== 'win32' && fs.existsSync(singletonLockFile)) {
3140
+ try {
3141
+ // Try to read the lock file to get PID
3142
+ const lockContent = fs.readFileSync(singletonLockFile, 'utf-8');
3143
+ const pidMatch = lockContent.match(/(\d+)/);
3144
+ if (pidMatch) {
3145
+ const pid = parseInt(pidMatch[1], 10);
3146
+ try {
3147
+ // Check if process exists (signal 0 doesn't kill, just checks)
3148
+ process.kill(pid, 0);
3149
+ return true; // Process exists, lock is valid
3150
+ }
3151
+ catch {
3152
+ // Process doesn't exist, stale lock
3153
+ this.logger.debug(`Stale singleton lock detected, removing`);
3154
+ fs.unlinkSync(singletonLockFile);
3155
+ return false;
3156
+ }
3157
+ }
3158
+ }
3159
+ catch {
3160
+ // Couldn't read lock file
3161
+ }
3162
+ }
3163
+ return true;
3164
+ }
3165
+ return false;
3166
+ }
3167
+ catch (error) {
3168
+ this.logger.debug(`Error checking singleton lock: ${error.message}`);
3169
+ return false;
3170
+ }
3171
+ }
3172
+ /**
3173
+ * Fallback to a temporary profile when the primary one is locked
3174
+ */
3175
+ async _fallbackToTempProfile() {
3176
+ const tempDir = await this._createTempUserDataDir();
3177
+ this.logger.info(`Using temporary profile: ${tempDir}`);
3178
+ return tempDir;
3179
+ }
3180
+ /**
3181
+ * Create a temporary user data directory
3182
+ */
3183
+ async _createTempUserDataDir() {
3184
+ const osTempDir = require('os').tmpdir();
3185
+ const tempDir = path.join(osTempDir, `browser-use-${Date.now()}-${Math.random().toString(36).slice(2)}`);
3186
+ fs.mkdirSync(tempDir, { recursive: true });
3187
+ return tempDir;
3188
+ }
3189
+ // endregion
3190
+ // region - Page Visibility Listeners
3191
+ /**
3192
+ * Setup listeners for page visibility changes
3193
+ * Tracks when user switches tabs to update human_current_page
3194
+ */
3195
+ async _setupCurrentPageChangeListeners() {
3196
+ if (!this.browser_context) {
3197
+ return;
3198
+ }
3199
+ // Listen for page events to track which page the user is viewing
3200
+ this.browser_context.on?.('page', (page) => {
3201
+ this.logger.debug(`New page created: ${page.url?.() || 'about:blank'}`);
3202
+ // Note: 'visibilitychange' is not a standard Playwright page event
3203
+ // Visibility tracking would need to be implemented differently
3204
+ // (e.g., through page.evaluate polling or browser context events)
3205
+ // Track new page
3206
+ if (page.url && !page.url().startsWith('about:')) {
3207
+ this.human_current_page = page;
3208
+ }
3209
+ });
3210
+ }
3211
+ /**
3212
+ * Callback when tab visibility changes
3213
+ * Updates human_current_page to reflect which tab the user is viewing
3214
+ */
3215
+ _onTabVisibilityChange(page) {
3216
+ try {
3217
+ // Check if page is visible
3218
+ page
3219
+ .evaluate?.(() => document.visibilityState === 'visible')
3220
+ .then((isVisible) => {
3221
+ if (isVisible) {
3222
+ this.logger.debug(`Tab became visible: ${page.url?.() || 'unknown'}`);
3223
+ this.human_current_page = page;
3224
+ }
3225
+ })
3226
+ .catch(() => {
3227
+ // Ignore errors from closed pages
3228
+ });
3229
+ }
3230
+ catch {
3231
+ // Ignore errors
3232
+ }
3233
+ }
3234
+ // endregion
3235
+ // region - Process Management
3236
+ /**
3237
+ * Kill all child processes spawned by this browser session
3238
+ */
3239
+ async _killChildProcesses() {
3240
+ if (this._childProcesses.size === 0) {
3241
+ return;
3242
+ }
3243
+ this.logger.debug(`Killing ${this._childProcesses.size} child processes`);
3244
+ for (const pid of this._childProcesses) {
3245
+ try {
3246
+ // Try to kill the process
3247
+ process.kill(pid, 'SIGTERM');
3248
+ this.logger.debug(`Sent SIGTERM to process ${pid}`);
3249
+ // Wait briefly and check if still alive
3250
+ await new Promise((resolve) => setTimeout(resolve, 500));
3251
+ try {
3252
+ // Check if process still exists
3253
+ process.kill(pid, 0);
3254
+ // If we get here, process is still alive, force kill
3255
+ process.kill(pid, 'SIGKILL');
3256
+ this.logger.debug(`Sent SIGKILL to process ${pid}`);
3257
+ }
3258
+ catch {
3259
+ // Process is dead, ignore
3260
+ }
3261
+ }
3262
+ catch (error) {
3263
+ // Process doesn't exist or we don't have permission
3264
+ this.logger.debug(`Could not kill process ${pid}: ${error.message}`);
3265
+ }
3266
+ }
3267
+ this._childProcesses.clear();
3268
+ }
3269
+ /**
3270
+ * Terminate the browser process and all its children
3271
+ */
3272
+ async _terminateBrowserProcess() {
3273
+ if (!this.browser_pid) {
3274
+ return;
3275
+ }
3276
+ try {
3277
+ this.logger.debug(`Terminating browser process ${this.browser_pid}`);
3278
+ // Platform-specific process tree termination
3279
+ if (process.platform === 'win32') {
3280
+ // Windows: use taskkill to kill process tree
3281
+ await execAsync(`taskkill /PID ${this.browser_pid} /T /F`).catch(() => {
3282
+ // Ignore errors if process already dead
3283
+ });
3284
+ }
3285
+ else {
3286
+ // Unix-like: kill process group
3287
+ try {
3288
+ // Try to kill the process group
3289
+ process.kill(-this.browser_pid, 'SIGTERM');
3290
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3291
+ // Check if still alive and force kill if needed
3292
+ try {
3293
+ process.kill(-this.browser_pid, 0);
3294
+ process.kill(-this.browser_pid, 'SIGKILL');
3295
+ }
3296
+ catch {
3297
+ // Process is dead
3298
+ }
3299
+ }
3300
+ catch {
3301
+ // Fallback to killing just the process
3302
+ try {
3303
+ process.kill(this.browser_pid, 'SIGTERM');
3304
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3305
+ process.kill(this.browser_pid, 'SIGKILL');
3306
+ }
3307
+ catch {
3308
+ // Process doesn't exist
3309
+ }
3310
+ }
3311
+ }
3312
+ }
3313
+ catch (error) {
3314
+ this.logger.debug(`Error terminating browser process: ${error.message}`);
3315
+ }
3316
+ }
3317
+ /**
3318
+ * Get child processes of a given PID
3319
+ * Cross-platform implementation using ps on Unix-like systems and WMIC on Windows
3320
+ */
3321
+ async _getChildProcesses(pid) {
3322
+ try {
3323
+ if (process.platform === 'win32') {
3324
+ // Windows: use WMIC
3325
+ const { stdout } = await execAsync(`wmic process where (ParentProcessId=${pid}) get ProcessId`);
3326
+ const pids = stdout
3327
+ .split('\n')
3328
+ .slice(1) // Skip header
3329
+ .map((line) => parseInt(line.trim(), 10))
3330
+ .filter((p) => !isNaN(p));
3331
+ return pids;
3332
+ }
3333
+ else {
3334
+ // Unix-like: use ps
3335
+ const { stdout } = await execAsync(`ps -o pid= --ppid ${pid}`);
3336
+ const pids = stdout
3337
+ .split('\n')
3338
+ .map((line) => parseInt(line.trim(), 10))
3339
+ .filter((p) => !isNaN(p));
3340
+ return pids;
3341
+ }
3342
+ }
3343
+ catch {
3344
+ return [];
3345
+ }
3346
+ }
3347
+ /**
3348
+ * Track a child process
3349
+ */
3350
+ _trackChildProcess(pid) {
3351
+ this._childProcesses.add(pid);
3352
+ }
3353
+ /**
3354
+ * Untrack a child process
3355
+ */
3356
+ _untrackChildProcess(pid) {
3357
+ this._childProcesses.delete(pid);
3358
+ }
3359
+ // region: Loading Animations
3360
+ /**
3361
+ * Show DVD screensaver loading animation
3362
+ * Returns a function to stop the animation
3363
+ *
3364
+ * @param message - Message to display (default: 'Loading...')
3365
+ * @param fps - Frames per second (default: 10)
3366
+ * @returns Function to stop the animation
3367
+ *
3368
+ * @example
3369
+ * const stopAnimation = this._showDvdScreensaverLoadingAnimation('Loading page...');
3370
+ * await someLongOperation();
3371
+ * stopAnimation();
3372
+ */
3373
+ _showDvdScreensaverLoadingAnimation(message = 'Loading...', fps = 10) {
3374
+ return showDVDScreensaver(message, fps);
3375
+ }
3376
+ /**
3377
+ * Show simple spinner loading animation
3378
+ * Returns a function to stop the animation
3379
+ *
3380
+ * @param message - Message to display (default: 'Loading...')
3381
+ * @param fps - Frames per second (default: 10)
3382
+ * @returns Function to stop the animation
3383
+ *
3384
+ * @example
3385
+ * const stopSpinner = this._showSpinnerLoadingAnimation('Processing...');
3386
+ * await someLongOperation();
3387
+ * stopSpinner();
3388
+ */
3389
+ _showSpinnerLoadingAnimation(message = 'Loading...', fps = 10) {
3390
+ return showSpinner(message, fps);
3391
+ }
3392
+ /**
3393
+ * Execute an async operation with DVD screensaver animation
3394
+ *
3395
+ * @param operation - Async operation to execute
3396
+ * @param message - Message to display during operation
3397
+ * @returns Result of the operation
3398
+ *
3399
+ * @example
3400
+ * const page = await this._withDvdScreensaver(
3401
+ * async () => await this.browser_context!.newPage(),
3402
+ * 'Opening new page...'
3403
+ * );
3404
+ */
3405
+ async _withDvdScreensaver(operation, message = 'Loading...') {
3406
+ return withDVDScreensaver(operation, message);
3407
+ }
3408
+ }
3409
+ export { DEFAULT_BROWSER_PROFILE };