browser-use 0.2.0 → 0.4.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 (259) hide show
  1. package/README.md +295 -686
  2. package/dist/actor/element.d.ts +19 -0
  3. package/dist/actor/element.js +46 -0
  4. package/dist/actor/index.d.ts +4 -0
  5. package/dist/actor/index.js +4 -0
  6. package/dist/actor/mouse.d.ts +19 -0
  7. package/dist/actor/mouse.js +39 -0
  8. package/dist/actor/page.d.ts +29 -0
  9. package/dist/actor/page.js +88 -0
  10. package/dist/actor/utils.d.ts +4 -0
  11. package/dist/actor/utils.js +35 -0
  12. package/dist/agent/cloud-events.d.ts +18 -0
  13. package/dist/agent/cloud-events.js +65 -2
  14. package/dist/agent/gif.d.ts +1 -0
  15. package/dist/agent/gif.js +24 -2
  16. package/dist/agent/judge.d.ts +17 -0
  17. package/dist/agent/judge.js +197 -0
  18. package/dist/agent/message-manager/service.d.ts +12 -4
  19. package/dist/agent/message-manager/service.js +205 -39
  20. package/dist/agent/message-manager/utils.js +0 -1
  21. package/dist/agent/message-manager/views.d.ts +4 -0
  22. package/dist/agent/message-manager/views.js +11 -7
  23. package/dist/agent/prompts.d.ts +24 -3
  24. package/dist/agent/prompts.js +274 -59
  25. package/dist/agent/service.d.ts +103 -41
  26. package/dist/agent/service.js +2336 -472
  27. package/dist/agent/variable-detector.d.ts +12 -0
  28. package/dist/agent/variable-detector.js +211 -0
  29. package/dist/agent/views.d.ts +237 -18
  30. package/dist/agent/views.js +446 -33
  31. package/dist/browser/cloud/cloud.d.ts +20 -0
  32. package/dist/browser/cloud/cloud.js +129 -0
  33. package/dist/browser/cloud/index.d.ts +2 -0
  34. package/dist/browser/cloud/index.js +2 -0
  35. package/dist/browser/cloud/views.d.ts +41 -0
  36. package/dist/browser/cloud/views.js +35 -0
  37. package/dist/browser/events.d.ts +345 -0
  38. package/dist/browser/events.js +566 -0
  39. package/dist/browser/extensions.js +17 -17
  40. package/dist/browser/index.d.ts +4 -0
  41. package/dist/browser/index.js +4 -0
  42. package/dist/browser/profile.d.ts +10 -4
  43. package/dist/browser/profile.js +79 -12
  44. package/dist/browser/session-manager.d.ts +85 -0
  45. package/dist/browser/session-manager.js +208 -0
  46. package/dist/browser/session.d.ts +105 -9
  47. package/dist/browser/session.js +1166 -95
  48. package/dist/browser/types.d.ts +153 -156
  49. package/dist/browser/views.d.ts +39 -0
  50. package/dist/browser/views.js +32 -0
  51. package/dist/browser/watchdogs/aboutblank-watchdog.d.ts +12 -0
  52. package/dist/browser/watchdogs/aboutblank-watchdog.js +131 -0
  53. package/dist/browser/watchdogs/base.d.ts +21 -0
  54. package/dist/browser/watchdogs/base.js +81 -0
  55. package/dist/browser/watchdogs/cdp-session-watchdog.d.ts +14 -0
  56. package/dist/browser/watchdogs/cdp-session-watchdog.js +177 -0
  57. package/dist/browser/watchdogs/crash-watchdog.d.ts +38 -0
  58. package/dist/browser/watchdogs/crash-watchdog.js +296 -0
  59. package/dist/browser/watchdogs/default-action-watchdog.d.ts +49 -0
  60. package/dist/browser/watchdogs/default-action-watchdog.js +212 -0
  61. package/dist/browser/watchdogs/dom-watchdog.d.ts +8 -0
  62. package/dist/browser/watchdogs/dom-watchdog.js +31 -0
  63. package/dist/browser/watchdogs/downloads-watchdog.d.ts +77 -0
  64. package/dist/browser/watchdogs/downloads-watchdog.js +409 -0
  65. package/dist/browser/watchdogs/har-recording-watchdog.d.ts +19 -0
  66. package/dist/browser/watchdogs/har-recording-watchdog.js +317 -0
  67. package/dist/browser/watchdogs/index.d.ts +15 -0
  68. package/dist/browser/watchdogs/index.js +15 -0
  69. package/dist/browser/watchdogs/local-browser-watchdog.d.ts +10 -0
  70. package/dist/browser/watchdogs/local-browser-watchdog.js +32 -0
  71. package/dist/browser/watchdogs/permissions-watchdog.d.ts +8 -0
  72. package/dist/browser/watchdogs/permissions-watchdog.js +73 -0
  73. package/dist/browser/watchdogs/popups-watchdog.d.ts +13 -0
  74. package/dist/browser/watchdogs/popups-watchdog.js +77 -0
  75. package/dist/browser/watchdogs/recording-watchdog.d.ts +27 -0
  76. package/dist/browser/watchdogs/recording-watchdog.js +249 -0
  77. package/dist/browser/watchdogs/screenshot-watchdog.d.ts +6 -0
  78. package/dist/browser/watchdogs/screenshot-watchdog.js +13 -0
  79. package/dist/browser/watchdogs/security-watchdog.d.ts +10 -0
  80. package/dist/browser/watchdogs/security-watchdog.js +84 -0
  81. package/dist/browser/watchdogs/storage-state-watchdog.d.ts +24 -0
  82. package/dist/browser/watchdogs/storage-state-watchdog.js +288 -0
  83. package/dist/cli.d.ts +7 -2
  84. package/dist/cli.js +182 -25
  85. package/dist/code-use/formatting.d.ts +3 -0
  86. package/dist/code-use/formatting.js +18 -0
  87. package/dist/code-use/index.d.ts +6 -0
  88. package/dist/code-use/index.js +6 -0
  89. package/dist/code-use/namespace.d.ts +5 -0
  90. package/dist/code-use/namespace.js +81 -0
  91. package/dist/code-use/notebook-export.d.ts +3 -0
  92. package/dist/code-use/notebook-export.js +56 -0
  93. package/dist/code-use/service.d.ts +24 -0
  94. package/dist/code-use/service.js +104 -0
  95. package/dist/code-use/utils.d.ts +4 -0
  96. package/dist/code-use/utils.js +98 -0
  97. package/dist/code-use/views.d.ts +108 -0
  98. package/dist/code-use/views.js +165 -0
  99. package/dist/config.d.ts +15 -0
  100. package/dist/config.js +109 -7
  101. package/dist/controller/registry/service.d.ts +10 -1
  102. package/dist/controller/registry/service.js +266 -10
  103. package/dist/controller/registry/views.d.ts +4 -1
  104. package/dist/controller/registry/views.js +25 -2
  105. package/dist/controller/service.d.ts +10 -1
  106. package/dist/controller/service.js +1814 -268
  107. package/dist/controller/views.d.ts +78 -155
  108. package/dist/controller/views.js +61 -12
  109. package/dist/dom/history-tree-processor/service.d.ts +5 -0
  110. package/dist/dom/history-tree-processor/service.js +169 -14
  111. package/dist/dom/history-tree-processor/view.d.ts +7 -1
  112. package/dist/dom/history-tree-processor/view.js +10 -1
  113. package/dist/dom/markdown-extractor.d.ts +37 -0
  114. package/dist/dom/markdown-extractor.js +345 -0
  115. package/dist/dom/service.d.ts +3 -1
  116. package/dist/dom/service.js +76 -0
  117. package/dist/dom/views.d.ts +1 -0
  118. package/dist/dom/views.js +45 -0
  119. package/dist/event-bus.d.ts +107 -7
  120. package/dist/event-bus.js +313 -10
  121. package/dist/exceptions.d.ts +0 -3
  122. package/dist/exceptions.js +0 -7
  123. package/dist/filesystem/file-system.d.ts +18 -0
  124. package/dist/filesystem/file-system.js +503 -42
  125. package/dist/index.d.ts +7 -0
  126. package/dist/index.js +6 -0
  127. package/dist/integrations/gmail/actions.d.ts +3 -3
  128. package/dist/integrations/gmail/actions.js +4 -4
  129. package/dist/llm/anthropic/chat.d.ts +18 -1
  130. package/dist/llm/anthropic/chat.js +123 -55
  131. package/dist/llm/anthropic/serializer.d.ts +2 -0
  132. package/dist/llm/anthropic/serializer.js +81 -9
  133. package/dist/llm/aws/chat-anthropic.d.ts +17 -0
  134. package/dist/llm/aws/chat-anthropic.js +126 -26
  135. package/dist/llm/aws/chat-bedrock.d.ts +28 -1
  136. package/dist/llm/aws/chat-bedrock.js +161 -34
  137. package/dist/llm/aws/serializer.d.ts +13 -1
  138. package/dist/llm/aws/serializer.js +56 -17
  139. package/dist/llm/azure/chat.d.ts +53 -2
  140. package/dist/llm/azure/chat.js +366 -54
  141. package/dist/llm/base.d.ts +2 -0
  142. package/dist/llm/browser-use/chat.d.ts +40 -0
  143. package/dist/llm/browser-use/chat.js +305 -0
  144. package/dist/llm/browser-use/index.d.ts +1 -0
  145. package/dist/llm/browser-use/index.js +1 -0
  146. package/dist/llm/cerebras/chat.d.ts +39 -0
  147. package/dist/llm/cerebras/chat.js +178 -0
  148. package/dist/llm/cerebras/index.d.ts +2 -0
  149. package/dist/llm/cerebras/index.js +2 -0
  150. package/dist/llm/cerebras/serializer.d.ts +7 -0
  151. package/dist/llm/cerebras/serializer.js +82 -0
  152. package/dist/llm/deepseek/chat.d.ts +19 -2
  153. package/dist/llm/deepseek/chat.js +138 -25
  154. package/dist/llm/google/chat.d.ts +46 -2
  155. package/dist/llm/google/chat.js +267 -64
  156. package/dist/llm/google/serializer.d.ts +9 -1
  157. package/dist/llm/google/serializer.js +141 -34
  158. package/dist/llm/groq/chat.d.ts +21 -2
  159. package/dist/llm/groq/chat.js +125 -26
  160. package/dist/llm/groq/parser.js +3 -1
  161. package/dist/llm/mistral/chat.d.ts +43 -0
  162. package/dist/llm/mistral/chat.js +154 -0
  163. package/dist/llm/mistral/index.d.ts +2 -0
  164. package/dist/llm/mistral/index.js +2 -0
  165. package/dist/llm/mistral/schema.d.ts +8 -0
  166. package/dist/llm/mistral/schema.js +27 -0
  167. package/dist/llm/models.d.ts +2 -0
  168. package/dist/llm/models.js +317 -0
  169. package/dist/llm/ollama/chat.d.ts +13 -1
  170. package/dist/llm/ollama/chat.js +110 -19
  171. package/dist/llm/ollama/serializer.d.ts +1 -0
  172. package/dist/llm/ollama/serializer.js +34 -12
  173. package/dist/llm/openai/chat.d.ts +16 -0
  174. package/dist/llm/openai/chat.js +94 -44
  175. package/dist/llm/openai/like.d.ts +5 -3
  176. package/dist/llm/openai/like.js +7 -3
  177. package/dist/llm/openai/responses-serializer.d.ts +18 -0
  178. package/dist/llm/openai/responses-serializer.js +72 -0
  179. package/dist/llm/openrouter/chat.d.ts +28 -2
  180. package/dist/llm/openrouter/chat.js +115 -29
  181. package/dist/llm/schema.d.ts +11 -1
  182. package/dist/llm/schema.js +109 -4
  183. package/dist/llm/vercel/chat.d.ts +50 -0
  184. package/dist/llm/vercel/chat.js +276 -0
  185. package/dist/llm/vercel/index.d.ts +1 -0
  186. package/dist/llm/vercel/index.js +1 -0
  187. package/dist/llm/vercel/serializer.d.ts +5 -0
  188. package/dist/llm/vercel/serializer.js +7 -0
  189. package/dist/llm/views.d.ts +2 -1
  190. package/dist/llm/views.js +3 -1
  191. package/dist/logging-config.d.ts +2 -0
  192. package/dist/logging-config.js +82 -29
  193. package/dist/mcp/client.d.ts +10 -5
  194. package/dist/mcp/client.js +14 -9
  195. package/dist/mcp/controller.d.ts +42 -3
  196. package/dist/mcp/controller.js +56 -31
  197. package/dist/mcp/server.d.ts +15 -0
  198. package/dist/mcp/server.js +261 -52
  199. package/dist/observability.js +10 -4
  200. package/dist/sandbox/index.d.ts +2 -0
  201. package/dist/sandbox/index.js +2 -0
  202. package/dist/sandbox/sandbox.d.ts +19 -0
  203. package/dist/sandbox/sandbox.js +140 -0
  204. package/dist/sandbox/views.d.ts +67 -0
  205. package/dist/sandbox/views.js +121 -0
  206. package/dist/skill-cli/index.d.ts +3 -0
  207. package/dist/skill-cli/index.js +3 -0
  208. package/dist/skill-cli/protocol.d.ts +30 -0
  209. package/dist/skill-cli/protocol.js +48 -0
  210. package/dist/skill-cli/server.d.ts +11 -0
  211. package/dist/skill-cli/server.js +85 -0
  212. package/dist/skill-cli/sessions.d.ts +24 -0
  213. package/dist/skill-cli/sessions.js +47 -0
  214. package/dist/skills/index.d.ts +3 -0
  215. package/dist/skills/index.js +3 -0
  216. package/dist/skills/service.d.ts +27 -0
  217. package/dist/skills/service.js +266 -0
  218. package/dist/skills/utils.d.ts +6 -0
  219. package/dist/skills/utils.js +53 -0
  220. package/dist/skills/views.d.ts +40 -0
  221. package/dist/skills/views.js +10 -0
  222. package/dist/sync/auth.js +8 -3
  223. package/dist/sync/service.d.ts +6 -6
  224. package/dist/sync/service.js +54 -89
  225. package/dist/telemetry/views.d.ts +20 -6
  226. package/dist/telemetry/views.js +23 -5
  227. package/dist/tokens/custom-pricing.d.ts +2 -0
  228. package/dist/tokens/custom-pricing.js +22 -0
  229. package/dist/tokens/index.d.ts +2 -0
  230. package/dist/tokens/index.js +2 -0
  231. package/dist/tokens/mappings.d.ts +1 -0
  232. package/dist/tokens/mappings.js +3 -0
  233. package/dist/tokens/service.js +27 -8
  234. package/dist/tools/extraction/index.d.ts +2 -0
  235. package/dist/tools/extraction/index.js +2 -0
  236. package/dist/tools/extraction/schema-utils.d.ts +6 -0
  237. package/dist/tools/extraction/schema-utils.js +237 -0
  238. package/dist/tools/extraction/views.d.ts +7 -0
  239. package/dist/tools/index.d.ts +5 -0
  240. package/dist/tools/index.js +5 -0
  241. package/dist/tools/registry/index.d.ts +2 -0
  242. package/dist/tools/registry/index.js +2 -0
  243. package/dist/tools/registry/service.d.ts +1 -0
  244. package/dist/tools/registry/service.js +1 -0
  245. package/dist/tools/registry/views.d.ts +1 -0
  246. package/dist/tools/registry/views.js +1 -0
  247. package/dist/tools/service.d.ts +2 -0
  248. package/dist/tools/service.js +1 -0
  249. package/dist/tools/utils.d.ts +2 -0
  250. package/dist/tools/utils.js +57 -0
  251. package/dist/tools/views.d.ts +1 -0
  252. package/dist/tools/views.js +1 -0
  253. package/dist/utils.d.ts +10 -1
  254. package/dist/utils.js +70 -3
  255. package/package.json +116 -49
  256. package/dist/dom/playground/process-dom.js +0 -5
  257. package/dist/dom/playground/test-accessibility.d.ts +0 -44
  258. package/dist/dom/playground/test-accessibility.js +0 -111
  259. /package/dist/{dom/playground/process-dom.d.ts → tools/extraction/views.js} +0 -0
@@ -1,18 +1,36 @@
1
1
  import fs from 'node:fs';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
- import { exec } from 'node:child_process';
4
+ import { isIP } from 'node:net';
5
+ import { execFile } from 'node:child_process';
5
6
  import { promisify } from 'node:util';
6
7
  import { createLogger } from '../logging-config.js';
7
8
  import { match_url_with_domain_pattern, uuid7str } from '../utils.js';
9
+ import { EventBus, } from '../event-bus.js';
8
10
  import { async_playwright, } from './types.js';
9
11
  import { BrowserProfile, CHROME_DOCKER_ARGS, DEFAULT_BROWSER_PROFILE, } from './profile.js';
10
- import { BrowserStateSummary, BrowserError } from './views.js';
12
+ import { BrowserStateSummary, BrowserError, URLNotAllowedError, } from './views.js';
13
+ import { AgentFocusChangedEvent, BrowserConnectedEvent, BrowserLaunchEvent, BrowserStartEvent, BrowserStoppedEvent, BrowserStopEvent, DialogOpenedEvent, DownloadProgressEvent, DownloadStartedEvent, FileDownloadedEvent, TabClosedEvent, TabCreatedEvent, } from './events.js';
11
14
  import { DOMElementNode, DOMState } from '../dom/views.js';
12
15
  import { normalize_url } from './utils.js';
13
16
  import { DomService } from '../dom/service.js';
14
17
  import { showDVDScreensaver, showSpinner, withDVDScreensaver, } from './dvd-screensaver.js';
15
- const execAsync = promisify(exec);
18
+ import { SessionManager } from './session-manager.js';
19
+ import { AboutBlankWatchdog } from './watchdogs/aboutblank-watchdog.js';
20
+ import { CDPSessionWatchdog } from './watchdogs/cdp-session-watchdog.js';
21
+ import { CrashWatchdog } from './watchdogs/crash-watchdog.js';
22
+ import { DefaultActionWatchdog } from './watchdogs/default-action-watchdog.js';
23
+ import { DOMWatchdog } from './watchdogs/dom-watchdog.js';
24
+ import { DownloadsWatchdog } from './watchdogs/downloads-watchdog.js';
25
+ import { HarRecordingWatchdog } from './watchdogs/har-recording-watchdog.js';
26
+ import { LocalBrowserWatchdog } from './watchdogs/local-browser-watchdog.js';
27
+ import { PermissionsWatchdog } from './watchdogs/permissions-watchdog.js';
28
+ import { PopupsWatchdog } from './watchdogs/popups-watchdog.js';
29
+ import { RecordingWatchdog } from './watchdogs/recording-watchdog.js';
30
+ import { ScreenshotWatchdog } from './watchdogs/screenshot-watchdog.js';
31
+ import { SecurityWatchdog } from './watchdogs/security-watchdog.js';
32
+ import { StorageStateWatchdog } from './watchdogs/storage-state-watchdog.js';
33
+ const execFileAsync = promisify(execFile);
16
34
  const createEmptyDomState = () => {
17
35
  const root = new DOMElementNode(true, null, 'html', '/html[1]', {}, []);
18
36
  return new DOMState(root, {});
@@ -20,6 +38,8 @@ const createEmptyDomState = () => {
20
38
  export class BrowserSession {
21
39
  id;
22
40
  browser_profile;
41
+ event_bus;
42
+ session_manager;
23
43
  browser;
24
44
  browser_context;
25
45
  agent_current_page;
@@ -39,6 +59,8 @@ export class BrowserSession {
39
59
  currentTabIndex = 0;
40
60
  historyStack = [];
41
61
  downloaded_files = [];
62
+ llm_screenshot_size = null;
63
+ _original_viewport_size = null;
42
64
  ownsBrowserResources = true;
43
65
  _autoDownloadPdfs = true;
44
66
  tabPages = new Map();
@@ -48,6 +70,13 @@ export class BrowserSession {
48
70
  attachedAgentId = null;
49
71
  attachedSharedAgentIds = new Set();
50
72
  _stoppingPromise = null;
73
+ _closedPopupMessages = [];
74
+ _dialogHandlersAttached = new WeakSet();
75
+ _maxClosedPopupMessages = 20;
76
+ _recentEvents = [];
77
+ _maxRecentEvents = 100;
78
+ _watchdogs = new Set();
79
+ _defaultWatchdogsAttached = false;
51
80
  constructor(init = {}) {
52
81
  const sourceProfileConfig = init.browser_profile
53
82
  ? typeof structuredClone === 'function'
@@ -56,6 +85,8 @@ export class BrowserSession {
56
85
  : (init.profile ?? {});
57
86
  this.browser_profile = new BrowserProfile(sourceProfileConfig);
58
87
  this.id = init.id ?? uuid7str();
88
+ this.event_bus = new EventBus(`BrowserSession_${this.id.slice(-4)}`);
89
+ this.session_manager = new SessionManager();
59
90
  this.browser = init.browser ?? null;
60
91
  this.browser_context = init.browser_context ?? null;
61
92
  this.agent_current_page = init.page ?? null;
@@ -69,20 +100,122 @@ export class BrowserSession {
69
100
  this.downloaded_files = Array.isArray(init.downloaded_files)
70
101
  ? [...init.downloaded_files]
71
102
  : [];
103
+ this._closedPopupMessages = Array.isArray(init.closed_popup_messages)
104
+ ? [...init.closed_popup_messages]
105
+ : [];
72
106
  if (typeof init?.auto_download_pdfs === 'boolean') {
73
107
  this._autoDownloadPdfs = Boolean(init.auto_download_pdfs);
74
108
  }
109
+ const initialPageId = this._tabCounter++;
75
110
  this._tabs = [
76
- {
77
- page_id: this._tabCounter++,
111
+ this._createTabInfo({
112
+ page_id: initialPageId,
78
113
  url: this.currentUrl,
79
114
  title: this.currentTitle || this.currentUrl,
80
- parent_page_id: null,
81
- },
115
+ }),
82
116
  ];
83
117
  this.historyStack.push(this.currentUrl);
84
118
  this.ownsBrowserResources = this._determineOwnership();
85
119
  this.tabPages.set(this._tabs[0].page_id, this.agent_current_page ?? null);
120
+ this._syncSessionManagerFromTabs();
121
+ this._attachDialogHandler(this.agent_current_page);
122
+ this._recordRecentEvent('session_initialized', { url: this.currentUrl });
123
+ }
124
+ attach_watchdog(watchdog) {
125
+ if (this._watchdogs.has(watchdog)) {
126
+ return;
127
+ }
128
+ watchdog.attach_to_session();
129
+ this._watchdogs.add(watchdog);
130
+ }
131
+ attach_watchdogs(watchdogs) {
132
+ for (const watchdog of watchdogs) {
133
+ this.attach_watchdog(watchdog);
134
+ }
135
+ }
136
+ detach_watchdog(watchdog) {
137
+ if (!this._watchdogs.has(watchdog)) {
138
+ return;
139
+ }
140
+ watchdog.detach_from_session();
141
+ this._watchdogs.delete(watchdog);
142
+ }
143
+ detach_all_watchdogs() {
144
+ for (const watchdog of [...this._watchdogs]) {
145
+ this.detach_watchdog(watchdog);
146
+ }
147
+ this._defaultWatchdogsAttached = false;
148
+ }
149
+ get_watchdogs() {
150
+ return [...this._watchdogs];
151
+ }
152
+ async dispatch_browser_event(event, options = {}) {
153
+ return this.event_bus.dispatch_or_throw(event, options);
154
+ }
155
+ async launch() {
156
+ this.attach_default_watchdogs();
157
+ const dispatchResult = await this.dispatch_browser_event(new BrowserLaunchEvent());
158
+ const eventResult = dispatchResult.event.event_result;
159
+ return {
160
+ cdp_url: eventResult?.cdp_url ?? this.cdp_url ?? this.wss_url ?? 'playwright',
161
+ };
162
+ }
163
+ attach_default_watchdogs() {
164
+ if (this._defaultWatchdogsAttached) {
165
+ return;
166
+ }
167
+ const watchdogs = [
168
+ new LocalBrowserWatchdog({ browser_session: this }),
169
+ new CDPSessionWatchdog({ browser_session: this }),
170
+ new CrashWatchdog({ browser_session: this }),
171
+ new AboutBlankWatchdog({ browser_session: this }),
172
+ new PermissionsWatchdog({ browser_session: this }),
173
+ new PopupsWatchdog({ browser_session: this }),
174
+ new SecurityWatchdog({ browser_session: this }),
175
+ new DOMWatchdog({ browser_session: this }),
176
+ new ScreenshotWatchdog({ browser_session: this }),
177
+ new RecordingWatchdog({ browser_session: this }),
178
+ new DownloadsWatchdog({ browser_session: this }),
179
+ new StorageStateWatchdog({ browser_session: this }),
180
+ new DefaultActionWatchdog({ browser_session: this }),
181
+ ];
182
+ const configuredHarPath = this.browser_profile.config.record_har_path;
183
+ if (typeof configuredHarPath === 'string' &&
184
+ configuredHarPath.trim().length > 0) {
185
+ watchdogs.push(new HarRecordingWatchdog({ browser_session: this }));
186
+ }
187
+ this.attach_watchdogs(watchdogs);
188
+ this._defaultWatchdogsAttached = true;
189
+ }
190
+ _formatTabId(pageId) {
191
+ const normalized = Number.isFinite(pageId) && pageId >= 0 ? Math.floor(pageId) : 0;
192
+ return String(normalized).padStart(4, '0').slice(-4);
193
+ }
194
+ _createTabInfo({ page_id, url, title, parent_page_id = null, }) {
195
+ return {
196
+ page_id,
197
+ tab_id: this._formatTabId(page_id),
198
+ target_id: this._buildSyntheticTargetId(page_id),
199
+ url,
200
+ title,
201
+ parent_page_id,
202
+ };
203
+ }
204
+ _buildSyntheticTargetId(pageId) {
205
+ return `tab_${this.id.slice(-4)}_${this._formatTabId(pageId)}`;
206
+ }
207
+ _syncSessionManagerFromTabs() {
208
+ this.session_manager.sync_tabs(this._tabs, this.currentTabIndex, (page_id) => this._buildSyntheticTargetId(page_id));
209
+ }
210
+ async get_or_create_cdp_session(page = null) {
211
+ if (!this.browser_context?.newCDPSession) {
212
+ throw new Error('CDP sessions are not available for this browser context');
213
+ }
214
+ const targetPage = page ?? (await this.get_current_page());
215
+ if (!targetPage) {
216
+ throw new Error('No active page available to create CDP session');
217
+ }
218
+ return this.browser_context.newCDPSession(targetPage);
86
219
  }
87
220
  async _waitForStableNetwork(page, signal = null) {
88
221
  const pendingRequests = new Set();
@@ -262,9 +395,193 @@ export class BrowserSession {
262
395
  const currentTab = this._tabs[this.currentTabIndex];
263
396
  if (currentTab) {
264
397
  this.tabPages.set(currentTab.page_id, page ?? null);
398
+ this.session_manager.set_focused_target(currentTab.target_id ?? null);
265
399
  }
400
+ this._attachDialogHandler(page);
266
401
  this.agent_current_page = page ?? null;
267
402
  }
403
+ _captureClosedPopupMessage(dialogType, message) {
404
+ const normalizedType = String(dialogType || 'alert').trim() || 'alert';
405
+ const normalizedMessage = String(message || '').trim();
406
+ if (!normalizedMessage) {
407
+ return;
408
+ }
409
+ const formatted = `[${normalizedType}] ${normalizedMessage}`;
410
+ this._closedPopupMessages.push(formatted);
411
+ if (this._closedPopupMessages.length > this._maxClosedPopupMessages) {
412
+ this._closedPopupMessages.splice(0, this._closedPopupMessages.length - this._maxClosedPopupMessages);
413
+ }
414
+ }
415
+ _getClosedPopupMessagesSnapshot() {
416
+ return [...this._closedPopupMessages];
417
+ }
418
+ _recordRecentEvent(event_type, details = {}) {
419
+ const event = {
420
+ event_type: String(event_type || 'unknown').trim() || 'unknown',
421
+ timestamp: new Date().toISOString(),
422
+ };
423
+ if (typeof details.url === 'string' && details.url.trim()) {
424
+ event.url = details.url.trim();
425
+ }
426
+ if (typeof details.error_message === 'string' &&
427
+ details.error_message.trim()) {
428
+ event.error_message = details.error_message.trim();
429
+ }
430
+ if (typeof details.page_id === 'number' &&
431
+ Number.isFinite(details.page_id)) {
432
+ event.page_id = details.page_id;
433
+ }
434
+ if (typeof details.tab_id === 'string' && details.tab_id.trim()) {
435
+ event.tab_id = details.tab_id.trim();
436
+ }
437
+ this._recentEvents.push(event);
438
+ if (this._recentEvents.length > this._maxRecentEvents) {
439
+ this._recentEvents.splice(0, this._recentEvents.length - this._maxRecentEvents);
440
+ }
441
+ }
442
+ _getRecentEventsSummary(limit = 10) {
443
+ if (!this._recentEvents.length || limit <= 0) {
444
+ return null;
445
+ }
446
+ const events = this._recentEvents.slice(-limit);
447
+ return JSON.stringify(events);
448
+ }
449
+ _attachDialogHandler(page) {
450
+ if (!page || this._dialogHandlersAttached.has(page)) {
451
+ return;
452
+ }
453
+ const pageWithEvents = page;
454
+ if (typeof pageWithEvents.on !== 'function') {
455
+ return;
456
+ }
457
+ const handler = async (dialog) => {
458
+ try {
459
+ const dialogType = typeof dialog?.type === 'function' ? dialog.type() : 'alert';
460
+ const message = typeof dialog?.message === 'function' ? dialog.message() : '';
461
+ try {
462
+ await this.event_bus.dispatch(new DialogOpenedEvent({
463
+ dialog_type: dialogType,
464
+ message: String(message ?? ''),
465
+ url: this.currentUrl ?? 'about:blank',
466
+ frame_id: null,
467
+ }));
468
+ }
469
+ catch (error) {
470
+ this.logger.debug(`Failed to dispatch DialogOpenedEvent: ${error.message}`);
471
+ }
472
+ this._captureClosedPopupMessage(dialogType, message);
473
+ this._recordRecentEvent('javascript_dialog_closed', {
474
+ url: this.currentUrl,
475
+ error_message: message
476
+ ? `[${dialogType}] ${String(message).trim()}`
477
+ : `[${dialogType}]`,
478
+ });
479
+ const shouldAccept = dialogType === 'alert' ||
480
+ dialogType === 'confirm' ||
481
+ dialogType === 'beforeunload';
482
+ if (shouldAccept && typeof dialog?.accept === 'function') {
483
+ await dialog.accept();
484
+ }
485
+ else if (typeof dialog?.dismiss === 'function') {
486
+ await dialog.dismiss();
487
+ }
488
+ }
489
+ catch (error) {
490
+ this.logger.debug(`Failed to auto-handle JavaScript dialog: ${error.message}`);
491
+ }
492
+ };
493
+ pageWithEvents.on('dialog', handler);
494
+ this._dialogHandlersAttached.add(page);
495
+ }
496
+ async _getPendingNetworkRequests(page) {
497
+ if (!page || typeof page.evaluate !== 'function') {
498
+ return [];
499
+ }
500
+ try {
501
+ const pending = await page.evaluate(() => {
502
+ const perf = window.performance;
503
+ if (!perf?.getEntriesByType) {
504
+ return [];
505
+ }
506
+ const entries = perf.getEntriesByType('resource');
507
+ const now = perf.now?.() ?? Date.now();
508
+ const blockedPatterns = [
509
+ 'doubleclick',
510
+ 'analytics',
511
+ 'tracking',
512
+ 'metrics',
513
+ 'telemetry',
514
+ 'facebook.net',
515
+ 'hotjar',
516
+ 'clarity',
517
+ 'mixpanel',
518
+ 'segment',
519
+ '/beacon/',
520
+ '/collector/',
521
+ '/telemetry/',
522
+ ];
523
+ const pendingRequests = [];
524
+ for (const entry of entries) {
525
+ const responseEnd = typeof entry.responseEnd === 'number'
526
+ ? entry.responseEnd
527
+ : 0;
528
+ if (responseEnd !== 0) {
529
+ continue;
530
+ }
531
+ const url = String(entry.name ?? '');
532
+ if (!url || url.startsWith('data:') || url.length > 500) {
533
+ continue;
534
+ }
535
+ const lower = url.toLowerCase();
536
+ if (blockedPatterns.some((pattern) => lower.includes(pattern))) {
537
+ continue;
538
+ }
539
+ const startTime = typeof entry.startTime === 'number'
540
+ ? entry.startTime
541
+ : now;
542
+ const loadingDuration = Math.max(0, now - startTime);
543
+ if (loadingDuration > 10000) {
544
+ continue;
545
+ }
546
+ const resourceType = String(entry.initiatorType ?? '').toLowerCase();
547
+ if ((resourceType === 'img' ||
548
+ resourceType === 'image' ||
549
+ resourceType === 'font') &&
550
+ loadingDuration > 3000) {
551
+ continue;
552
+ }
553
+ pendingRequests.push({
554
+ url,
555
+ method: 'GET',
556
+ loading_duration_ms: Math.round(loadingDuration),
557
+ resource_type: resourceType || null,
558
+ });
559
+ if (pendingRequests.length >= 20) {
560
+ break;
561
+ }
562
+ }
563
+ return pendingRequests;
564
+ });
565
+ return Array.isArray(pending)
566
+ ? pending.map((entry) => ({
567
+ url: String(entry.url ?? ''),
568
+ method: typeof entry.method === 'string'
569
+ ? entry.method
570
+ : 'GET',
571
+ loading_duration_ms: typeof entry.loading_duration_ms === 'number'
572
+ ? entry.loading_duration_ms
573
+ : 0,
574
+ resource_type: typeof entry.resource_type === 'string'
575
+ ? entry.resource_type
576
+ : null,
577
+ }))
578
+ : [];
579
+ }
580
+ catch (error) {
581
+ this.logger.debug(`Failed to gather pending network requests: ${error.message}`);
582
+ return [];
583
+ }
584
+ }
268
585
  get tabs() {
269
586
  return this._tabs.slice();
270
587
  }
@@ -280,6 +597,9 @@ export class BrowserSession {
280
597
  get _owns_browser_resources() {
281
598
  return this.ownsBrowserResources;
282
599
  }
600
+ get is_stopping() {
601
+ return this._stoppingPromise !== null;
602
+ }
283
603
  claim_agent(agentId, mode = 'exclusive') {
284
604
  if (!agentId) {
285
605
  return false;
@@ -363,7 +683,11 @@ export class BrowserSession {
363
683
  return this.get_attached_agent_ids();
364
684
  }
365
685
  _determineOwnership() {
366
- if (this.cdp_url || this.wss_url || this.browser || this.browser_context) {
686
+ if (this.cdp_url ||
687
+ this.wss_url ||
688
+ this.browser ||
689
+ this.browser_context ||
690
+ this.browser_pid) {
367
691
  return false;
368
692
  }
369
693
  return true;
@@ -521,9 +845,13 @@ export class BrowserSession {
521
845
  return this._logger;
522
846
  }
523
847
  async start() {
848
+ this.attach_default_watchdogs();
524
849
  if (this.initialized) {
525
850
  return this;
526
851
  }
852
+ await this.event_bus.dispatch(new BrowserStartEvent({
853
+ cdp_url: this.cdp_url,
854
+ }));
527
855
  const ensurePage = async () => {
528
856
  const current = this.agent_current_page;
529
857
  if (current && !current.isClosed?.()) {
@@ -608,7 +936,11 @@ export class BrowserSession {
608
936
  }
609
937
  }
610
938
  this.initialized = true;
939
+ this._recordRecentEvent('browser_started', { url: this.currentUrl });
611
940
  this.logger.debug(`Started ${this.describe()} with profile ${this.browser_profile.toString()}`);
941
+ await this.event_bus.dispatch(new BrowserConnectedEvent({
942
+ cdp_url: this.cdp_url ?? this.wss_url ?? 'playwright',
943
+ }));
612
944
  return this;
613
945
  }
614
946
  /**
@@ -663,7 +995,7 @@ export class BrowserSession {
663
995
  this.logger.info(`Successfully connected to browser PID ${browserPid}`);
664
996
  }
665
997
  catch (error) {
666
- throw new Error(`Failed to connect to browser PID ${browserPid}: ${error.message}`);
998
+ throw new Error(`Failed to connect to browser PID ${browserPid}: ${error.message}`, { cause: error });
667
999
  }
668
1000
  }
669
1001
  /**
@@ -743,13 +1075,18 @@ export class BrowserSession {
743
1075
  this.playwright = null;
744
1076
  this.cachedBrowserState = null;
745
1077
  this._tabs = [];
1078
+ this.session_manager.clear();
746
1079
  this.downloaded_files = [];
1080
+ this._closedPopupMessages = [];
1081
+ this._dialogHandlersAttached = new WeakSet();
1082
+ this._recentEvents = [];
747
1083
  }
748
1084
  async close() {
749
1085
  await this.stop();
750
1086
  }
751
1087
  async get_browser_state_with_recovery(options = {}) {
752
1088
  const signal = options.signal ?? null;
1089
+ const includeRecentEvents = options.include_recent_events ?? false;
753
1090
  this._throwIfAborted(signal);
754
1091
  if (!this.initialized) {
755
1092
  await this._withAbort(this.start(), signal);
@@ -796,8 +1133,6 @@ export class BrowserSession {
796
1133
  let pageInfo = null;
797
1134
  let pixelsAbove = 0;
798
1135
  let pixelsBelow = 0;
799
- let pixelsLeft = 0;
800
- let pixelsRight = 0;
801
1136
  if (page) {
802
1137
  try {
803
1138
  const metrics = await this._withAbort(page.evaluate(() => {
@@ -818,8 +1153,8 @@ export class BrowserSession {
818
1153
  const viewportHeight = metrics.viewportHeight ?? 0;
819
1154
  const viewportWidth = metrics.viewportWidth ?? 0;
820
1155
  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);
1156
+ const pixelsLeft = Math.max(metrics.scrollX ?? 0, 0);
1157
+ const pixelsRight = Math.max((metrics.pageWidth ?? 0) - (metrics.scrollX + viewportWidth), 0);
823
1158
  pageInfo = {
824
1159
  viewport_width: viewportWidth,
825
1160
  viewport_height: viewportHeight,
@@ -840,6 +1175,16 @@ export class BrowserSession {
840
1175
  this.logger.debug(`Failed to compute page metrics: ${error.message}`);
841
1176
  }
842
1177
  }
1178
+ const pendingNetworkRequests = await this._getPendingNetworkRequests(page);
1179
+ if (pageInfo &&
1180
+ Number.isFinite(pageInfo.viewport_width) &&
1181
+ Number.isFinite(pageInfo.viewport_height)) {
1182
+ this._original_viewport_size = [
1183
+ Math.floor(pageInfo.viewport_width),
1184
+ Math.floor(pageInfo.viewport_height),
1185
+ ];
1186
+ }
1187
+ const paginationButtons = DomService.detect_pagination_buttons(domState.selector_map);
843
1188
  const summary = new BrowserStateSummary(domState, {
844
1189
  url: this.currentUrl,
845
1190
  title: this.currentTitle || this.currentUrl,
@@ -853,6 +1198,12 @@ export class BrowserSession {
853
1198
  : [],
854
1199
  is_pdf_viewer: Boolean(this.currentUrl?.toLowerCase().endsWith('.pdf')),
855
1200
  loading_status: this.currentPageLoadingStatus,
1201
+ recent_events: includeRecentEvents
1202
+ ? this._getRecentEventsSummary()
1203
+ : null,
1204
+ pending_network_requests: pendingNetworkRequests,
1205
+ pagination_buttons: paginationButtons,
1206
+ closed_popup_messages: this._getClosedPopupMessagesSnapshot(),
856
1207
  });
857
1208
  // Implement clickable element hash caching to detect new elements
858
1209
  if (options.cache_clickable_elements_hashes && page) {
@@ -901,29 +1252,48 @@ export class BrowserSession {
901
1252
  }
902
1253
  _buildTabs() {
903
1254
  if (!this._tabs.length) {
904
- this._tabs.push({
905
- page_id: this._tabCounter++,
1255
+ const pageId = this._tabCounter++;
1256
+ this._tabs.push(this._createTabInfo({
1257
+ page_id: pageId,
906
1258
  url: this.currentUrl,
907
1259
  title: this.currentTitle || this.currentUrl,
908
- parent_page_id: null,
909
- });
1260
+ }));
910
1261
  }
911
1262
  else {
912
1263
  const tab = this._tabs[this.currentTabIndex];
1264
+ if (tab && !tab.tab_id) {
1265
+ tab.tab_id = this._formatTabId(tab.page_id);
1266
+ }
913
1267
  tab.url = this.currentUrl;
914
1268
  tab.title = this.currentTitle || this.currentUrl;
915
1269
  }
1270
+ this._syncSessionManagerFromTabs();
916
1271
  return this._tabs.slice();
917
1272
  }
918
1273
  async navigate_to(url, options = {}) {
919
1274
  const signal = options.signal ?? null;
920
1275
  this._throwIfAborted(signal);
1276
+ this._assert_url_allowed(url);
921
1277
  const normalized = normalize_url(url);
1278
+ const waitUntil = options.wait_until ?? 'domcontentloaded';
1279
+ const timeoutMs = typeof options.timeout_ms === 'number' &&
1280
+ Number.isFinite(options.timeout_ms)
1281
+ ? Math.max(0, options.timeout_ms)
1282
+ : null;
1283
+ this._recordRecentEvent('navigation_started', { url: normalized });
922
1284
  const page = await this._withAbort(this.get_current_page(), signal);
923
1285
  if (page?.goto) {
924
1286
  try {
925
1287
  this.currentPageLoadingStatus = null;
926
- await this._withAbort(page.goto(normalized, { waitUntil: 'domcontentloaded' }), signal);
1288
+ const gotoOptions = {
1289
+ waitUntil,
1290
+ };
1291
+ if (timeoutMs !== null) {
1292
+ gotoOptions.timeout = timeoutMs;
1293
+ }
1294
+ await this._withAbort(page.goto(normalized, gotoOptions), signal);
1295
+ const finalUrl = page.url();
1296
+ this._assert_url_allowed(finalUrl);
927
1297
  await this._waitForStableNetwork(page, signal);
928
1298
  }
929
1299
  catch (error) {
@@ -931,6 +1301,10 @@ export class BrowserSession {
931
1301
  throw error;
932
1302
  }
933
1303
  const message = error.message ?? 'Navigation failed';
1304
+ this._recordRecentEvent('navigation_failed', {
1305
+ url: normalized,
1306
+ error_message: message,
1307
+ });
934
1308
  throw new BrowserError(message);
935
1309
  }
936
1310
  }
@@ -942,32 +1316,52 @@ export class BrowserSession {
942
1316
  this._tabs[this.currentTabIndex].url = normalized;
943
1317
  this._tabs[this.currentTabIndex].title = normalized;
944
1318
  }
1319
+ this._syncSessionManagerFromTabs();
945
1320
  this._setActivePage(page ?? null);
1321
+ this._recordRecentEvent('navigation_completed', { url: normalized });
946
1322
  this.cachedBrowserState = null;
947
1323
  return this.agent_current_page;
948
1324
  }
949
1325
  async create_new_tab(url, options = {}) {
950
1326
  const signal = options.signal ?? null;
951
1327
  this._throwIfAborted(signal);
1328
+ this._assert_url_allowed(url);
952
1329
  const normalized = normalize_url(url);
953
- const newTab = {
1330
+ const waitUntil = options.wait_until ?? 'domcontentloaded';
1331
+ const timeoutMs = typeof options.timeout_ms === 'number' &&
1332
+ Number.isFinite(options.timeout_ms)
1333
+ ? Math.max(0, options.timeout_ms)
1334
+ : null;
1335
+ const newTab = this._createTabInfo({
954
1336
  page_id: this._tabCounter++,
955
1337
  url: normalized,
956
1338
  title: normalized,
957
- parent_page_id: null,
958
- };
1339
+ });
959
1340
  this._tabs.push(newTab);
960
1341
  this.currentTabIndex = this._tabs.length - 1;
961
1342
  this.currentUrl = normalized;
962
1343
  this.currentTitle = normalized;
963
1344
  this.historyStack.push(normalized);
1345
+ this._recordRecentEvent('tab_created', {
1346
+ url: normalized,
1347
+ page_id: newTab.page_id,
1348
+ tab_id: newTab.tab_id,
1349
+ });
964
1350
  let page = null;
965
1351
  try {
966
1352
  page =
967
1353
  (await this._withAbort(this.browser_context?.newPage?.() ?? Promise.resolve(null), signal)) ?? null;
968
1354
  if (page) {
969
1355
  this.currentPageLoadingStatus = null;
970
- await this._withAbort(page.goto(normalized, { waitUntil: 'domcontentloaded' }), signal);
1356
+ const gotoOptions = {
1357
+ waitUntil,
1358
+ };
1359
+ if (timeoutMs !== null) {
1360
+ gotoOptions.timeout = timeoutMs;
1361
+ }
1362
+ await this._withAbort(page.goto(normalized, gotoOptions), signal);
1363
+ const finalUrl = page.url();
1364
+ this._assert_url_allowed(finalUrl);
971
1365
  await this._waitForStableNetwork(page, signal);
972
1366
  }
973
1367
  }
@@ -975,18 +1369,56 @@ export class BrowserSession {
975
1369
  if (this._isAbortError(error)) {
976
1370
  throw error;
977
1371
  }
1372
+ this._recordRecentEvent('tab_navigation_failed', {
1373
+ url: normalized,
1374
+ page_id: newTab.page_id,
1375
+ tab_id: newTab.tab_id,
1376
+ error_message: error.message ?? 'Failed to open new tab',
1377
+ });
978
1378
  this.logger.debug(`Failed to open new tab via Playwright: ${error.message}`);
979
1379
  }
980
1380
  this.tabPages.set(newTab.page_id, page);
1381
+ this._syncSessionManagerFromTabs();
981
1382
  this._setActivePage(page);
982
1383
  this.currentPageLoadingStatus = null;
983
1384
  if (!this.human_current_page) {
984
1385
  this.human_current_page = page;
985
1386
  }
1387
+ this._recordRecentEvent('tab_ready', {
1388
+ url: normalized,
1389
+ page_id: newTab.page_id,
1390
+ tab_id: newTab.tab_id,
1391
+ });
1392
+ await this.event_bus.dispatch(new TabCreatedEvent({
1393
+ target_id: newTab.target_id ?? newTab.tab_id ?? 'unknown_target',
1394
+ url: normalized,
1395
+ }));
986
1396
  this.cachedBrowserState = null;
987
1397
  return this.agent_current_page;
988
1398
  }
989
1399
  _resolveTabIndex(identifier) {
1400
+ if (typeof identifier === 'string') {
1401
+ const normalized = identifier.trim();
1402
+ if (!normalized) {
1403
+ return -1;
1404
+ }
1405
+ if (normalized === '-1') {
1406
+ return Math.max(0, this._tabs.length - 1);
1407
+ }
1408
+ const byTabId = this._tabs.findIndex((tab) => tab.tab_id === normalized);
1409
+ if (byTabId !== -1) {
1410
+ return byTabId;
1411
+ }
1412
+ const byTargetId = this._tabs.findIndex((tab) => tab.target_id === normalized);
1413
+ if (byTargetId !== -1) {
1414
+ return byTargetId;
1415
+ }
1416
+ const numeric = Number.parseInt(normalized, 10);
1417
+ if (Number.isFinite(numeric)) {
1418
+ return this._resolveTabIndex(numeric);
1419
+ }
1420
+ return -1;
1421
+ }
990
1422
  if (identifier === -1) {
991
1423
  return Math.max(0, this._tabs.length - 1);
992
1424
  }
@@ -1005,11 +1437,15 @@ export class BrowserSession {
1005
1437
  const index = this._resolveTabIndex(identifier);
1006
1438
  const tab = index >= 0 ? (this._tabs[index] ?? null) : null;
1007
1439
  if (!tab) {
1008
- throw new Error(`Tab index ${identifier} does not exist`);
1440
+ throw new Error(`Tab '${identifier}' does not exist`);
1441
+ }
1442
+ if (!tab.tab_id) {
1443
+ tab.tab_id = this._formatTabId(tab.page_id);
1009
1444
  }
1010
1445
  this.currentTabIndex = index;
1011
1446
  this.currentUrl = tab.url;
1012
1447
  this.currentTitle = tab.title;
1448
+ this._syncSessionManagerFromTabs();
1013
1449
  const page = this.tabPages.get(tab.page_id) ?? null;
1014
1450
  this._setActivePage(page);
1015
1451
  if (page?.bringToFront) {
@@ -1024,15 +1460,27 @@ export class BrowserSession {
1024
1460
  }
1025
1461
  }
1026
1462
  await this._waitForLoad(page, 5000, signal);
1463
+ this._recordRecentEvent('tab_switched', {
1464
+ url: tab.url,
1465
+ page_id: tab.page_id,
1466
+ tab_id: tab.tab_id,
1467
+ });
1468
+ await this.event_bus.dispatch(new AgentFocusChangedEvent({
1469
+ target_id: tab.target_id ?? tab.tab_id,
1470
+ url: tab.url,
1471
+ }));
1027
1472
  this.cachedBrowserState = null;
1028
1473
  return page;
1029
1474
  }
1030
1475
  async close_tab(identifier) {
1031
1476
  const index = this._resolveTabIndex(identifier);
1032
1477
  if (index < 0 || index >= this._tabs.length) {
1033
- throw new Error(`Tab index ${identifier} does not exist`);
1478
+ throw new Error(`Tab '${identifier}' does not exist`);
1034
1479
  }
1035
1480
  const closingTab = this._tabs[index];
1481
+ if (!closingTab.tab_id) {
1482
+ closingTab.tab_id = this._formatTabId(closingTab.page_id);
1483
+ }
1036
1484
  const closingPage = this.tabPages.get(closingTab.page_id) ?? null;
1037
1485
  if (closingPage?.close) {
1038
1486
  try {
@@ -1043,10 +1491,16 @@ export class BrowserSession {
1043
1491
  }
1044
1492
  }
1045
1493
  this.tabPages.delete(closingTab.page_id);
1494
+ this._recordRecentEvent('tab_closed', {
1495
+ url: closingTab.url,
1496
+ page_id: closingTab.page_id,
1497
+ tab_id: closingTab.tab_id,
1498
+ });
1046
1499
  this._tabs.splice(index, 1);
1047
1500
  if (this.currentTabIndex >= this._tabs.length) {
1048
1501
  this.currentTabIndex = Math.max(0, this._tabs.length - 1);
1049
1502
  }
1503
+ this._syncSessionManagerFromTabs();
1050
1504
  const tab = this._tabs[this.currentTabIndex] ?? null;
1051
1505
  const current = tab ? (this.tabPages.get(tab.page_id) ?? null) : null;
1052
1506
  this._setActivePage(current);
@@ -1056,12 +1510,379 @@ export class BrowserSession {
1056
1510
  const tab = this._tabs[this.currentTabIndex];
1057
1511
  this.currentUrl = tab.url;
1058
1512
  this.currentTitle = tab.title;
1513
+ await this.event_bus.dispatch(new AgentFocusChangedEvent({
1514
+ target_id: tab.target_id ?? tab.tab_id ?? 'unknown_target',
1515
+ url: tab.url,
1516
+ }));
1059
1517
  }
1060
1518
  else {
1061
1519
  this.currentUrl = 'about:blank';
1062
1520
  this.currentTitle = 'about:blank';
1063
1521
  this._setActivePage(null);
1064
1522
  }
1523
+ await this.event_bus.dispatch(new TabClosedEvent({
1524
+ target_id: closingTab.target_id ?? closingTab.tab_id,
1525
+ }));
1526
+ }
1527
+ async wait(seconds, options = {}) {
1528
+ const signal = options.signal ?? null;
1529
+ this._throwIfAborted(signal);
1530
+ const boundedSeconds = Math.max(Number(seconds) || 0, 0);
1531
+ const delayMs = boundedSeconds * 1000;
1532
+ if (delayMs <= 0) {
1533
+ return;
1534
+ }
1535
+ await this._waitWithAbort(delayMs, signal);
1536
+ }
1537
+ async send_keys(keys, options = {}) {
1538
+ const signal = options.signal ?? null;
1539
+ this._throwIfAborted(signal);
1540
+ const page = await this._withAbort(this.get_current_page(), signal);
1541
+ const keyboard = page?.keyboard;
1542
+ if (!keyboard) {
1543
+ throw new BrowserError('Keyboard input is not available on the current page.');
1544
+ }
1545
+ try {
1546
+ await this._withAbort(keyboard.press(keys), signal);
1547
+ }
1548
+ catch (error) {
1549
+ if (error instanceof Error && error.message.includes('Unknown key')) {
1550
+ for (const char of keys) {
1551
+ await this._withAbort(keyboard.press(char), signal);
1552
+ }
1553
+ return;
1554
+ }
1555
+ throw error;
1556
+ }
1557
+ }
1558
+ async click_coordinates(coordinate_x, coordinate_y, options = {}) {
1559
+ const signal = options.signal ?? null;
1560
+ this._throwIfAborted(signal);
1561
+ const page = await this._withAbort(this.get_current_page(), signal);
1562
+ if (!page?.mouse?.click) {
1563
+ throw new BrowserError('Unable to perform coordinate click on the current page.');
1564
+ }
1565
+ await this._withAbort(page.mouse.click(coordinate_x, coordinate_y, {
1566
+ button: options.button ?? 'left',
1567
+ }), signal);
1568
+ }
1569
+ async scroll(direction, amount, options = {}) {
1570
+ const signal = options.signal ?? null;
1571
+ this._throwIfAborted(signal);
1572
+ const normalizedAmount = Math.max(Math.floor(Math.abs(amount)), 0);
1573
+ if (normalizedAmount === 0) {
1574
+ return;
1575
+ }
1576
+ const page = await this._withAbort(this.get_current_page(), signal);
1577
+ if (!page?.evaluate) {
1578
+ throw new BrowserError('Unable to access current page for scrolling.');
1579
+ }
1580
+ const node = options.node ?? null;
1581
+ if (node?.xpath) {
1582
+ const scrolled = await this._withAbort(page.evaluate((payload) => {
1583
+ const root = document.evaluate(payload.xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
1584
+ if (!root) {
1585
+ return false;
1586
+ }
1587
+ const topDelta = payload.direction === 'up'
1588
+ ? -payload.amount
1589
+ : payload.direction === 'down'
1590
+ ? payload.amount
1591
+ : 0;
1592
+ const leftDelta = payload.direction === 'left'
1593
+ ? -payload.amount
1594
+ : payload.direction === 'right'
1595
+ ? payload.amount
1596
+ : 0;
1597
+ root.scrollBy({
1598
+ top: topDelta,
1599
+ left: leftDelta,
1600
+ behavior: 'auto',
1601
+ });
1602
+ return true;
1603
+ }, { xpath: node.xpath, direction, amount: normalizedAmount }), signal);
1604
+ if (scrolled) {
1605
+ return;
1606
+ }
1607
+ }
1608
+ if (direction === 'up' || direction === 'down') {
1609
+ const pixels = direction === 'down' ? -normalizedAmount : normalizedAmount;
1610
+ await this._withAbort(this._scrollContainer(pixels), signal);
1611
+ return;
1612
+ }
1613
+ const horizontalDelta = direction === 'left' ? -normalizedAmount : normalizedAmount;
1614
+ await this._withAbort(page.evaluate((x) => window.scrollBy(x, 0), horizontalDelta), signal);
1615
+ }
1616
+ async scroll_to_text(text, options = {}) {
1617
+ const signal = options.signal ?? null;
1618
+ this._throwIfAborted(signal);
1619
+ const page = await this._withAbort(this.get_current_page(), signal);
1620
+ if (!page?.evaluate) {
1621
+ throw new BrowserError('Unable to access page for scrolling.');
1622
+ }
1623
+ const success = await this._withAbort(page.evaluate((payload) => {
1624
+ const query = payload.text.toLowerCase();
1625
+ const iterator = document.createNodeIterator(document.body, NodeFilter.SHOW_ELEMENT);
1626
+ let node;
1627
+ while ((node = iterator.nextNode())) {
1628
+ const el = node;
1629
+ if (!el || !el.textContent) {
1630
+ continue;
1631
+ }
1632
+ if (el.textContent.toLowerCase().includes(query)) {
1633
+ el.scrollIntoView({
1634
+ behavior: 'smooth',
1635
+ block: payload.direction === 'up' ? 'start' : 'center',
1636
+ });
1637
+ return true;
1638
+ }
1639
+ }
1640
+ return false;
1641
+ }, { text, direction: options.direction ?? 'down' }), signal);
1642
+ if (!success) {
1643
+ throw new BrowserError(`Text '${text}' not found on page`);
1644
+ }
1645
+ }
1646
+ async get_dropdown_options(element_node, options = {}) {
1647
+ const signal = options.signal ?? null;
1648
+ this._throwIfAborted(signal);
1649
+ const page = await this._withAbort(this.get_current_page(), signal);
1650
+ if (!page?.evaluate) {
1651
+ throw new BrowserError('Unable to evaluate dropdown options on current page.');
1652
+ }
1653
+ if (!element_node?.xpath) {
1654
+ throw new BrowserError('DOM element does not include an XPath selector.');
1655
+ }
1656
+ const payload = await this._withAbort(page.evaluate(({ xpath }) => {
1657
+ const element = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
1658
+ if (!element)
1659
+ return null;
1660
+ if (element.tagName?.toLowerCase() === 'select') {
1661
+ const options = Array.from(element.options).map((opt, index) => ({
1662
+ text: opt.textContent?.trim() ?? '',
1663
+ value: (opt.value ?? '').trim(),
1664
+ index,
1665
+ }));
1666
+ return { type: 'select', options };
1667
+ }
1668
+ const ariaRoles = new Set(['menu', 'listbox', 'combobox']);
1669
+ const role = element.getAttribute('role');
1670
+ if (role && ariaRoles.has(role)) {
1671
+ const nodes = element.querySelectorAll('[role="menuitem"],[role="option"]');
1672
+ const options = Array.from(nodes).map((node, index) => ({
1673
+ text: node.textContent?.trim() ?? '',
1674
+ value: node.textContent?.trim() ?? '',
1675
+ index,
1676
+ }));
1677
+ return { type: 'aria', options };
1678
+ }
1679
+ return null;
1680
+ }, { xpath: element_node.xpath }), signal);
1681
+ if (!payload || !Array.isArray(payload.options)) {
1682
+ throw new BrowserError('No options found for the specified dropdown.');
1683
+ }
1684
+ const normalizedOptions = payload.options
1685
+ .map((option, index) => ({
1686
+ index: typeof option?.index === 'number' && Number.isFinite(option.index)
1687
+ ? option.index
1688
+ : index,
1689
+ text: String(option?.text ?? ''),
1690
+ value: String(option?.value ?? ''),
1691
+ }))
1692
+ .filter((option) => option.text.length > 0 || option.value.length > 0);
1693
+ if (normalizedOptions.length === 0) {
1694
+ throw new BrowserError('No options found for the specified dropdown.');
1695
+ }
1696
+ const formattedOptions = normalizedOptions.map((option) => `${option.index}: text=${JSON.stringify(option.text)}, value=${JSON.stringify(option.value)}`);
1697
+ formattedOptions.push('Prefer exact text first; if needed select_dropdown_option also supports case-insensitive text/value matching.');
1698
+ const message = formattedOptions.join('\n');
1699
+ const indexForMemory = element_node.highlight_index ?? 'unknown';
1700
+ return {
1701
+ type: String(payload.type ?? 'unknown'),
1702
+ options: JSON.stringify(normalizedOptions),
1703
+ formatted_options: formattedOptions.join('\n'),
1704
+ message,
1705
+ short_term_memory: message,
1706
+ long_term_memory: `Found dropdown options for index ${indexForMemory}.`,
1707
+ };
1708
+ }
1709
+ async select_dropdown_option(element_node, text, options = {}) {
1710
+ const signal = options.signal ?? null;
1711
+ this._throwIfAborted(signal);
1712
+ if (!element_node?.xpath) {
1713
+ throw new BrowserError('DOM element does not include an XPath selector.');
1714
+ }
1715
+ const page = await this._withAbort(this.get_current_page(), signal);
1716
+ if (!page) {
1717
+ throw new BrowserError('No active page for selection.');
1718
+ }
1719
+ const formatAvailableOptions = (opts) => opts
1720
+ .map((opt) => ` - [${opt.index}] text=${JSON.stringify(opt.text)} value=${JSON.stringify(opt.value)}`)
1721
+ .join('\n');
1722
+ const pageFrames = (() => {
1723
+ const framesAccessor = page.frames;
1724
+ if (typeof framesAccessor === 'function') {
1725
+ try {
1726
+ const result = framesAccessor.call(page);
1727
+ return Array.isArray(result) ? result : [];
1728
+ }
1729
+ catch {
1730
+ return [];
1731
+ }
1732
+ }
1733
+ return Array.isArray(framesAccessor) ? framesAccessor : [];
1734
+ })();
1735
+ for (const frame of pageFrames) {
1736
+ try {
1737
+ const typeInfo = await this._withAbort(frame.evaluate((xpath) => {
1738
+ const element = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
1739
+ if (!element)
1740
+ return { found: false };
1741
+ const tagName = element.tagName?.toLowerCase();
1742
+ const role = element.getAttribute?.('role');
1743
+ if (tagName === 'select')
1744
+ return { found: true, type: 'select' };
1745
+ if (role && ['menu', 'listbox', 'combobox'].includes(role))
1746
+ return { found: true, type: 'aria' };
1747
+ return { found: false };
1748
+ }, element_node.xpath), signal);
1749
+ if (!typeInfo?.found) {
1750
+ continue;
1751
+ }
1752
+ if (typeInfo.type === 'select') {
1753
+ const selection = await this._withAbort(frame.evaluate(({ xpath, optionText, }) => {
1754
+ const root = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
1755
+ if (!root || root.tagName?.toLowerCase() !== 'select') {
1756
+ return { found: false };
1757
+ }
1758
+ const options = Array.from(root.options).map((opt, index) => ({
1759
+ index,
1760
+ text: opt.textContent?.trim() ?? '',
1761
+ value: (opt.value ?? '').trim(),
1762
+ }));
1763
+ const targetRaw = optionText.trim();
1764
+ const targetLower = optionText.trim().toLowerCase();
1765
+ let matchedIndex = options.findIndex((opt) => opt.text === targetRaw || opt.value === targetRaw);
1766
+ if (matchedIndex < 0) {
1767
+ matchedIndex = options.findIndex((opt) => opt.text.trim().toLowerCase() === targetLower ||
1768
+ opt.value.trim().toLowerCase() === targetLower);
1769
+ }
1770
+ if (matchedIndex < 0) {
1771
+ return { found: true, success: false, options };
1772
+ }
1773
+ const matched = options[matchedIndex];
1774
+ root.value = matched.value;
1775
+ root.dispatchEvent(new Event('input', { bubbles: true }));
1776
+ root.dispatchEvent(new Event('change', { bubbles: true }));
1777
+ const selectedOption = root.selectedIndex >= 0
1778
+ ? root.options[root.selectedIndex]
1779
+ : null;
1780
+ const selectedText = selectedOption?.textContent?.trim() ?? '';
1781
+ const selectedValue = (root.value ?? '').trim();
1782
+ const selectedValueLower = selectedValue.trim().toLowerCase();
1783
+ const selectedTextLower = selectedText.trim().toLowerCase();
1784
+ const matchedValueLower = String(matched.value ?? '')
1785
+ .trim()
1786
+ .toLowerCase();
1787
+ const matchedTextLower = String(matched.text ?? '')
1788
+ .trim()
1789
+ .toLowerCase();
1790
+ const verified = selectedValueLower === matchedValueLower ||
1791
+ selectedTextLower === matchedTextLower;
1792
+ return {
1793
+ found: true,
1794
+ success: verified,
1795
+ options,
1796
+ selectedText,
1797
+ selectedValue,
1798
+ matched,
1799
+ };
1800
+ }, { xpath: element_node.xpath, optionText: text }), signal);
1801
+ if (selection?.found && selection.success) {
1802
+ const matchedText = selection.matched?.text ?? text;
1803
+ const matchedValue = selection.matched?.value ?? '';
1804
+ const msg = `Selected option ${matchedText} (${matchedValue})`;
1805
+ return {
1806
+ message: msg,
1807
+ short_term_memory: msg,
1808
+ long_term_memory: msg,
1809
+ matched_text: String(matchedText),
1810
+ matched_value: String(matchedValue),
1811
+ };
1812
+ }
1813
+ if (selection?.found) {
1814
+ const details = formatAvailableOptions(selection.options ?? []);
1815
+ throw new BrowserError(`Could not select option '${text}' for index ${element_node.highlight_index ?? 'unknown'}.\nAvailable options:\n${details}`);
1816
+ }
1817
+ continue;
1818
+ }
1819
+ const clicked = await this._withAbort(frame.evaluate(({ xpath, optionText }) => {
1820
+ const root = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
1821
+ if (!root)
1822
+ return false;
1823
+ const nodes = root.querySelectorAll('[role="menuitem"],[role="option"]');
1824
+ const options = Array.from(nodes).map((node, index) => ({
1825
+ index,
1826
+ text: node.textContent?.trim() ?? '',
1827
+ value: node.textContent?.trim() ?? '',
1828
+ }));
1829
+ const targetRaw = optionText.trim();
1830
+ const targetLower = optionText.trim().toLowerCase();
1831
+ let matchedIndex = options.findIndex((opt) => opt.text === targetRaw || opt.value === targetRaw);
1832
+ if (matchedIndex < 0) {
1833
+ matchedIndex = options.findIndex((opt) => opt.text.trim().toLowerCase() === targetLower ||
1834
+ opt.value.trim().toLowerCase() === targetLower);
1835
+ }
1836
+ if (matchedIndex < 0) {
1837
+ return { found: true, success: false, options };
1838
+ }
1839
+ nodes[matchedIndex].click();
1840
+ return {
1841
+ found: true,
1842
+ success: true,
1843
+ options,
1844
+ matched: options[matchedIndex],
1845
+ };
1846
+ }, { xpath: element_node.xpath, optionText: text }), signal);
1847
+ if (clicked?.found && clicked.success) {
1848
+ const matchedText = clicked.matched?.text ?? text;
1849
+ const msg = `Selected menu item ${matchedText}`;
1850
+ return {
1851
+ message: msg,
1852
+ short_term_memory: msg,
1853
+ long_term_memory: msg,
1854
+ matched_text: String(matchedText),
1855
+ };
1856
+ }
1857
+ if (clicked?.found) {
1858
+ const details = formatAvailableOptions(clicked.options ?? []);
1859
+ throw new BrowserError(`Could not select option '${text}' for index ${element_node.highlight_index ?? 'unknown'}.\nAvailable options:\n${details}`);
1860
+ }
1861
+ }
1862
+ catch (error) {
1863
+ if (error instanceof BrowserError) {
1864
+ throw error;
1865
+ }
1866
+ continue;
1867
+ }
1868
+ }
1869
+ throw new BrowserError(`Could not select option '${text}' for index ${element_node.highlight_index ?? 'unknown'}`);
1870
+ }
1871
+ async upload_file(element_node, file_path, options = {}) {
1872
+ const signal = options.signal ?? null;
1873
+ this._throwIfAborted(signal);
1874
+ const locator = await this.get_locate_element(element_node);
1875
+ if (!locator) {
1876
+ throw new Error('Element not found');
1877
+ }
1878
+ if (!fs.existsSync(file_path)) {
1879
+ throw new Error(`File does not exist: ${file_path}`);
1880
+ }
1881
+ const locatorWithUpload = locator;
1882
+ if (typeof locatorWithUpload.setInputFiles !== 'function') {
1883
+ throw new Error('Element does not support file upload');
1884
+ }
1885
+ await this._withAbort(locatorWithUpload.setInputFiles(file_path, { timeout: 5000 }), signal);
1065
1886
  }
1066
1887
  async go_back(options = {}) {
1067
1888
  const signal = options.signal ?? null;
@@ -1090,6 +1911,7 @@ export class BrowserSession {
1090
1911
  this._tabs[this.currentTabIndex].url = previous;
1091
1912
  this._tabs[this.currentTabIndex].title = previous;
1092
1913
  }
1914
+ this._recordRecentEvent('navigation_back', { url: previous });
1093
1915
  }
1094
1916
  async get_dom_element_by_index(_index, options = {}) {
1095
1917
  const selectorMap = await this.get_selector_map(options);
@@ -1217,13 +2039,19 @@ export class BrowserSession {
1217
2039
  }
1218
2040
  async _input_text_element_node(node, text, options = {}) {
1219
2041
  const signal = options.signal ?? null;
2042
+ const clear = options.clear ?? true;
1220
2043
  this._throwIfAborted(signal);
1221
2044
  const locator = await this.get_locate_element(node);
1222
2045
  if (!locator) {
1223
2046
  throw new Error('Element not found');
1224
2047
  }
1225
2048
  await this._withAbort(locator.click({ timeout: 5000 }), signal);
1226
- await this._withAbort(locator.fill(text, { timeout: 5000 }), signal);
2049
+ if (clear) {
2050
+ await this._withAbort(locator.fill(text, { timeout: 5000 }), signal);
2051
+ }
2052
+ else {
2053
+ await this._withAbort(locator.type(text, { timeout: 5000 }), signal);
2054
+ }
1227
2055
  }
1228
2056
  async _click_element_node(node, options = {}) {
1229
2057
  const signal = options.signal ?? null;
@@ -1242,15 +2070,46 @@ export class BrowserSession {
1242
2070
  await performClick();
1243
2071
  try {
1244
2072
  const download = await this._withAbort(downloadPromise, signal);
2073
+ const downloadGuid = uuid7str();
1245
2074
  const suggested = typeof download.suggestedFilename === 'function'
1246
2075
  ? download.suggestedFilename()
1247
2076
  : 'download';
2077
+ const downloadUrl = typeof download.url === 'function'
2078
+ ? download.url()
2079
+ : (this.currentUrl ?? '');
2080
+ await this.event_bus.dispatch(new DownloadStartedEvent({
2081
+ guid: downloadGuid,
2082
+ url: downloadUrl,
2083
+ suggested_filename: suggested,
2084
+ auto_download: false,
2085
+ }));
1248
2086
  const uniqueFilename = await BrowserSession.get_unique_filename(downloadsDir, suggested);
1249
2087
  const downloadPath = path.join(downloadsDir, uniqueFilename);
1250
2088
  if (typeof download.saveAs === 'function') {
1251
2089
  await download.saveAs(downloadPath);
1252
2090
  }
1253
- this.add_downloaded_file(downloadPath);
2091
+ const stats = fs.existsSync(downloadPath)
2092
+ ? fs.statSync(downloadPath)
2093
+ : null;
2094
+ await this.event_bus.dispatch(new DownloadProgressEvent({
2095
+ guid: downloadGuid,
2096
+ received_bytes: stats?.size ?? 0,
2097
+ total_bytes: stats?.size ?? 0,
2098
+ state: 'completed',
2099
+ }));
2100
+ const fileDownloadedResult = await this.event_bus.dispatch(new FileDownloadedEvent({
2101
+ guid: downloadGuid,
2102
+ url: downloadUrl,
2103
+ path: downloadPath,
2104
+ file_name: uniqueFilename,
2105
+ file_size: stats?.size ?? 0,
2106
+ file_type: path.extname(uniqueFilename).replace('.', '') || null,
2107
+ mime_type: null,
2108
+ auto_download: false,
2109
+ }));
2110
+ if (fileDownloadedResult.handler_results.length === 0) {
2111
+ this.add_downloaded_file(downloadPath);
2112
+ }
1254
2113
  return downloadPath;
1255
2114
  }
1256
2115
  catch (error) {
@@ -1609,7 +2468,7 @@ export class BrowserSession {
1609
2468
  try {
1610
2469
  this.logger.debug(`📸 Taking ${full_page ? 'full-page' : 'viewport'} PNG screenshot via CDP: ${url}`);
1611
2470
  // Create CDP session for the screenshot
1612
- cdp_session = await this.browser_context.newCDPSession(page);
2471
+ cdp_session = await this.get_or_create_cdp_session(page);
1613
2472
  // Capture screenshot via CDP
1614
2473
  const screenshot_response = await cdp_session.send('Page.captureScreenshot', {
1615
2474
  captureBeyondViewport: false,
@@ -1683,7 +2542,7 @@ export class BrowserSession {
1683
2542
  // ==================== P2 Additional Functions ====================
1684
2543
  /**
1685
2544
  * Get information about all open tabs
1686
- * @returns Array of tab information including page_id, url, and title
2545
+ * @returns Array of tab information including page_id, tab_id, url, and title
1687
2546
  */
1688
2547
  async get_tabs_info() {
1689
2548
  if (!this.browser_context) {
@@ -1693,6 +2552,9 @@ export class BrowserSession {
1693
2552
  const pages = this.browser_context.pages();
1694
2553
  for (let page_id = 0; page_id < pages.length; page_id++) {
1695
2554
  const page = pages[page_id];
2555
+ const tab_id = this._tabs.find((tab) => tab.page_id === page_id)?.tab_id ??
2556
+ this._formatTabId(page_id);
2557
+ this._attachDialogHandler(page ?? null);
1696
2558
  // Skip chrome:// pages and new tab pages
1697
2559
  const isNewTab = page.url() === 'about:blank' ||
1698
2560
  page.url().startsWith('chrome://newtab');
@@ -1700,6 +2562,7 @@ export class BrowserSession {
1700
2562
  if (isNewTab) {
1701
2563
  tabs_info.push({
1702
2564
  page_id,
2565
+ tab_id,
1703
2566
  url: page.url(),
1704
2567
  title: 'ignore this tab and do not use it',
1705
2568
  });
@@ -1707,6 +2570,7 @@ export class BrowserSession {
1707
2570
  else {
1708
2571
  tabs_info.push({
1709
2572
  page_id,
2573
+ tab_id,
1710
2574
  url: page.url(),
1711
2575
  title: page.url(),
1712
2576
  });
@@ -1720,13 +2584,14 @@ export class BrowserSession {
1720
2584
  setTimeout(() => reject(new Error('timeout')), 2000);
1721
2585
  });
1722
2586
  const title = await Promise.race([titlePromise, timeoutPromise]);
1723
- tabs_info.push({ page_id, url: page.url(), title });
2587
+ tabs_info.push({ page_id, tab_id, url: page.url(), title });
1724
2588
  }
1725
2589
  catch (error) {
1726
2590
  this.logger.debug(`⚠️ Failed to get tab info for tab #${page_id}: ${page.url()} (using fallback title)`);
1727
2591
  if (isNewTab) {
1728
2592
  tabs_info.push({
1729
2593
  page_id,
2594
+ tab_id,
1730
2595
  url: page.url(),
1731
2596
  title: 'ignore this tab and do not use it',
1732
2597
  });
@@ -1734,6 +2599,7 @@ export class BrowserSession {
1734
2599
  else {
1735
2600
  tabs_info.push({
1736
2601
  page_id,
2602
+ tab_id,
1737
2603
  url: page.url(),
1738
2604
  title: page.url(), // Use URL as fallback title
1739
2605
  });
@@ -1800,9 +2666,9 @@ export class BrowserSession {
1800
2666
  * @param include_screenshot - Include screenshot in state summary
1801
2667
  * @returns BrowserStateSummary with current page state
1802
2668
  */
1803
- async get_state_summary(cache_clickable_elements_hashes = true, include_screenshot = true) {
2669
+ async get_state_summary(cache_clickable_elements_hashes = true, include_screenshot = true, include_recent_events = false) {
1804
2670
  this.logger.debug('🔄 Starting get_state_summary...');
1805
- const updated_state = await this._get_updated_state(-1, include_screenshot);
2671
+ const updated_state = await this._get_updated_state(-1, include_screenshot, include_recent_events);
1806
2672
  // Implement clickable element hash caching to detect new elements
1807
2673
  if (cache_clickable_elements_hashes) {
1808
2674
  const page = await this.get_current_page();
@@ -1828,7 +2694,7 @@ export class BrowserSession {
1828
2694
  * Get minimal state summary without DOM processing, but with screenshot
1829
2695
  * Used when page is in error state or unresponsive
1830
2696
  */
1831
- async get_minimal_state_summary() {
2697
+ async get_minimal_state_summary(include_recent_events = false) {
1832
2698
  try {
1833
2699
  const page = await this.get_current_page();
1834
2700
  const url = page ? page.url() : 'unknown';
@@ -1870,6 +2736,7 @@ export class BrowserSession {
1870
2736
  height: 720,
1871
2737
  };
1872
2738
  const dom_state = new DOMState(minimal_element_tree, {});
2739
+ this._original_viewport_size = [viewport.width, viewport.height];
1873
2740
  return new BrowserStateSummary(dom_state, {
1874
2741
  url,
1875
2742
  title,
@@ -1892,6 +2759,12 @@ export class BrowserSession {
1892
2759
  browser_errors: ['Page in error state - minimal navigation available'],
1893
2760
  is_pdf_viewer: false,
1894
2761
  loading_status: this.currentPageLoadingStatus,
2762
+ recent_events: include_recent_events
2763
+ ? this._getRecentEventsSummary()
2764
+ : null,
2765
+ pending_network_requests: [],
2766
+ pagination_buttons: [],
2767
+ closed_popup_messages: this._getClosedPopupMessagesSnapshot(),
1895
2768
  });
1896
2769
  }
1897
2770
  catch (error) {
@@ -1904,7 +2777,7 @@ export class BrowserSession {
1904
2777
  * @param focus_element - Element index to focus on (default: -1)
1905
2778
  * @param include_screenshot - Whether to include screenshot
1906
2779
  */
1907
- async _get_updated_state(focus_element = -1, include_screenshot = true) {
2780
+ async _get_updated_state(focus_element = -1, include_screenshot = true, include_recent_events = false) {
1908
2781
  const page = await this.get_current_page();
1909
2782
  if (!page) {
1910
2783
  throw new Error('No current page available');
@@ -1922,6 +2795,7 @@ export class BrowserSession {
1922
2795
  height: 720,
1923
2796
  };
1924
2797
  const dom_state = new DOMState(minimal_element_tree, {});
2798
+ this._original_viewport_size = [viewport.width, viewport.height];
1925
2799
  return new BrowserStateSummary(dom_state, {
1926
2800
  url: page_url,
1927
2801
  title: this._is_new_tab_page(page_url) ? 'New Tab' : 'Chrome Page',
@@ -1944,6 +2818,12 @@ export class BrowserSession {
1944
2818
  browser_errors: [],
1945
2819
  is_pdf_viewer: false,
1946
2820
  loading_status: this.currentPageLoadingStatus,
2821
+ recent_events: include_recent_events
2822
+ ? this._getRecentEventsSummary()
2823
+ : null,
2824
+ pending_network_requests: [],
2825
+ pagination_buttons: [],
2826
+ closed_popup_messages: this._getClosedPopupMessagesSnapshot(),
1947
2827
  });
1948
2828
  }
1949
2829
  // Normal path for regular pages
@@ -1998,6 +2878,14 @@ export class BrowserSession {
1998
2878
  }
1999
2879
  // Get page info and scroll info
2000
2880
  const page_info = await this.get_page_info(page);
2881
+ if (page_info &&
2882
+ Number.isFinite(page_info.viewport_width) &&
2883
+ Number.isFinite(page_info.viewport_height)) {
2884
+ this._original_viewport_size = [
2885
+ Math.floor(page_info.viewport_width),
2886
+ Math.floor(page_info.viewport_height),
2887
+ ];
2888
+ }
2001
2889
  let pixels_above = 0;
2002
2890
  let pixels_below = 0;
2003
2891
  try {
@@ -2032,6 +2920,8 @@ export class BrowserSession {
2032
2920
  }
2033
2921
  // Check if PDF viewer
2034
2922
  const is_pdf_viewer = await this._is_pdf_viewer(page);
2923
+ const pendingNetworkRequests = await this._getPendingNetworkRequests(page);
2924
+ const paginationButtons = DomService.detect_pagination_buttons(content.selector_map);
2035
2925
  const browser_state = new BrowserStateSummary(content, {
2036
2926
  url: page_url,
2037
2927
  title,
@@ -2043,6 +2933,12 @@ export class BrowserSession {
2043
2933
  browser_errors,
2044
2934
  is_pdf_viewer,
2045
2935
  loading_status: this.currentPageLoadingStatus,
2936
+ recent_events: include_recent_events
2937
+ ? this._getRecentEventsSummary()
2938
+ : null,
2939
+ pending_network_requests: pendingNetworkRequests,
2940
+ pagination_buttons: paginationButtons,
2941
+ closed_popup_messages: this._getClosedPopupMessagesSnapshot(),
2046
2942
  });
2047
2943
  this.logger.debug('✅ get_state_summary completed successfully');
2048
2944
  return browser_state;
@@ -2053,7 +2949,22 @@ export class BrowserSession {
2053
2949
  _is_new_tab_page(url) {
2054
2950
  return (url === 'about:blank' ||
2055
2951
  url === 'about:newtab' ||
2056
- url === 'chrome://newtab/');
2952
+ url === 'chrome://newtab/' ||
2953
+ url === 'chrome://new-tab-page/' ||
2954
+ url === 'chrome://new-tab-page');
2955
+ }
2956
+ _is_ip_address_host(hostname) {
2957
+ const normalized = hostname.startsWith('[') && hostname.endsWith(']')
2958
+ ? hostname.slice(1, -1)
2959
+ : hostname;
2960
+ return isIP(normalized) !== 0;
2961
+ }
2962
+ _get_domain_variants(hostname) {
2963
+ const host = hostname.toLowerCase();
2964
+ if (host.startsWith('www.')) {
2965
+ return [host, host.slice(4)];
2966
+ }
2967
+ return [host, `www.${host}`];
2057
2968
  }
2058
2969
  /**
2059
2970
  * Check if page is displaying a PDF
@@ -2319,37 +3230,111 @@ export class BrowserSession {
2319
3230
  * Check if a URL is allowed based on allowed_domains configuration
2320
3231
  * @param url - URL to check
2321
3232
  */
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
3233
+ _get_url_access_denial_reason(url) {
3234
+ // Always allow new tab pages and browser-internal pages we intentionally use.
2328
3235
  if (this._is_new_tab_page(url)) {
2329
- return true;
3236
+ return null;
2330
3237
  }
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;
3238
+ let parsed;
3239
+ try {
3240
+ parsed = new URL(url);
3241
+ }
3242
+ catch {
3243
+ return 'invalid_url';
3244
+ }
3245
+ if (parsed.protocol === 'data:' || parsed.protocol === 'blob:') {
3246
+ return null;
3247
+ }
3248
+ if (!parsed.hostname) {
3249
+ return 'missing_host';
3250
+ }
3251
+ const [hostVariant, hostAlt] = this._get_domain_variants(parsed.hostname);
3252
+ if (this.browser_profile.block_ip_addresses &&
3253
+ this._is_ip_address_host(parsed.hostname)) {
3254
+ return 'ip_address_blocked';
3255
+ }
3256
+ const allowedDomains = this.browser_profile.allowed_domains;
3257
+ if (allowedDomains &&
3258
+ ((Array.isArray(allowedDomains) && allowedDomains.length > 0) ||
3259
+ (allowedDomains instanceof Set && allowedDomains.size > 0))) {
3260
+ if (allowedDomains instanceof Set) {
3261
+ if (allowedDomains.has(hostVariant) || allowedDomains.has(hostAlt)) {
3262
+ return null;
2336
3263
  }
2337
3264
  }
2338
- catch (error) {
2339
- this.logger.warning(`Invalid domain pattern: ${allowed_domain}`);
3265
+ else {
3266
+ for (const allowedDomain of allowedDomains) {
3267
+ try {
3268
+ if (match_url_with_domain_pattern(url, allowedDomain, true)) {
3269
+ return null;
3270
+ }
3271
+ }
3272
+ catch {
3273
+ this.logger.warning(`Invalid domain pattern: ${allowedDomain}`);
3274
+ }
3275
+ }
3276
+ }
3277
+ return 'not_in_allowed_domains';
3278
+ }
3279
+ const prohibitedDomains = this.browser_profile.prohibited_domains;
3280
+ if (prohibitedDomains &&
3281
+ ((Array.isArray(prohibitedDomains) && prohibitedDomains.length > 0) ||
3282
+ (prohibitedDomains instanceof Set && prohibitedDomains.size > 0))) {
3283
+ if (prohibitedDomains instanceof Set) {
3284
+ if (prohibitedDomains.has(hostVariant) ||
3285
+ prohibitedDomains.has(hostAlt)) {
3286
+ return 'in_prohibited_domains';
3287
+ }
2340
3288
  }
3289
+ else {
3290
+ for (const prohibitedDomain of prohibitedDomains) {
3291
+ try {
3292
+ if (match_url_with_domain_pattern(url, prohibitedDomain, true)) {
3293
+ return 'in_prohibited_domains';
3294
+ }
3295
+ }
3296
+ catch {
3297
+ this.logger.warning(`Invalid domain pattern: ${prohibitedDomain}`);
3298
+ }
3299
+ }
3300
+ }
3301
+ }
3302
+ return null;
3303
+ }
3304
+ _is_url_allowed(url) {
3305
+ return this._get_url_access_denial_reason(url) === null;
3306
+ }
3307
+ _formatDomainCollection(value) {
3308
+ if (value instanceof Set) {
3309
+ return JSON.stringify(Array.from(value));
3310
+ }
3311
+ return JSON.stringify(value ?? null);
3312
+ }
3313
+ _assert_url_allowed(url) {
3314
+ const denialReason = this._get_url_access_denial_reason(url);
3315
+ if (!denialReason) {
3316
+ return;
3317
+ }
3318
+ this._recordRecentEvent('navigation_blocked', {
3319
+ url,
3320
+ error_message: denialReason,
3321
+ });
3322
+ if (denialReason === 'not_in_allowed_domains') {
3323
+ throw new URLNotAllowedError(`URL ${url} is not in allowed_domains. Current allowed_domains: ${this._formatDomainCollection(this.browser_profile.allowed_domains)}`);
3324
+ }
3325
+ if (denialReason === 'in_prohibited_domains') {
3326
+ throw new URLNotAllowedError(`URL ${url} is blocked by prohibited_domains. Current prohibited_domains: ${this._formatDomainCollection(this.browser_profile.prohibited_domains)}`);
2341
3327
  }
2342
- return false;
3328
+ if (denialReason === 'ip_address_blocked') {
3329
+ throw new URLNotAllowedError(`URL ${url} is blocked because block_ip_addresses=true`);
3330
+ }
3331
+ throw new URLNotAllowedError(`URL ${url} is not allowed (${denialReason})`);
2343
3332
  }
2344
3333
  /**
2345
3334
  * Navigate helper with URL validation
2346
3335
  */
2347
3336
  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
- }
3337
+ this._assert_url_allowed(url);
2353
3338
  await this.navigate_to(url);
2354
3339
  }
2355
3340
  /**
@@ -2389,11 +3374,18 @@ export class BrowserSession {
2389
3374
  if (!hasActiveResources) {
2390
3375
  return;
2391
3376
  }
2392
- this._stoppingPromise = this._shutdown_browser_session();
3377
+ this._stoppingPromise = Promise.resolve().then(async () => {
3378
+ await this.event_bus.dispatch(new BrowserStopEvent());
3379
+ await this._shutdown_browser_session();
3380
+ });
2393
3381
  try {
2394
3382
  await this._stoppingPromise;
3383
+ this._recordRecentEvent('browser_stopped');
3384
+ await this.event_bus.dispatch(new BrowserStoppedEvent());
2395
3385
  }
2396
3386
  finally {
3387
+ this.detach_all_watchdogs();
3388
+ await this.event_bus.stop();
2397
3389
  this._stoppingPromise = null;
2398
3390
  }
2399
3391
  }
@@ -2426,10 +3418,40 @@ export class BrowserSession {
2426
3418
  const suggested_filename = download.suggestedFilename();
2427
3419
  const unique_filename = await BrowserSession.get_unique_filename(downloads_path, suggested_filename);
2428
3420
  const download_path = path.join(downloads_path, unique_filename);
3421
+ const download_guid = uuid7str();
3422
+ const download_url = typeof download.url === 'function'
3423
+ ? download.url()
3424
+ : (this.currentUrl ?? '');
3425
+ await this.event_bus.dispatch(new DownloadStartedEvent({
3426
+ guid: download_guid,
3427
+ url: download_url,
3428
+ suggested_filename,
3429
+ auto_download: false,
3430
+ }));
2429
3431
  await download.saveAs(download_path);
2430
3432
  this.logger.info(`⬇️ Downloaded file to: ${download_path}`);
2431
- // Track the downloaded file
2432
- this.add_downloaded_file(download_path);
3433
+ const stats = fs.existsSync(download_path)
3434
+ ? fs.statSync(download_path)
3435
+ : null;
3436
+ await this.event_bus.dispatch(new DownloadProgressEvent({
3437
+ guid: download_guid,
3438
+ received_bytes: stats?.size ?? 0,
3439
+ total_bytes: stats?.size ?? 0,
3440
+ state: 'completed',
3441
+ }));
3442
+ const fileDownloadedResult = await this.event_bus.dispatch(new FileDownloadedEvent({
3443
+ guid: download_guid,
3444
+ url: download_url,
3445
+ path: download_path,
3446
+ file_name: unique_filename,
3447
+ file_size: stats?.size ?? 0,
3448
+ file_type: path.extname(unique_filename).replace('.', '') || null,
3449
+ mime_type: null,
3450
+ auto_download: false,
3451
+ }));
3452
+ if (fileDownloadedResult.handler_results.length === 0) {
3453
+ this.add_downloaded_file(download_path);
3454
+ }
2433
3455
  return download_path;
2434
3456
  }
2435
3457
  catch (error) {
@@ -2477,6 +3499,19 @@ export class BrowserSession {
2477
3499
  }
2478
3500
  }
2479
3501
  // region - Trace Recording
3502
+ /**
3503
+ * Start tracing on browser context if traces_dir is configured
3504
+ * Note: Currently optional as it may cause performance issues in some cases
3505
+ */
3506
+ async start_trace_recording() {
3507
+ await this._startContextTracing();
3508
+ }
3509
+ /**
3510
+ * Save browser trace recording if active
3511
+ */
3512
+ async save_trace_recording() {
3513
+ await this._saveTraceRecording();
3514
+ }
2480
3515
  /**
2481
3516
  * Start tracing on browser context if traces_dir is configured
2482
3517
  * Note: Currently optional as it may cause performance issues in some cases
@@ -2493,6 +3528,7 @@ export class BrowserSession {
2493
3528
  }
2494
3529
  catch (error) {
2495
3530
  this.logger.warning(`Failed to start tracing: ${error.message}`);
3531
+ throw error;
2496
3532
  }
2497
3533
  }
2498
3534
  }
@@ -2519,6 +3555,7 @@ export class BrowserSession {
2519
3555
  }
2520
3556
  catch (error) {
2521
3557
  this.logger.warning(`Failed to save trace recording: ${error.message}`);
3558
+ throw error;
2522
3559
  }
2523
3560
  }
2524
3561
  }
@@ -2533,7 +3570,7 @@ export class BrowserSession {
2533
3570
  async _scrollWithCdpGesture(page, pixels) {
2534
3571
  try {
2535
3572
  // Use CDP to synthesize scroll gesture - works in all contexts including PDFs
2536
- const cdpSession = await this.browser_context.newCDPSession(page);
3573
+ const cdpSession = await this.get_or_create_cdp_session(page);
2537
3574
  // Get viewport center for scroll origin
2538
3575
  const viewport = await page.evaluate(() => ({
2539
3576
  width: window.innerWidth,
@@ -2705,7 +3742,7 @@ export class BrowserSession {
2705
3742
  * @returns A new BrowserSession instance with copied state
2706
3743
  */
2707
3744
  modelCopy() {
2708
- return new BrowserSession({
3745
+ const copy = new BrowserSession({
2709
3746
  id: this.id,
2710
3747
  browser_profile: this.browser_profile,
2711
3748
  browser: this.browser,
@@ -2718,7 +3755,15 @@ export class BrowserSession {
2718
3755
  browser_pid: this.browser_pid,
2719
3756
  playwright: this.playwright,
2720
3757
  downloaded_files: [...this.downloaded_files],
3758
+ closed_popup_messages: [...this._closedPopupMessages],
2721
3759
  });
3760
+ copy.llm_screenshot_size = this.llm_screenshot_size
3761
+ ? [...this.llm_screenshot_size]
3762
+ : null;
3763
+ copy._original_viewport_size = this._original_viewport_size
3764
+ ? [...this._original_viewport_size]
3765
+ : null;
3766
+ return copy;
2722
3767
  }
2723
3768
  model_copy() {
2724
3769
  return this.modelCopy();
@@ -2767,7 +3812,7 @@ export class BrowserSession {
2767
3812
  try {
2768
3813
  // Create CDP session from the clean page
2769
3814
  const cdpSession = await Promise.race([
2770
- this.browser_context.newCDPSession(tempPage),
3815
+ this.get_or_create_cdp_session(tempPage),
2771
3816
  new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout creating CDP session')), 5000)),
2772
3817
  ]);
2773
3818
  try {
@@ -3233,6 +4278,16 @@ export class BrowserSession {
3233
4278
  }
3234
4279
  // endregion
3235
4280
  // region - Process Management
4281
+ /**
4282
+ * Normalize pid values before issuing process operations.
4283
+ */
4284
+ _normalizePid(pid) {
4285
+ if (!Number.isSafeInteger(pid) || pid <= 0) {
4286
+ this.logger.debug(`Skipping process operation for invalid pid: ${String(pid)}`);
4287
+ return null;
4288
+ }
4289
+ return pid;
4290
+ }
3236
4291
  /**
3237
4292
  * Kill all child processes spawned by this browser session
3238
4293
  */
@@ -3241,17 +4296,17 @@ export class BrowserSession {
3241
4296
  return;
3242
4297
  }
3243
4298
  this.logger.debug(`Killing ${this._childProcesses.size} child processes`);
3244
- for (const pid of this._childProcesses) {
4299
+ for (const trackedPid of this._childProcesses) {
4300
+ const pid = this._normalizePid(trackedPid);
4301
+ if (!pid) {
4302
+ continue;
4303
+ }
3245
4304
  try {
3246
- // Try to kill the process
3247
4305
  process.kill(pid, 'SIGTERM');
3248
4306
  this.logger.debug(`Sent SIGTERM to process ${pid}`);
3249
- // Wait briefly and check if still alive
3250
4307
  await new Promise((resolve) => setTimeout(resolve, 500));
3251
4308
  try {
3252
- // Check if process still exists
3253
4309
  process.kill(pid, 0);
3254
- // If we get here, process is still alive, force kill
3255
4310
  process.kill(pid, 'SIGKILL');
3256
4311
  this.logger.debug(`Sent SIGKILL to process ${pid}`);
3257
4312
  }
@@ -3260,7 +4315,6 @@ export class BrowserSession {
3260
4315
  }
3261
4316
  }
3262
4317
  catch (error) {
3263
- // Process doesn't exist or we don't have permission
3264
4318
  this.logger.debug(`Could not kill process ${pid}: ${error.message}`);
3265
4319
  }
3266
4320
  }
@@ -3270,39 +4324,39 @@ export class BrowserSession {
3270
4324
  * Terminate the browser process and all its children
3271
4325
  */
3272
4326
  async _terminateBrowserProcess() {
3273
- if (!this.browser_pid) {
4327
+ const browserPid = this._normalizePid(this.browser_pid);
4328
+ if (!browserPid) {
3274
4329
  return;
3275
4330
  }
3276
4331
  try {
3277
- this.logger.debug(`Terminating browser process ${this.browser_pid}`);
3278
- // Platform-specific process tree termination
4332
+ this.logger.debug(`Terminating browser process ${browserPid}`);
3279
4333
  if (process.platform === 'win32') {
3280
- // Windows: use taskkill to kill process tree
3281
- await execAsync(`taskkill /PID ${this.browser_pid} /T /F`).catch(() => {
4334
+ await execFileAsync('taskkill', [
4335
+ '/PID',
4336
+ String(browserPid),
4337
+ '/T',
4338
+ '/F',
4339
+ ]).catch(() => {
3282
4340
  // Ignore errors if process already dead
3283
4341
  });
3284
4342
  }
3285
4343
  else {
3286
- // Unix-like: kill process group
3287
4344
  try {
3288
- // Try to kill the process group
3289
- process.kill(-this.browser_pid, 'SIGTERM');
4345
+ process.kill(-browserPid, 'SIGTERM');
3290
4346
  await new Promise((resolve) => setTimeout(resolve, 1000));
3291
- // Check if still alive and force kill if needed
3292
4347
  try {
3293
- process.kill(-this.browser_pid, 0);
3294
- process.kill(-this.browser_pid, 'SIGKILL');
4348
+ process.kill(-browserPid, 0);
4349
+ process.kill(-browserPid, 'SIGKILL');
3295
4350
  }
3296
4351
  catch {
3297
4352
  // Process is dead
3298
4353
  }
3299
4354
  }
3300
4355
  catch {
3301
- // Fallback to killing just the process
3302
4356
  try {
3303
- process.kill(this.browser_pid, 'SIGTERM');
4357
+ process.kill(browserPid, 'SIGTERM');
3304
4358
  await new Promise((resolve) => setTimeout(resolve, 1000));
3305
- process.kill(this.browser_pid, 'SIGKILL');
4359
+ process.kill(browserPid, 'SIGKILL');
3306
4360
  }
3307
4361
  catch {
3308
4362
  // Process doesn't exist
@@ -3319,26 +4373,37 @@ export class BrowserSession {
3319
4373
  * Cross-platform implementation using ps on Unix-like systems and WMIC on Windows
3320
4374
  */
3321
4375
  async _getChildProcesses(pid) {
4376
+ const normalizedPid = this._normalizePid(pid);
4377
+ if (!normalizedPid) {
4378
+ return [];
4379
+ }
3322
4380
  try {
3323
4381
  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}`);
4382
+ const { stdout } = await execFileAsync('wmic', [
4383
+ 'process',
4384
+ 'where',
4385
+ `ParentProcessId=${normalizedPid}`,
4386
+ 'get',
4387
+ 'ProcessId',
4388
+ ]);
3336
4389
  const pids = stdout
3337
4390
  .split('\n')
4391
+ .slice(1)
3338
4392
  .map((line) => parseInt(line.trim(), 10))
3339
- .filter((p) => !isNaN(p));
4393
+ .filter((p) => Number.isFinite(p));
3340
4394
  return pids;
3341
4395
  }
4396
+ const { stdout } = await execFileAsync('ps', [
4397
+ '-o',
4398
+ 'pid=',
4399
+ '--ppid',
4400
+ String(normalizedPid),
4401
+ ]);
4402
+ const pids = stdout
4403
+ .split('\n')
4404
+ .map((line) => parseInt(line.trim(), 10))
4405
+ .filter((p) => Number.isFinite(p));
4406
+ return pids;
3342
4407
  }
3343
4408
  catch {
3344
4409
  return [];
@@ -3348,13 +4413,19 @@ export class BrowserSession {
3348
4413
  * Track a child process
3349
4414
  */
3350
4415
  _trackChildProcess(pid) {
3351
- this._childProcesses.add(pid);
4416
+ const normalizedPid = this._normalizePid(pid);
4417
+ if (normalizedPid) {
4418
+ this._childProcesses.add(normalizedPid);
4419
+ }
3352
4420
  }
3353
4421
  /**
3354
4422
  * Untrack a child process
3355
4423
  */
3356
4424
  _untrackChildProcess(pid) {
3357
- this._childProcesses.delete(pid);
4425
+ const normalizedPid = this._normalizePid(pid);
4426
+ if (normalizedPid) {
4427
+ this._childProcesses.delete(normalizedPid);
4428
+ }
3358
4429
  }
3359
4430
  // region: Loading Animations
3360
4431
  /**