tabwright 1.0.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 (599) hide show
  1. package/LICENSE +21 -0
  2. package/bin.js +4 -0
  3. package/dist/a11y-client.js +204 -0
  4. package/dist/agent-skills/tabwright/SKILL.md +119 -0
  5. package/dist/aria-snapshot.d.ts +262 -0
  6. package/dist/aria-snapshot.d.ts.map +1 -0
  7. package/dist/aria-snapshot.js +1256 -0
  8. package/dist/aria-snapshot.js.map +1 -0
  9. package/dist/aria-snapshot.test.d.ts +2 -0
  10. package/dist/aria-snapshot.test.d.ts.map +1 -0
  11. package/dist/aria-snapshot.test.js +270 -0
  12. package/dist/aria-snapshot.test.js.map +1 -0
  13. package/dist/aria-snapshot.unit.test.d.ts +2 -0
  14. package/dist/aria-snapshot.unit.test.d.ts.map +1 -0
  15. package/dist/aria-snapshot.unit.test.js +542 -0
  16. package/dist/aria-snapshot.unit.test.js.map +1 -0
  17. package/dist/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.d.ts +5 -0
  18. package/dist/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.d.ts.map +1 -0
  19. package/dist/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.js +5 -0
  20. package/dist/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.js.map +1 -0
  21. package/dist/bippy.js +894 -0
  22. package/dist/browser-config.d.ts +13 -0
  23. package/dist/browser-config.d.ts.map +1 -0
  24. package/dist/browser-config.js +126 -0
  25. package/dist/browser-config.js.map +1 -0
  26. package/dist/browser-install.d.ts +16 -0
  27. package/dist/browser-install.d.ts.map +1 -0
  28. package/dist/browser-install.js +238 -0
  29. package/dist/browser-install.js.map +1 -0
  30. package/dist/browser-launch.d.ts +17 -0
  31. package/dist/browser-launch.d.ts.map +1 -0
  32. package/dist/browser-launch.js +44 -0
  33. package/dist/browser-launch.js.map +1 -0
  34. package/dist/builtin-capabilities.d.ts +28 -0
  35. package/dist/builtin-capabilities.d.ts.map +1 -0
  36. package/dist/builtin-capabilities.js +2673 -0
  37. package/dist/builtin-capabilities.js.map +1 -0
  38. package/dist/capability-agent-skill.d.ts +60 -0
  39. package/dist/capability-agent-skill.d.ts.map +1 -0
  40. package/dist/capability-agent-skill.js +225 -0
  41. package/dist/capability-agent-skill.js.map +1 -0
  42. package/dist/capability-auth-state.d.ts +34 -0
  43. package/dist/capability-auth-state.d.ts.map +1 -0
  44. package/dist/capability-auth-state.js +157 -0
  45. package/dist/capability-auth-state.js.map +1 -0
  46. package/dist/capability-auth.d.ts +36 -0
  47. package/dist/capability-auth.d.ts.map +1 -0
  48. package/dist/capability-auth.js +198 -0
  49. package/dist/capability-auth.js.map +1 -0
  50. package/dist/capability-cli-confirmation.test.d.ts +2 -0
  51. package/dist/capability-cli-confirmation.test.d.ts.map +1 -0
  52. package/dist/capability-cli-confirmation.test.js +79 -0
  53. package/dist/capability-cli-confirmation.test.js.map +1 -0
  54. package/dist/capability-options.d.ts +20 -0
  55. package/dist/capability-options.d.ts.map +1 -0
  56. package/dist/capability-options.js +31 -0
  57. package/dist/capability-options.js.map +1 -0
  58. package/dist/capability-options.test.d.ts +2 -0
  59. package/dist/capability-options.test.d.ts.map +1 -0
  60. package/dist/capability-options.test.js +81 -0
  61. package/dist/capability-options.test.js.map +1 -0
  62. package/dist/capability-package.d.ts +27 -0
  63. package/dist/capability-package.d.ts.map +1 -0
  64. package/dist/capability-package.js +444 -0
  65. package/dist/capability-package.js.map +1 -0
  66. package/dist/capability-package.test.d.ts +2 -0
  67. package/dist/capability-package.test.d.ts.map +1 -0
  68. package/dist/capability-package.test.js +179 -0
  69. package/dist/capability-package.test.js.map +1 -0
  70. package/dist/capability-registry.d.ts +262 -0
  71. package/dist/capability-registry.d.ts.map +1 -0
  72. package/dist/capability-registry.js +931 -0
  73. package/dist/capability-registry.js.map +1 -0
  74. package/dist/capability-registry.test.d.ts +2 -0
  75. package/dist/capability-registry.test.d.ts.map +1 -0
  76. package/dist/capability-registry.test.js +1683 -0
  77. package/dist/capability-registry.test.js.map +1 -0
  78. package/dist/capability-runner.d.ts +99 -0
  79. package/dist/capability-runner.d.ts.map +1 -0
  80. package/dist/capability-runner.js +608 -0
  81. package/dist/capability-runner.js.map +1 -0
  82. package/dist/capability-search-relevance.test.d.ts +2 -0
  83. package/dist/capability-search-relevance.test.d.ts.map +1 -0
  84. package/dist/capability-search-relevance.test.js +67 -0
  85. package/dist/capability-search-relevance.test.js.map +1 -0
  86. package/dist/capability-studio.d.ts +11 -0
  87. package/dist/capability-studio.d.ts.map +1 -0
  88. package/dist/capability-studio.js +593 -0
  89. package/dist/capability-studio.js.map +1 -0
  90. package/dist/capability-studio.test.d.ts +2 -0
  91. package/dist/capability-studio.test.d.ts.map +1 -0
  92. package/dist/capability-studio.test.js +41 -0
  93. package/dist/capability-studio.test.js.map +1 -0
  94. package/dist/cdp-log.d.ts +19 -0
  95. package/dist/cdp-log.d.ts.map +1 -0
  96. package/dist/cdp-log.js +83 -0
  97. package/dist/cdp-log.js.map +1 -0
  98. package/dist/cdp-log.test.d.ts +2 -0
  99. package/dist/cdp-log.test.d.ts.map +1 -0
  100. package/dist/cdp-log.test.js +109 -0
  101. package/dist/cdp-log.test.js.map +1 -0
  102. package/dist/cdp-relay.d.ts +19 -0
  103. package/dist/cdp-relay.d.ts.map +1 -0
  104. package/dist/cdp-relay.js +2375 -0
  105. package/dist/cdp-relay.js.map +1 -0
  106. package/dist/cdp-session.d.ts +37 -0
  107. package/dist/cdp-session.d.ts.map +1 -0
  108. package/dist/cdp-session.js +36 -0
  109. package/dist/cdp-session.js.map +1 -0
  110. package/dist/cdp-types.d.ts +65 -0
  111. package/dist/cdp-types.d.ts.map +1 -0
  112. package/dist/cdp-types.js +91 -0
  113. package/dist/cdp-types.js.map +1 -0
  114. package/dist/channel-owner-inspect.test.d.ts +2 -0
  115. package/dist/channel-owner-inspect.test.d.ts.map +1 -0
  116. package/dist/channel-owner-inspect.test.js +75 -0
  117. package/dist/channel-owner-inspect.test.js.map +1 -0
  118. package/dist/chrome-discovery.d.ts +65 -0
  119. package/dist/chrome-discovery.d.ts.map +1 -0
  120. package/dist/chrome-discovery.js +173 -0
  121. package/dist/chrome-discovery.js.map +1 -0
  122. package/dist/chrome-discovery.test.d.ts +2 -0
  123. package/dist/chrome-discovery.test.d.ts.map +1 -0
  124. package/dist/chrome-discovery.test.js +41 -0
  125. package/dist/chrome-discovery.test.js.map +1 -0
  126. package/dist/clean-html.d.ts +11 -0
  127. package/dist/clean-html.d.ts.map +1 -0
  128. package/dist/clean-html.js +106 -0
  129. package/dist/clean-html.js.map +1 -0
  130. package/dist/cli-help.test.d.ts +2 -0
  131. package/dist/cli-help.test.d.ts.map +1 -0
  132. package/dist/cli-help.test.js +98 -0
  133. package/dist/cli-help.test.js.map +1 -0
  134. package/dist/cli.d.ts +3 -0
  135. package/dist/cli.d.ts.map +1 -0
  136. package/dist/cli.js +2709 -0
  137. package/dist/cli.js.map +1 -0
  138. package/dist/cloud-client.d.ts +56 -0
  139. package/dist/cloud-client.d.ts.map +1 -0
  140. package/dist/cloud-client.js +127 -0
  141. package/dist/cloud-client.js.map +1 -0
  142. package/dist/create-logger.d.ts +9 -0
  143. package/dist/create-logger.d.ts.map +1 -0
  144. package/dist/create-logger.js +27 -0
  145. package/dist/create-logger.js.map +1 -0
  146. package/dist/debugger-api.md +458 -0
  147. package/dist/debugger-examples-types.d.ts +24 -0
  148. package/dist/debugger-examples-types.d.ts.map +1 -0
  149. package/dist/debugger-examples-types.js +2 -0
  150. package/dist/debugger-examples-types.js.map +1 -0
  151. package/dist/debugger-examples.d.ts +6 -0
  152. package/dist/debugger-examples.d.ts.map +1 -0
  153. package/dist/debugger-examples.js +53 -0
  154. package/dist/debugger-examples.js.map +1 -0
  155. package/dist/debugger.d.ts +381 -0
  156. package/dist/debugger.d.ts.map +1 -0
  157. package/dist/debugger.js +630 -0
  158. package/dist/debugger.js.map +1 -0
  159. package/dist/diff-utils.d.ts +20 -0
  160. package/dist/diff-utils.d.ts.map +1 -0
  161. package/dist/diff-utils.js +50 -0
  162. package/dist/diff-utils.js.map +1 -0
  163. package/dist/diff-utils.test.d.ts +2 -0
  164. package/dist/diff-utils.test.d.ts.map +1 -0
  165. package/dist/diff-utils.test.js +137 -0
  166. package/dist/diff-utils.test.js.map +1 -0
  167. package/dist/doctor.d.ts +44 -0
  168. package/dist/doctor.d.ts.map +1 -0
  169. package/dist/doctor.js +246 -0
  170. package/dist/doctor.js.map +1 -0
  171. package/dist/doctor.test.d.ts +2 -0
  172. package/dist/doctor.test.d.ts.map +1 -0
  173. package/dist/doctor.test.js +208 -0
  174. package/dist/doctor.test.js.map +1 -0
  175. package/dist/editor-api.md +374 -0
  176. package/dist/editor-examples.d.ts +11 -0
  177. package/dist/editor-examples.d.ts.map +1 -0
  178. package/dist/editor-examples.js +124 -0
  179. package/dist/editor-examples.js.map +1 -0
  180. package/dist/editor.d.ts +203 -0
  181. package/dist/editor.d.ts.map +1 -0
  182. package/dist/editor.js +363 -0
  183. package/dist/editor.js.map +1 -0
  184. package/dist/env-compat.d.ts +2 -0
  185. package/dist/env-compat.d.ts.map +1 -0
  186. package/dist/env-compat.js +27 -0
  187. package/dist/env-compat.js.map +1 -0
  188. package/dist/executor.d.ts +229 -0
  189. package/dist/executor.d.ts.map +1 -0
  190. package/dist/executor.js +1465 -0
  191. package/dist/executor.js.map +1 -0
  192. package/dist/executor.unit.test.d.ts +2 -0
  193. package/dist/executor.unit.test.d.ts.map +1 -0
  194. package/dist/executor.unit.test.js +143 -0
  195. package/dist/executor.unit.test.js.map +1 -0
  196. package/dist/extension/_locales/en/messages.json +278 -0
  197. package/dist/extension/_locales/zh_CN/messages.json +256 -0
  198. package/dist/extension/assets/options-CYH96Lcx.css +1349 -0
  199. package/dist/extension/background.js +3994 -0
  200. package/dist/extension/icons/SolarCursorSquareBold.png +0 -0
  201. package/dist/extension/icons/icon-black-128.png +0 -0
  202. package/dist/extension/icons/icon-black-16.png +0 -0
  203. package/dist/extension/icons/icon-black-32.png +0 -0
  204. package/dist/extension/icons/icon-black-48.png +0 -0
  205. package/dist/extension/icons/icon-gray-128.png +0 -0
  206. package/dist/extension/icons/icon-gray-16.png +0 -0
  207. package/dist/extension/icons/icon-gray-32.png +0 -0
  208. package/dist/extension/icons/icon-gray-48.png +0 -0
  209. package/dist/extension/icons/icon-green-128.png +0 -0
  210. package/dist/extension/icons/icon-green-16.png +0 -0
  211. package/dist/extension/icons/icon-green-32.png +0 -0
  212. package/dist/extension/icons/icon-green-48.png +0 -0
  213. package/dist/extension/icons/tabwright-icon-black.png +0 -0
  214. package/dist/extension/icons/tabwright-icon-gray-disabled.png +0 -0
  215. package/dist/extension/icons/tabwright-icon-gray-disabled.svg +1 -0
  216. package/dist/extension/manifest.json +54 -0
  217. package/dist/extension/options.js +16145 -0
  218. package/dist/extension/rrweb-recorder.js +12952 -0
  219. package/dist/extension/src/options.html +165 -0
  220. package/dist/extension/src/prism-bash.min.js +1 -0
  221. package/dist/extension/src/prism.min.js +1 -0
  222. package/dist/extension/src/welcome.html +396 -0
  223. package/dist/extension-connection.test.d.ts +2 -0
  224. package/dist/extension-connection.test.d.ts.map +1 -0
  225. package/dist/extension-connection.test.js +708 -0
  226. package/dist/extension-connection.test.js.map +1 -0
  227. package/dist/extension-icon-warning.test.d.ts +2 -0
  228. package/dist/extension-icon-warning.test.d.ts.map +1 -0
  229. package/dist/extension-icon-warning.test.js +33 -0
  230. package/dist/extension-icon-warning.test.js.map +1 -0
  231. package/dist/ghost-browser.d.ts +62 -0
  232. package/dist/ghost-browser.d.ts.map +1 -0
  233. package/dist/ghost-browser.js +107 -0
  234. package/dist/ghost-browser.js.map +1 -0
  235. package/dist/ghost-cursor-client.js +374 -0
  236. package/dist/ghost-cursor-controller.d.ts +46 -0
  237. package/dist/ghost-cursor-controller.d.ts.map +1 -0
  238. package/dist/ghost-cursor-controller.js +98 -0
  239. package/dist/ghost-cursor-controller.js.map +1 -0
  240. package/dist/ghost-cursor.d.ts +27 -0
  241. package/dist/ghost-cursor.d.ts.map +1 -0
  242. package/dist/ghost-cursor.js +79 -0
  243. package/dist/ghost-cursor.js.map +1 -0
  244. package/dist/htmlrewrite.d.ts +8 -0
  245. package/dist/htmlrewrite.d.ts.map +1 -0
  246. package/dist/htmlrewrite.js +339 -0
  247. package/dist/htmlrewrite.js.map +1 -0
  248. package/dist/htmlrewrite.test.d.ts +2 -0
  249. package/dist/htmlrewrite.test.d.ts.map +1 -0
  250. package/dist/htmlrewrite.test.js +7975 -0
  251. package/dist/htmlrewrite.test.js.map +1 -0
  252. package/dist/index.d.ts +20 -0
  253. package/dist/index.d.ts.map +1 -0
  254. package/dist/index.js +11 -0
  255. package/dist/index.js.map +1 -0
  256. package/dist/kill-port.d.ts +29 -0
  257. package/dist/kill-port.d.ts.map +1 -0
  258. package/dist/kill-port.js +190 -0
  259. package/dist/kill-port.js.map +1 -0
  260. package/dist/kitty-graphics.d.ts +22 -0
  261. package/dist/kitty-graphics.d.ts.map +1 -0
  262. package/dist/kitty-graphics.js +71 -0
  263. package/dist/kitty-graphics.js.map +1 -0
  264. package/dist/kitty-graphics.test.d.ts +2 -0
  265. package/dist/kitty-graphics.test.d.ts.map +1 -0
  266. package/dist/kitty-graphics.test.js +125 -0
  267. package/dist/kitty-graphics.test.js.map +1 -0
  268. package/dist/locator-selector.test.d.ts +2 -0
  269. package/dist/locator-selector.test.d.ts.map +1 -0
  270. package/dist/locator-selector.test.js +98 -0
  271. package/dist/locator-selector.test.js.map +1 -0
  272. package/dist/mcp-client.d.ts +20 -0
  273. package/dist/mcp-client.d.ts.map +1 -0
  274. package/dist/mcp-client.js +56 -0
  275. package/dist/mcp-client.js.map +1 -0
  276. package/dist/mcp.d.ts +6 -0
  277. package/dist/mcp.d.ts.map +1 -0
  278. package/dist/mcp.js +511 -0
  279. package/dist/mcp.js.map +1 -0
  280. package/dist/on-mouse-action.test.d.ts +2 -0
  281. package/dist/on-mouse-action.test.d.ts.map +1 -0
  282. package/dist/on-mouse-action.test.js +180 -0
  283. package/dist/on-mouse-action.test.js.map +1 -0
  284. package/dist/options-page.test.d.ts +2 -0
  285. package/dist/options-page.test.d.ts.map +1 -0
  286. package/dist/options-page.test.js +773 -0
  287. package/dist/options-page.test.js.map +1 -0
  288. package/dist/package-paths.d.ts +4 -0
  289. package/dist/package-paths.d.ts.map +1 -0
  290. package/dist/package-paths.js +30 -0
  291. package/dist/package-paths.js.map +1 -0
  292. package/dist/page-markdown.d.ts +40 -0
  293. package/dist/page-markdown.d.ts.map +1 -0
  294. package/dist/page-markdown.js +181 -0
  295. package/dist/page-markdown.js.map +1 -0
  296. package/dist/performance-examples.d.ts +5 -0
  297. package/dist/performance-examples.d.ts.map +1 -0
  298. package/dist/performance-examples.js +112 -0
  299. package/dist/performance-examples.js.map +1 -0
  300. package/dist/performance-profiling.md +417 -0
  301. package/dist/playwright-import.d.ts +19 -0
  302. package/dist/playwright-import.d.ts.map +1 -0
  303. package/dist/playwright-import.js +40 -0
  304. package/dist/playwright-import.js.map +1 -0
  305. package/dist/popup-relocation.test.d.ts +7 -0
  306. package/dist/popup-relocation.test.d.ts.map +1 -0
  307. package/dist/popup-relocation.test.js +139 -0
  308. package/dist/popup-relocation.test.js.map +1 -0
  309. package/dist/product-paths.d.ts +9 -0
  310. package/dist/product-paths.d.ts.map +1 -0
  311. package/dist/product-paths.js +24 -0
  312. package/dist/product-paths.js.map +1 -0
  313. package/dist/product-paths.test.d.ts +2 -0
  314. package/dist/product-paths.test.d.ts.map +1 -0
  315. package/dist/product-paths.test.js +39 -0
  316. package/dist/product-paths.test.js.map +1 -0
  317. package/dist/prompt.md +1004 -0
  318. package/dist/protocol.d.ts +248 -0
  319. package/dist/protocol.d.ts.map +1 -0
  320. package/dist/protocol.js +59 -0
  321. package/dist/protocol.js.map +1 -0
  322. package/dist/protocol.test.d.ts +2 -0
  323. package/dist/protocol.test.d.ts.map +1 -0
  324. package/dist/protocol.test.js +31 -0
  325. package/dist/protocol.test.js.map +1 -0
  326. package/dist/react-source.d.ts +57 -0
  327. package/dist/react-source.d.ts.map +1 -0
  328. package/dist/react-source.js +254 -0
  329. package/dist/react-source.js.map +1 -0
  330. package/dist/readability.js +1674 -0
  331. package/dist/relay-client.d.ts +104 -0
  332. package/dist/relay-client.d.ts.map +1 -0
  333. package/dist/relay-client.js +426 -0
  334. package/dist/relay-client.js.map +1 -0
  335. package/dist/relay-client.test.d.ts +2 -0
  336. package/dist/relay-client.test.d.ts.map +1 -0
  337. package/dist/relay-client.test.js +305 -0
  338. package/dist/relay-client.test.js.map +1 -0
  339. package/dist/relay-core.test.d.ts +2 -0
  340. package/dist/relay-core.test.d.ts.map +1 -0
  341. package/dist/relay-core.test.js +1427 -0
  342. package/dist/relay-core.test.js.map +1 -0
  343. package/dist/relay-navigation.test.d.ts +2 -0
  344. package/dist/relay-navigation.test.d.ts.map +1 -0
  345. package/dist/relay-navigation.test.js +661 -0
  346. package/dist/relay-navigation.test.js.map +1 -0
  347. package/dist/relay-session.test.d.ts +2 -0
  348. package/dist/relay-session.test.d.ts.map +1 -0
  349. package/dist/relay-session.test.js +1121 -0
  350. package/dist/relay-session.test.js.map +1 -0
  351. package/dist/relay-state.d.ts +178 -0
  352. package/dist/relay-state.d.ts.map +1 -0
  353. package/dist/relay-state.js +346 -0
  354. package/dist/relay-state.js.map +1 -0
  355. package/dist/relay-state.test.d.ts +2 -0
  356. package/dist/relay-state.test.d.ts.map +1 -0
  357. package/dist/relay-state.test.js +594 -0
  358. package/dist/relay-state.test.js.map +1 -0
  359. package/dist/replay-ai-index.d.ts +99 -0
  360. package/dist/replay-ai-index.d.ts.map +1 -0
  361. package/dist/replay-ai-index.js +677 -0
  362. package/dist/replay-ai-index.js.map +1 -0
  363. package/dist/replay-ai-index.test.d.ts +2 -0
  364. package/dist/replay-ai-index.test.d.ts.map +1 -0
  365. package/dist/replay-ai-index.test.js +255 -0
  366. package/dist/replay-ai-index.test.js.map +1 -0
  367. package/dist/replay-cli.test.d.ts +2 -0
  368. package/dist/replay-cli.test.d.ts.map +1 -0
  369. package/dist/replay-cli.test.js +275 -0
  370. package/dist/replay-cli.test.js.map +1 -0
  371. package/dist/replay-eval.d.ts +44 -0
  372. package/dist/replay-eval.d.ts.map +1 -0
  373. package/dist/replay-eval.js +902 -0
  374. package/dist/replay-eval.js.map +1 -0
  375. package/dist/replay-handoff.d.ts +29 -0
  376. package/dist/replay-handoff.d.ts.map +1 -0
  377. package/dist/replay-handoff.js +83 -0
  378. package/dist/replay-handoff.js.map +1 -0
  379. package/dist/replay-handoff.test.d.ts +2 -0
  380. package/dist/replay-handoff.test.d.ts.map +1 -0
  381. package/dist/replay-handoff.test.js +123 -0
  382. package/dist/replay-handoff.test.js.map +1 -0
  383. package/dist/replay-workflow-compiler.d.ts +44 -0
  384. package/dist/replay-workflow-compiler.d.ts.map +1 -0
  385. package/dist/replay-workflow-compiler.js +601 -0
  386. package/dist/replay-workflow-compiler.js.map +1 -0
  387. package/dist/replay-workflow-compiler.test.d.ts +2 -0
  388. package/dist/replay-workflow-compiler.test.d.ts.map +1 -0
  389. package/dist/replay-workflow-compiler.test.js +223 -0
  390. package/dist/replay-workflow-compiler.test.js.map +1 -0
  391. package/dist/rrweb-recording-relay.d.ts +56 -0
  392. package/dist/rrweb-recording-relay.d.ts.map +1 -0
  393. package/dist/rrweb-recording-relay.js +299 -0
  394. package/dist/rrweb-recording-relay.js.map +1 -0
  395. package/dist/rrweb-recording-relay.test.d.ts +2 -0
  396. package/dist/rrweb-recording-relay.test.d.ts.map +1 -0
  397. package/dist/rrweb-recording-relay.test.js +85 -0
  398. package/dist/rrweb-recording-relay.test.js.map +1 -0
  399. package/dist/rrweb-recording.d.ts +104 -0
  400. package/dist/rrweb-recording.d.ts.map +1 -0
  401. package/dist/rrweb-recording.js +180 -0
  402. package/dist/rrweb-recording.js.map +1 -0
  403. package/dist/scoped-fs.d.ts +95 -0
  404. package/dist/scoped-fs.d.ts.map +1 -0
  405. package/dist/scoped-fs.js +358 -0
  406. package/dist/scoped-fs.js.map +1 -0
  407. package/dist/scoped-fs.test.d.ts +5 -0
  408. package/dist/scoped-fs.test.d.ts.map +1 -0
  409. package/dist/scoped-fs.test.js +73 -0
  410. package/dist/scoped-fs.test.js.map +1 -0
  411. package/dist/selector-generator.js +7378 -0
  412. package/dist/skill-stub.test.d.ts +2 -0
  413. package/dist/skill-stub.test.d.ts.map +1 -0
  414. package/dist/skill-stub.test.js +22 -0
  415. package/dist/skill-stub.test.js.map +1 -0
  416. package/dist/snapshot-tools.test.d.ts +2 -0
  417. package/dist/snapshot-tools.test.d.ts.map +1 -0
  418. package/dist/snapshot-tools.test.js +856 -0
  419. package/dist/snapshot-tools.test.js.map +1 -0
  420. package/dist/start-relay-server.d.ts +6 -0
  421. package/dist/start-relay-server.d.ts.map +1 -0
  422. package/dist/start-relay-server.js +57 -0
  423. package/dist/start-relay-server.js.map +1 -0
  424. package/dist/styles-api.md +124 -0
  425. package/dist/styles-examples.d.ts +8 -0
  426. package/dist/styles-examples.d.ts.map +1 -0
  427. package/dist/styles-examples.js +64 -0
  428. package/dist/styles-examples.js.map +1 -0
  429. package/dist/styles.d.ts +27 -0
  430. package/dist/styles.d.ts.map +1 -0
  431. package/dist/styles.js +231 -0
  432. package/dist/styles.js.map +1 -0
  433. package/dist/tabwright-agent-skill.d.ts +30 -0
  434. package/dist/tabwright-agent-skill.d.ts.map +1 -0
  435. package/dist/tabwright-agent-skill.js +74 -0
  436. package/dist/tabwright-agent-skill.js.map +1 -0
  437. package/dist/tabwright-agent-skill.test.d.ts +2 -0
  438. package/dist/tabwright-agent-skill.test.d.ts.map +1 -0
  439. package/dist/tabwright-agent-skill.test.js +51 -0
  440. package/dist/tabwright-agent-skill.test.js.map +1 -0
  441. package/dist/test-declarations.d.ts +13 -0
  442. package/dist/test-declarations.d.ts.map +1 -0
  443. package/dist/test-declarations.js +2 -0
  444. package/dist/test-declarations.js.map +1 -0
  445. package/dist/test-utils.d.ts +69 -0
  446. package/dist/test-utils.d.ts.map +1 -0
  447. package/dist/test-utils.js +448 -0
  448. package/dist/test-utils.js.map +1 -0
  449. package/dist/test-utils.test.d.ts +2 -0
  450. package/dist/test-utils.test.d.ts.map +1 -0
  451. package/dist/test-utils.test.js +55 -0
  452. package/dist/test-utils.test.js.map +1 -0
  453. package/dist/utils.d.ts +26 -0
  454. package/dist/utils.d.ts.map +1 -0
  455. package/dist/utils.js +60 -0
  456. package/dist/utils.js.map +1 -0
  457. package/dist/wait-for-page-load.d.ts +16 -0
  458. package/dist/wait-for-page-load.d.ts.map +1 -0
  459. package/dist/wait-for-page-load.js +146 -0
  460. package/dist/wait-for-page-load.js.map +1 -0
  461. package/dist/workflow-capability.d.ts +72 -0
  462. package/dist/workflow-capability.d.ts.map +1 -0
  463. package/dist/workflow-capability.js +316 -0
  464. package/dist/workflow-capability.js.map +1 -0
  465. package/dist/workflow-script-flow.test.d.ts +2 -0
  466. package/dist/workflow-script-flow.test.d.ts.map +1 -0
  467. package/dist/workflow-script-flow.test.js +285 -0
  468. package/dist/workflow-script-flow.test.js.map +1 -0
  469. package/package.json +80 -0
  470. package/src/__snapshots__/x.com.processed.html +1003 -0
  471. package/src/__snapshots__/x.com.processed.withStyles.html +2720 -0
  472. package/src/a11y-client.ts +241 -0
  473. package/src/agent-skills/conan-config/SKILL.md +141 -0
  474. package/src/agent-skills/conan-config/agents/openai.yaml +4 -0
  475. package/src/aria-snapshot.test.ts +311 -0
  476. package/src/aria-snapshot.ts +1681 -0
  477. package/src/aria-snapshot.unit.test.ts +615 -0
  478. package/src/aria-snapshots/github-interactive.txt +193 -0
  479. package/src/aria-snapshots/github-raw.txt +309 -0
  480. package/src/aria-snapshots/hackernews-interactive.txt +477 -0
  481. package/src/aria-snapshots/hackernews-raw.txt +428 -0
  482. package/src/aria-snapshots/prosemirror-interactive.txt +66 -0
  483. package/src/aria-snapshots/prosemirror-raw.txt +145 -0
  484. package/src/assets/aria-labels-hacker-news.png +0 -0
  485. package/src/assets/aria-labels-old-reddit.png +0 -0
  486. package/src/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.ts +5 -0
  487. package/src/assets/cursors/screen-studio/pointer-macos-tahoe.svg +18 -0
  488. package/src/assets/x.com.html +32946 -0
  489. package/src/browser-config.ts +179 -0
  490. package/src/browser-install.ts +284 -0
  491. package/src/browser-launch.ts +75 -0
  492. package/src/builtin-capabilities.ts +2765 -0
  493. package/src/capability-agent-skill.ts +305 -0
  494. package/src/capability-auth-state.ts +197 -0
  495. package/src/capability-auth.ts +249 -0
  496. package/src/capability-cli-confirmation.test.ts +90 -0
  497. package/src/capability-options.test.ts +89 -0
  498. package/src/capability-options.ts +57 -0
  499. package/src/capability-package.test.ts +204 -0
  500. package/src/capability-package.ts +555 -0
  501. package/src/capability-registry.test.ts +1905 -0
  502. package/src/capability-registry.ts +1234 -0
  503. package/src/capability-runner.ts +801 -0
  504. package/src/capability-search-relevance.test.ts +73 -0
  505. package/src/capability-studio.test.ts +45 -0
  506. package/src/capability-studio.ts +641 -0
  507. package/src/cdp-log.test.ts +131 -0
  508. package/src/cdp-log.ts +112 -0
  509. package/src/cdp-relay.ts +2944 -0
  510. package/src/cdp-session.ts +77 -0
  511. package/src/cdp-types.ts +158 -0
  512. package/src/channel-owner-inspect.test.ts +94 -0
  513. package/src/chrome-discovery.test.ts +50 -0
  514. package/src/chrome-discovery.ts +220 -0
  515. package/src/clean-html.ts +141 -0
  516. package/src/cli-help.test.ts +123 -0
  517. package/src/cli.ts +3181 -0
  518. package/src/cloud-client.ts +179 -0
  519. package/src/create-logger.ts +38 -0
  520. package/src/debugger-examples-types.ts +16 -0
  521. package/src/debugger-examples.ts +66 -0
  522. package/src/debugger.ts +708 -0
  523. package/src/diff-utils.test.ts +152 -0
  524. package/src/diff-utils.ts +70 -0
  525. package/src/doctor.test.ts +244 -0
  526. package/src/doctor.ts +321 -0
  527. package/src/editor-examples.ts +158 -0
  528. package/src/editor.ts +426 -0
  529. package/src/env-compat.ts +28 -0
  530. package/src/executor.ts +1824 -0
  531. package/src/executor.unit.test.ts +168 -0
  532. package/src/extension-connection.test.ts +855 -0
  533. package/src/extension-icon-warning.test.ts +45 -0
  534. package/src/ghost-browser.ts +149 -0
  535. package/src/ghost-cursor-client.ts +494 -0
  536. package/src/ghost-cursor-controller.ts +129 -0
  537. package/src/ghost-cursor.ts +123 -0
  538. package/src/htmlrewrite.test.ts +8008 -0
  539. package/src/htmlrewrite.ts +390 -0
  540. package/src/index.ts +53 -0
  541. package/src/kill-port.ts +219 -0
  542. package/src/kitty-graphics.test.ts +139 -0
  543. package/src/kitty-graphics.ts +79 -0
  544. package/src/locator-selector.test.ts +119 -0
  545. package/src/mcp-client.ts +78 -0
  546. package/src/mcp.ts +629 -0
  547. package/src/on-mouse-action.test.ts +226 -0
  548. package/src/options-page.test.ts +839 -0
  549. package/src/package-paths.ts +38 -0
  550. package/src/page-markdown.ts +240 -0
  551. package/src/performance-examples.ts +186 -0
  552. package/src/playwright-import.ts +59 -0
  553. package/src/popup-relocation.test.ts +163 -0
  554. package/src/product-paths.test.ts +47 -0
  555. package/src/product-paths.ts +28 -0
  556. package/src/protocol.test.ts +52 -0
  557. package/src/protocol.ts +351 -0
  558. package/src/react-source.ts +379 -0
  559. package/src/relay-client.test.ts +375 -0
  560. package/src/relay-client.ts +572 -0
  561. package/src/relay-core.test.ts +1603 -0
  562. package/src/relay-navigation.test.ts +790 -0
  563. package/src/relay-session.test.ts +1375 -0
  564. package/src/relay-state.test.ts +729 -0
  565. package/src/relay-state.ts +561 -0
  566. package/src/replay-ai-index.test.ts +278 -0
  567. package/src/replay-ai-index.ts +872 -0
  568. package/src/replay-cli.test.ts +325 -0
  569. package/src/replay-eval.ts +1039 -0
  570. package/src/replay-handoff.test.ts +163 -0
  571. package/src/replay-handoff.ts +109 -0
  572. package/src/replay-workflow-compiler.test.ts +250 -0
  573. package/src/replay-workflow-compiler.ts +700 -0
  574. package/src/resource.md +409 -0
  575. package/src/rrweb-recording-relay.test.ts +99 -0
  576. package/src/rrweb-recording-relay.ts +400 -0
  577. package/src/rrweb-recording.ts +322 -0
  578. package/src/scoped-fs.test.ts +80 -0
  579. package/src/scoped-fs.ts +420 -0
  580. package/src/skill-stub.test.ts +24 -0
  581. package/src/skill.md +1413 -0
  582. package/src/snapshot-tools.test.ts +1017 -0
  583. package/src/snapshots/hacker-news-accessibility-full.md +81 -0
  584. package/src/snapshots/hacker-news-accessibility-interactive.md +43 -0
  585. package/src/snapshots/page-markdown-output.txt +16 -0
  586. package/src/snapshots/shadcn-ui-accessibility-full.md +201 -0
  587. package/src/snapshots/shadcn-ui-accessibility-interactive.md +109 -0
  588. package/src/start-relay-server.ts +69 -0
  589. package/src/styles-examples.ts +84 -0
  590. package/src/styles.ts +343 -0
  591. package/src/tabwright-agent-skill.test.ts +61 -0
  592. package/src/tabwright-agent-skill.ts +105 -0
  593. package/src/test-declarations.ts +13 -0
  594. package/src/test-utils.test.ts +70 -0
  595. package/src/test-utils.ts +566 -0
  596. package/src/utils.ts +78 -0
  597. package/src/wait-for-page-load.ts +198 -0
  598. package/src/workflow-capability.ts +417 -0
  599. package/src/workflow-script-flow.test.ts +388 -0
@@ -0,0 +1,1465 @@
1
+ /**
2
+ * PlaywrightExecutor - Manages browser connection and code execution per session.
3
+ * Used by both MCP and CLI to execute Playwright code with persistent state.
4
+ */
5
+ import { getChromium } from './playwright-import.js';
6
+ import crypto from 'node:crypto';
7
+ import fs from 'node:fs';
8
+ import path from 'node:path';
9
+ import os from 'node:os';
10
+ import util from 'node:util';
11
+ import { createRequire } from 'node:module';
12
+ import { fileURLToPath } from 'node:url';
13
+ import vm from 'node:vm';
14
+ import * as acorn from 'acorn';
15
+ import { createSmartDiff } from './diff-utils.js';
16
+ import { getCdpUrl, parseRelayHost, shouldAutoEnableTabwright } from './utils.js';
17
+ import { getExtensionOutdatedWarning } from './relay-client.js';
18
+ import { waitForPageLoad } from './wait-for-page-load.js';
19
+ import { getCDPSessionForPage } from './cdp-session.js';
20
+ import { Debugger } from './debugger.js';
21
+ import { Editor } from './editor.js';
22
+ import { getStylesForLocator, formatStylesAsText } from './styles.js';
23
+ import { getReactSource, getReactComponentInfo } from './react-source.js';
24
+ import { ScopedFS } from './scoped-fs.js';
25
+ import { screenshotWithAccessibilityLabels, getAriaSnapshot, resizeImageForAgent, } from './aria-snapshot.js';
26
+ import { createGhostBrowserChrome } from './ghost-browser.js';
27
+ import { getCleanHTML } from './clean-html.js';
28
+ import { getPageMarkdown } from './page-markdown.js';
29
+ import { createReplayApi } from './rrweb-recording.js';
30
+ import { saveWorkflowCapability, saveWorkflowFromRecording, } from './workflow-capability.js';
31
+ import { GhostCursorController } from './ghost-cursor-controller.js';
32
+ const __filename = fileURLToPath(import.meta.url);
33
+ const __dirname = path.dirname(__filename);
34
+ const require = createRequire(import.meta.url);
35
+ export class CodeExecutionTimeoutError extends Error {
36
+ constructor(timeout) {
37
+ super(`Code execution timed out after ${timeout}ms`);
38
+ this.name = 'CodeExecutionTimeoutError';
39
+ }
40
+ }
41
+ const usefulGlobals = {
42
+ setTimeout,
43
+ setInterval,
44
+ clearTimeout,
45
+ clearInterval,
46
+ URL,
47
+ URLSearchParams,
48
+ fetch,
49
+ Buffer,
50
+ TextEncoder,
51
+ TextDecoder,
52
+ crypto,
53
+ AbortController,
54
+ AbortSignal,
55
+ structuredClone,
56
+ process,
57
+ };
58
+ /**
59
+ * Parse code and check if it's a single expression that should be auto-returned.
60
+ * Returns the exact expression source (without trailing semicolon) using AST
61
+ * node offsets, or null if the code should not be auto-wrapped. See #58.
62
+ */
63
+ export function getAutoReturnExpression(code) {
64
+ try {
65
+ const ast = acorn.parse(code, {
66
+ ecmaVersion: 'latest',
67
+ allowAwaitOutsideFunction: true,
68
+ allowReturnOutsideFunction: true,
69
+ sourceType: 'script',
70
+ });
71
+ // Must be exactly one statement
72
+ if (ast.body.length !== 1) {
73
+ return null;
74
+ }
75
+ const stmt = ast.body[0];
76
+ // If it's already a return statement, don't auto-wrap
77
+ if (stmt.type === 'ReturnStatement') {
78
+ return null;
79
+ }
80
+ // Must be an ExpressionStatement
81
+ if (stmt.type !== 'ExpressionStatement') {
82
+ return null;
83
+ }
84
+ // Don't auto-return side-effect expressions
85
+ const expr = stmt.expression;
86
+ if (expr.type === 'AssignmentExpression' ||
87
+ expr.type === 'UpdateExpression' ||
88
+ (expr.type === 'UnaryExpression' && expr.operator === 'delete')) {
89
+ return null;
90
+ }
91
+ // Don't auto-return sequence expressions that contain assignments
92
+ if (expr.type === 'SequenceExpression') {
93
+ const hasAssignment = expr.expressions.some((e) => e.type === 'AssignmentExpression');
94
+ if (hasAssignment) {
95
+ return null;
96
+ }
97
+ }
98
+ // Use the expression node's start/end offsets to extract just the expression
99
+ // source, excluding any trailing semicolon. This is more robust than regex.
100
+ return code.slice(expr.start, expr.end);
101
+ }
102
+ catch {
103
+ // Parse failed, don't auto-return
104
+ return null;
105
+ }
106
+ }
107
+ /** Backward-compatible helper: returns true if code should be auto-wrapped. */
108
+ export function shouldAutoReturn(code) {
109
+ return getAutoReturnExpression(code) !== null;
110
+ }
111
+ /**
112
+ * Wraps user code in an async IIFE for vm execution.
113
+ * Uses AST node offsets to extract the expression without trailing semicolons,
114
+ * avoiding SyntaxError when embedding inside `return await (...)`. See #58.
115
+ */
116
+ export function wrapCode(code) {
117
+ const expr = getAutoReturnExpression(code);
118
+ if (expr !== null) {
119
+ return `(async () => { return await (${expr}) })()`;
120
+ }
121
+ return `(async () => { ${code} })()`;
122
+ }
123
+ const EXTENSION_NOT_CONNECTED_ERROR = `The Tabwright Chrome extension is not connected. Make sure you have:
124
+ 1. Installed the extension: https://chromewebstore.google.com/detail/tabwright/jfeammnjpkecdekppnclgkkffahnhfhe
125
+ 2. Clicked the extension icon on a tab to enable it (or refreshed the page if just installed)
126
+ 3. Or use a cloud browser instead: run \`tabwright cloud login\` in your terminal to rent a browser in the cloud, with auto CAPTCHA solving, residential proxies and anti-detection built in`;
127
+ const NO_PAGES_AVAILABLE_ERROR = 'No Playwright pages are available. Enable Tabwright on a tab or unset TABWRIGHT_AUTO_ENABLE=false to auto-create one.';
128
+ const CLOUD_SESSION_EXPIRED_ERROR = 'Cloud browser session expired or was destroyed. Create a new session with: tabwright session new --browser cloud';
129
+ /** Patterns that indicate the browser/page/context was closed or the WebSocket died.
130
+ * Used to detect cloud VM expiration vs other Playwright errors. */
131
+ const DISCONNECTION_PATTERNS = [
132
+ 'browser has been closed',
133
+ 'browser.close',
134
+ 'Target page, context or browser has been closed',
135
+ 'Target closed',
136
+ 'connection refused',
137
+ 'WebSocket is not open',
138
+ 'WebSocket error',
139
+ 'connect ECONNREFUSED',
140
+ 'Session closed',
141
+ 'Connection closed',
142
+ 'NS_ERROR_NET_RESET',
143
+ ];
144
+ function isDisconnectionError(error) {
145
+ const msg = error.message || '';
146
+ const stack = error.stack || '';
147
+ const matchesHere = DISCONNECTION_PATTERNS.some((pattern) => {
148
+ return msg.includes(pattern) || stack.includes(pattern);
149
+ });
150
+ if (matchesHere)
151
+ return true;
152
+ // Walk the cause chain — ensureConnection wraps the real WebSocket error
153
+ // in a new Error with { cause }, so we need to check nested causes too.
154
+ if (error.cause instanceof Error) {
155
+ return isDisconnectionError(error.cause);
156
+ }
157
+ return false;
158
+ }
159
+ const MAX_LOGS_PER_PAGE = 5000;
160
+ const ALLOWED_MODULES = new Set([
161
+ 'path',
162
+ 'node:path',
163
+ 'url',
164
+ 'node:url',
165
+ 'querystring',
166
+ 'node:querystring',
167
+ 'punycode',
168
+ 'node:punycode',
169
+ 'crypto',
170
+ 'node:crypto',
171
+ 'buffer',
172
+ 'node:buffer',
173
+ 'string_decoder',
174
+ 'node:string_decoder',
175
+ 'util',
176
+ 'node:util',
177
+ 'assert',
178
+ 'node:assert',
179
+ 'events',
180
+ 'node:events',
181
+ 'timers',
182
+ 'node:timers',
183
+ 'stream',
184
+ 'node:stream',
185
+ 'zlib',
186
+ 'node:zlib',
187
+ 'http',
188
+ 'node:http',
189
+ 'https',
190
+ 'node:https',
191
+ 'http2',
192
+ 'node:http2',
193
+ 'os',
194
+ 'node:os',
195
+ 'fs',
196
+ 'node:fs',
197
+ ]);
198
+ function isRegExp(value) {
199
+ return (typeof value === 'object' && value !== null && typeof value.test === 'function' && typeof value.exec === 'function');
200
+ }
201
+ function isPromise(value) {
202
+ return typeof value === 'object' && value !== null && typeof value.then === 'function';
203
+ }
204
+ /**
205
+ * Duck-type check for a Playwright ChannelOwner (Response, Page, Browser,
206
+ * Request, Frame, BrowserContext, etc.). Used to skip auto-printing these
207
+ * objects from the REPL — they're meant for programmatic use, and dumping
208
+ * them risks leaking internal fields. Users can still `console.log(obj)` to
209
+ * inspect them via the safe handler in playwright-core. See issue #82.
210
+ */
211
+ export function isPlaywrightChannelOwner(value) {
212
+ return (value !== null &&
213
+ typeof value === 'object' &&
214
+ typeof value._type === 'string' &&
215
+ typeof value._guid === 'string' &&
216
+ value._connection !== undefined);
217
+ }
218
+ export class PlaywrightExecutor {
219
+ isConnected = false;
220
+ page = null;
221
+ browser = null;
222
+ context = null;
223
+ userState = {};
224
+ browserLogs = new Map();
225
+ // Tracks the index up to which getLatestLogs({ sinceLastCall: true }) has
226
+ // returned logs. 0 means "return everything" (first call gets full buffer).
227
+ // When addBrowserLog shifts old entries (cap at MAX_LOGS_PER_PAGE), cursors
228
+ // are decremented so they stay in sync with the array.
229
+ pageLogCursor = new Map();
230
+ lastSnapshots = new WeakMap();
231
+ lastRefToLocator = new WeakMap();
232
+ warningEvents = [];
233
+ nextWarningEventId = 0;
234
+ lastDeliveredWarningEventId = 0;
235
+ activeWarningScopes = new Set();
236
+ pagesWithListeners = new WeakSet();
237
+ suppressPageCloseWarnings = false;
238
+ scopedFs;
239
+ sandboxedRequire;
240
+ cdpConfig;
241
+ logger;
242
+ sessionMetadata;
243
+ sessionCwd;
244
+ hasWarnedExtensionOutdated = false;
245
+ ghostCursorController;
246
+ /** Non-null when this executor is backed by a cloud Browser Use VM */
247
+ cloudSession;
248
+ /** Last minute bucket for which a cloud timeout warning was enqueued (dedup) */
249
+ lastCloudTimeoutWarningMinute = null;
250
+ constructor(options) {
251
+ this.cdpConfig = options.cdpConfig;
252
+ this.logger = options.logger || { log: console.log, error: console.error };
253
+ this.sessionMetadata = options.sessionMetadata || { extensionId: null, browser: null, profile: null };
254
+ this.sessionCwd = options.cwd ? path.resolve(options.cwd) : null;
255
+ this.cloudSession = options.cloudSession || null;
256
+ // ScopedFS expects an array of allowed directories. If cwd is provided, use it; otherwise use defaults.
257
+ this.scopedFs = new ScopedFS(this.sessionCwd ? [this.sessionCwd, '/tmp', os.tmpdir()] : undefined, this.sessionCwd || undefined);
258
+ this.sandboxedRequire = this.createSandboxedRequire(require);
259
+ this.ghostCursorController = new GhostCursorController({
260
+ logger: {
261
+ error: (...args) => {
262
+ this.logger.error(...args);
263
+ },
264
+ },
265
+ });
266
+ }
267
+ createSandboxedRequire(originalRequire) {
268
+ const scopedFs = this.scopedFs;
269
+ const sandboxedRequire = ((id) => {
270
+ if (!ALLOWED_MODULES.has(id)) {
271
+ const error = new Error(`Module "${id}" is not allowed in the sandbox. ` +
272
+ `Only safe Node.js built-ins are permitted: ${[...ALLOWED_MODULES].filter((m) => !m.startsWith('node:')).join(', ')}`);
273
+ error.name = 'ModuleNotAllowedError';
274
+ throw error;
275
+ }
276
+ if (id === 'fs' || id === 'node:fs') {
277
+ return scopedFs;
278
+ }
279
+ return originalRequire(id);
280
+ });
281
+ sandboxedRequire.resolve = originalRequire.resolve;
282
+ sandboxedRequire.cache = originalRequire.cache;
283
+ sandboxedRequire.extensions = originalRequire.extensions;
284
+ sandboxedRequire.main = originalRequire.main;
285
+ return sandboxedRequire;
286
+ }
287
+ async setDeviceScaleFactorForMacOS(context) {
288
+ if (os.platform() !== 'darwin') {
289
+ return;
290
+ }
291
+ const options = context._options;
292
+ if (!options || options.deviceScaleFactor === 2) {
293
+ return;
294
+ }
295
+ options.deviceScaleFactor = 2;
296
+ }
297
+ /** Block images, video, and font resources via Network.setBlockedURLs to save
298
+ * residential proxy bandwidth. Single CDP command, zero per-request overhead.
299
+ * Applied per-context on every page (existing and future). */
300
+ async applyProxyResourceBlocking(context) {
301
+ // URL patterns using the URLPattern spec syntax (absolute patterns).
302
+ // Covers the vast majority of image/video/font resources by file extension.
303
+ const blockedPatterns = [
304
+ // Images (SVGs excluded — lightweight and often used for icons/UI)
305
+ '*.png', '*.jpg', '*.jpeg', '*.gif', '*.webp', '*.ico', '*.bmp', '*.avif',
306
+ ];
307
+ const applyToPage = async (page) => {
308
+ try {
309
+ const cdpSession = await page.context().newCDPSession(page);
310
+ await cdpSession.send('Network.enable');
311
+ await cdpSession.send('Network.setBlockedURLs', {
312
+ urls: blockedPatterns,
313
+ });
314
+ await cdpSession.detach();
315
+ }
316
+ catch (err) {
317
+ // Best-effort: don't break the session if blocking fails
318
+ this.logger.error('Failed to apply proxy resource blocking:', err);
319
+ }
320
+ };
321
+ // Apply to existing pages
322
+ const pages = context.pages().filter((p) => !p.isClosed());
323
+ await Promise.all(pages.map(applyToPage));
324
+ // Apply to future pages
325
+ context.on('page', (page) => {
326
+ applyToPage(page);
327
+ });
328
+ this.logger.log('Proxy bandwidth acceleration enabled: blocking raster images');
329
+ }
330
+ clearUserState() {
331
+ Object.keys(this.userState).forEach((key) => delete this.userState[key]);
332
+ }
333
+ clearConnectionState() {
334
+ this.isConnected = false;
335
+ this.browser = null;
336
+ this.page = null;
337
+ this.context = null;
338
+ }
339
+ enqueueWarning(message) {
340
+ this.nextWarningEventId += 1;
341
+ this.warningEvents.push({ id: this.nextWarningEventId, message });
342
+ }
343
+ /** Update the cloud session timeout from external tracking (relay timer). */
344
+ updateCloudTimeout(timeoutAt) {
345
+ if (this.cloudSession) {
346
+ this.cloudSession.timeoutAt = timeoutAt;
347
+ }
348
+ }
349
+ beginWarningScope() {
350
+ // Use lastDeliveredWarningEventId as cursor (not nextWarningEventId) so
351
+ // warnings enqueued by the relay interval between execute() calls are
352
+ // picked up by the next scope. Using nextWarningEventId would skip them.
353
+ const scope = {
354
+ cursor: this.lastDeliveredWarningEventId,
355
+ };
356
+ this.activeWarningScopes.add(scope);
357
+ return scope;
358
+ }
359
+ flushWarningsForScope(scope) {
360
+ const relevantWarnings = this.warningEvents.filter((warning) => {
361
+ return warning.id > scope.cursor;
362
+ });
363
+ const latestWarningId = relevantWarnings.at(-1)?.id;
364
+ if (latestWarningId && latestWarningId > this.lastDeliveredWarningEventId) {
365
+ this.lastDeliveredWarningEventId = latestWarningId;
366
+ }
367
+ this.activeWarningScopes.delete(scope);
368
+ this.pruneDeliveredWarnings();
369
+ if (relevantWarnings.length === 0) {
370
+ return '';
371
+ }
372
+ return `${relevantWarnings.map((warning) => `[WARNING] ${warning.message}`).join('\n')}\n`;
373
+ }
374
+ pruneDeliveredWarnings() {
375
+ const activeCursors = [...this.activeWarningScopes].map((scope) => {
376
+ return scope.cursor;
377
+ });
378
+ const minActiveCursor = activeCursors.length > 0 ? Math.min(...activeCursors) : this.lastDeliveredWarningEventId;
379
+ const pruneBeforeOrAt = Math.min(this.lastDeliveredWarningEventId, minActiveCursor);
380
+ this.warningEvents = this.warningEvents.filter((warning) => {
381
+ return warning.id > pruneBeforeOrAt;
382
+ });
383
+ }
384
+ warnIfExtensionOutdated(playwriterVersion) {
385
+ if (this.hasWarnedExtensionOutdated) {
386
+ return;
387
+ }
388
+ const warning = getExtensionOutdatedWarning(playwriterVersion);
389
+ if (warning) {
390
+ this.logger.log(warning);
391
+ // Enqueue so MCP agents see version-skew messages in their next execute
392
+ // response — logger.log alone only reaches stdout, not the LLM.
393
+ this.enqueueWarning(warning);
394
+ this.hasWarnedExtensionOutdated = true;
395
+ }
396
+ }
397
+ setupPageListeners(page) {
398
+ if (this.pagesWithListeners.has(page)) {
399
+ return;
400
+ }
401
+ this.pagesWithListeners.add(page);
402
+ this.setupPageCloseDetection(page);
403
+ this.setupPageConsoleListener(page);
404
+ this.setupNewPageLogging(page);
405
+ this.ghostCursorController.attachToPage({ page });
406
+ page.on('close', () => {
407
+ this.ghostCursorController.detachFromPage({ page });
408
+ });
409
+ }
410
+ setupPageCloseDetection(page) {
411
+ page.on('close', () => {
412
+ const stateKeysForClosedPage = Object.entries(this.userState)
413
+ .filter(([, value]) => {
414
+ return value === page;
415
+ })
416
+ .map(([key]) => key);
417
+ const wasCurrentPage = this.page === page;
418
+ let replacementPageInfo = null;
419
+ if (wasCurrentPage) {
420
+ this.page = null;
421
+ const context = this.context || page.context();
422
+ const openPages = context.pages().filter((candidate) => {
423
+ return !candidate.isClosed();
424
+ });
425
+ if (openPages.length > 0) {
426
+ const replacementPage = openPages[0];
427
+ this.page = replacementPage;
428
+ const replacementIndex = context.pages().indexOf(replacementPage);
429
+ replacementPageInfo = {
430
+ index: replacementIndex >= 0 ? String(replacementIndex) : 'unknown',
431
+ url: replacementPage.url() || 'unknown',
432
+ };
433
+ }
434
+ }
435
+ if (!this.isConnected || this.suppressPageCloseWarnings || stateKeysForClosedPage.length === 0) {
436
+ return;
437
+ }
438
+ const stateKeyLabel = stateKeysForClosedPage.map((key) => `state.${key}`).join(', ');
439
+ const closedUrl = page.url() || 'unknown';
440
+ if (!wasCurrentPage) {
441
+ this.enqueueWarning(`Page closed (url: ${closedUrl}) for ${stateKeyLabel}. ` +
442
+ `Assign a new open page to ${stateKeyLabel} before reusing it.`);
443
+ return;
444
+ }
445
+ if (replacementPageInfo) {
446
+ this.enqueueWarning(`The current page in ${stateKeyLabel} was closed (url: ${closedUrl}). ` +
447
+ `Switched active page to index ${replacementPageInfo.index} (url: ${replacementPageInfo.url}). ` +
448
+ `Reassign ${stateKeyLabel} before using it again.`);
449
+ return;
450
+ }
451
+ this.enqueueWarning(`The current page in ${stateKeyLabel} was closed (url: ${closedUrl}). ` +
452
+ `No open pages remain. Open a tab with Tabwright enabled, then reassign ${stateKeyLabel}.`);
453
+ });
454
+ }
455
+ setupNewPageLogging(page) {
456
+ // page.on('popup') fires for window.open, target=_blank, and cmd+click
457
+ // (but not context.newPage() or CDP reconnection). The extension
458
+ // auto-relocates popups to tabs, so these pages are controllable via
459
+ // context.pages(). Enqueue synchronously so the warning lands in the
460
+ // enclosing execute() call's scope. initialUrl may be 'about:blank'
461
+ // for blank-then-scripted popups.
462
+ page.on('popup', (popup) => {
463
+ const pages = popup.context().pages();
464
+ const rawIndex = pages.indexOf(popup);
465
+ const pageIndex = rawIndex >= 0 ? String(rawIndex) : 'unknown';
466
+ const initialUrl = popup.url() || 'about:blank';
467
+ this.enqueueWarning(`New page opened from current page (index ${pageIndex}, initial url: ${initialUrl}). ` +
468
+ `Access it via context.pages()[${pageIndex}] to interact with it.`);
469
+ });
470
+ }
471
+ setupPageConsoleListener(page) {
472
+ if (!this.browserLogs.has(page)) {
473
+ this.browserLogs.set(page, []);
474
+ }
475
+ // Logs are NOT cleared on navigation so that getLatestLogs({ sinceLastCall: true })
476
+ // can return errors from the previous page load. The MAX_LOGS_PER_PAGE cap (5000)
477
+ // prevents unbounded growth; old entries are shifted out in addBrowserLog.
478
+ page.on('close', () => {
479
+ this.browserLogs.delete(page);
480
+ this.pageLogCursor.delete(page);
481
+ });
482
+ page.on('console', (msg) => {
483
+ try {
484
+ const logEntry = `[${msg.type()}] ${msg.text()}`;
485
+ this.addBrowserLog({ page, logEntry });
486
+ }
487
+ catch (e) {
488
+ this.logger.error('[Executor] Failed to get console message text:', e);
489
+ }
490
+ });
491
+ page.on('pageerror', (error) => {
492
+ this.addBrowserLog({ page, logEntry: `[pageerror] ${error.message}` });
493
+ });
494
+ }
495
+ addBrowserLog(options) {
496
+ if (!this.browserLogs.has(options.page)) {
497
+ this.browserLogs.set(options.page, []);
498
+ }
499
+ const pageLogs = this.browserLogs.get(options.page);
500
+ pageLogs.push(options.logEntry);
501
+ if (pageLogs.length > MAX_LOGS_PER_PAGE) {
502
+ pageLogs.shift();
503
+ // Decrement cursor so it stays in sync with the shifted array.
504
+ // Clamp to 0 so the cursor never goes negative.
505
+ const cursor = this.pageLogCursor.get(options.page);
506
+ if (cursor !== undefined && cursor > 0) {
507
+ this.pageLogCursor.set(options.page, cursor - 1);
508
+ }
509
+ }
510
+ }
511
+ pagesRelatedToPage(page) {
512
+ const frameUrls = new Set(page
513
+ .frames()
514
+ .map((frame) => {
515
+ return frame.url();
516
+ })
517
+ .filter((url) => {
518
+ return url && url !== 'about:blank';
519
+ }));
520
+ return page
521
+ .context()
522
+ .pages()
523
+ .filter((candidate) => {
524
+ return candidate === page || frameUrls.has(candidate.url());
525
+ });
526
+ }
527
+ async checkExtensionStatus() {
528
+ const { host = '127.0.0.1', port = 19988, extensionId, token } = this.cdpConfig;
529
+ const { httpBaseUrl } = parseRelayHost(host, port);
530
+ const notConnected = { connected: false, activeTargets: 0, playwriterVersion: null };
531
+ const headers = {};
532
+ const effectiveToken = token || process.env.TABWRIGHT_TOKEN || process.env.PLAYWRITER_TOKEN;
533
+ if (effectiveToken) {
534
+ headers['Authorization'] = `Bearer ${effectiveToken}`;
535
+ }
536
+ try {
537
+ if (extensionId) {
538
+ const response = await fetch(`${httpBaseUrl}/extensions/status`, {
539
+ signal: AbortSignal.timeout(2000),
540
+ headers,
541
+ });
542
+ if (!response.ok) {
543
+ const fallback = await fetch(`${httpBaseUrl}/extension/status`, {
544
+ signal: AbortSignal.timeout(2000),
545
+ headers,
546
+ });
547
+ if (!fallback.ok) {
548
+ return notConnected;
549
+ }
550
+ return (await fallback.json());
551
+ }
552
+ const data = (await response.json());
553
+ const extension = data.extensions.find((item) => {
554
+ return item.extensionId === extensionId || item.stableKey === extensionId;
555
+ });
556
+ if (!extension) {
557
+ return notConnected;
558
+ }
559
+ return {
560
+ connected: true,
561
+ activeTargets: extension.activeTargets,
562
+ playwriterVersion: extension?.playwriterVersion || null,
563
+ };
564
+ }
565
+ const response = await fetch(`${httpBaseUrl}/extension/status`, {
566
+ signal: AbortSignal.timeout(2000),
567
+ headers,
568
+ });
569
+ if (!response.ok) {
570
+ return notConnected;
571
+ }
572
+ return (await response.json());
573
+ }
574
+ catch {
575
+ return notConnected;
576
+ }
577
+ }
578
+ isDirectCdpMode() {
579
+ return !!this.cdpConfig.directCdpUrl;
580
+ }
581
+ isHeadlessMode() {
582
+ return !!this.cdpConfig.headless;
583
+ }
584
+ /**
585
+ * Connect to Chrome and set up context/page. Shared by ensureConnection and reset.
586
+ * In headless mode, launches Chrome via chromium.launch().
587
+ * In direct CDP mode, connects straight to Chrome's WebSocket.
588
+ * In extension mode, checks extension status then connects via relay.
589
+ */
590
+ async connectToBrowser() {
591
+ // Headless mode: launch Chrome directly via Playwright (no extension, no relay CDP routing)
592
+ if (this.isHeadlessMode()) {
593
+ return this.connectHeadlessBrowser();
594
+ }
595
+ if (this.isDirectCdpMode()) {
596
+ // Direct CDP: connect straight to Chrome, no relay or extension needed
597
+ const chromium = await getChromium();
598
+ const browser = await chromium.connectOverCDP(this.cdpConfig.directCdpUrl);
599
+ browser.on('disconnected', () => {
600
+ this.logger.log('Browser disconnected, clearing connection state');
601
+ this.clearConnectionState();
602
+ });
603
+ const contexts = browser.contexts();
604
+ const context = contexts.length > 0 ? contexts[0] : await browser.newContext();
605
+ context.setDefaultTimeout(60000);
606
+ context.setDefaultNavigationTimeout(10000);
607
+ context.on('page', (page) => {
608
+ this.setupPageListeners(page);
609
+ });
610
+ context.pages().forEach((p) => this.setupPageListeners(p));
611
+ // In direct CDP mode, pages are always available (all tabs visible).
612
+ // Use the first non-closed page, or create one.
613
+ const pages = context.pages().filter((p) => !p.isClosed());
614
+ const page = pages.length > 0 ? pages[0] : await context.newPage();
615
+ this.setupPageListeners(page);
616
+ await this.setDeviceScaleFactorForMacOS(context);
617
+ // Block images, video, and fonts for cloud sessions with proxy enabled
618
+ // to reduce residential proxy bandwidth costs. Uses Network.setBlockedURLs
619
+ // which is a single fire-and-forget CDP command with zero per-request overhead.
620
+ if (this.cloudSession?.blockProxyResources) {
621
+ await this.applyProxyResourceBlocking(context);
622
+ }
623
+ return { browser, page, context };
624
+ }
625
+ // Extension mode: check status first for better error messages
626
+ const extensionStatus = await this.checkExtensionStatus();
627
+ if (!extensionStatus.connected) {
628
+ throw new Error(EXTENSION_NOT_CONNECTED_ERROR);
629
+ }
630
+ this.warnIfExtensionOutdated(extensionStatus.playwriterVersion);
631
+ const cdpUrl = getCdpUrl(this.cdpConfig);
632
+ const chromium = await getChromium();
633
+ const browser = await chromium.connectOverCDP(cdpUrl);
634
+ browser.on('disconnected', () => {
635
+ this.logger.log('Browser disconnected, clearing connection state');
636
+ this.clearConnectionState();
637
+ });
638
+ const contexts = browser.contexts();
639
+ const context = contexts.length > 0 ? contexts[0] : await browser.newContext();
640
+ // Action timeout (click, fill, hover, etc.) is longer to tolerate slower
641
+ // SPA/Turbo navigations and post-click settling on real sites.
642
+ // Navigation timeout (goto, reload) remains separate.
643
+ context.setDefaultTimeout(60000);
644
+ context.setDefaultNavigationTimeout(10000);
645
+ context.on('page', (page) => {
646
+ this.setupPageListeners(page);
647
+ });
648
+ context.pages().forEach((p) => this.setupPageListeners(p));
649
+ const page = await this.ensurePageForContext({ context, timeout: 10000 });
650
+ await this.setDeviceScaleFactorForMacOS(context);
651
+ return { browser, page, context };
652
+ }
653
+ /**
654
+ * Launch a headless Chrome via chromium.launch(). No extension, no relay CDP routing.
655
+ * Reuses an existing shared browser if one was already launched for headless mode.
656
+ * Does NOT add per-session disconnect listeners to avoid accumulation on the shared
657
+ * browser; instead, ensureConnection checks browser.isConnected() on each call.
658
+ */
659
+ async connectHeadlessBrowser() {
660
+ const browser = await PlaywrightExecutor.getOrLaunchHeadlessBrowser();
661
+ const context = await browser.newContext();
662
+ context.setDefaultTimeout(60000);
663
+ context.setDefaultNavigationTimeout(10000);
664
+ context.on('page', (page) => {
665
+ this.setupPageListeners(page);
666
+ });
667
+ const page = await context.newPage();
668
+ this.setupPageListeners(page);
669
+ await this.setDeviceScaleFactorForMacOS(context);
670
+ return { browser, page, context };
671
+ }
672
+ /** Shared headless browser instance across all headless sessions.
673
+ * Uses a launch promise to prevent concurrent first-session races from
674
+ * spawning multiple browsers. The disconnect handler is registered once
675
+ * at launch time and clears both statics so the next session relaunches. */
676
+ static _sharedHeadlessBrowser = null;
677
+ static _sharedHeadlessBrowserPromise = null;
678
+ static async getOrLaunchHeadlessBrowser() {
679
+ // Check the cached browser is actually alive (not just non-null after a crash)
680
+ if (PlaywrightExecutor._sharedHeadlessBrowser?.isConnected()) {
681
+ return PlaywrightExecutor._sharedHeadlessBrowser;
682
+ }
683
+ // Deduplicate concurrent launches: second caller awaits the first's promise
684
+ if (PlaywrightExecutor._sharedHeadlessBrowserPromise) {
685
+ return PlaywrightExecutor._sharedHeadlessBrowserPromise;
686
+ }
687
+ const launchPromise = (async () => {
688
+ const chromium = await getChromium();
689
+ const { resolveBrowserExecutablePath } = await import('./browser-config.js');
690
+ const executablePath = resolveBrowserExecutablePath();
691
+ const browser = await chromium.launch({
692
+ headless: true,
693
+ executablePath,
694
+ });
695
+ // Single handler registered once per browser lifetime.
696
+ // Clears both statics so the next headless session relaunches.
697
+ browser.on('disconnected', () => {
698
+ PlaywrightExecutor._sharedHeadlessBrowser = null;
699
+ PlaywrightExecutor._sharedHeadlessBrowserPromise = null;
700
+ });
701
+ PlaywrightExecutor._sharedHeadlessBrowser = browser;
702
+ // Clear the promise now that the browser is cached; future callers
703
+ // use _sharedHeadlessBrowser directly. Concurrent waiters already
704
+ // hold a reference to launchPromise so they still resolve correctly.
705
+ PlaywrightExecutor._sharedHeadlessBrowserPromise = null;
706
+ return browser;
707
+ })();
708
+ PlaywrightExecutor._sharedHeadlessBrowserPromise = launchPromise;
709
+ try {
710
+ return await launchPromise;
711
+ }
712
+ catch (error) {
713
+ PlaywrightExecutor._sharedHeadlessBrowserPromise = null;
714
+ throw error;
715
+ }
716
+ }
717
+ /** Close the headless context for this session (called on session delete). */
718
+ async closeHeadlessContext() {
719
+ if (!this.isHeadlessMode() || !this.context) {
720
+ return;
721
+ }
722
+ await this.context.close().catch((e) => {
723
+ this.logger.error('Error closing headless context:', e);
724
+ });
725
+ this.clearConnectionState();
726
+ }
727
+ /** Close the shared headless browser (called on relay shutdown). */
728
+ static async closeSharedHeadlessBrowser() {
729
+ if (PlaywrightExecutor._sharedHeadlessBrowser) {
730
+ await PlaywrightExecutor._sharedHeadlessBrowser.close().catch(() => { });
731
+ PlaywrightExecutor._sharedHeadlessBrowser = null;
732
+ PlaywrightExecutor._sharedHeadlessBrowserPromise = null;
733
+ }
734
+ }
735
+ async ensureConnection() {
736
+ // In headless mode, also check the shared browser is still alive.
737
+ // After a crash, isConnected() returns false and we need to reconnect.
738
+ const browserAlive = this.isHeadlessMode() ? this.browser?.isConnected() : true;
739
+ if (this.isConnected && this.browser && this.page && browserAlive) {
740
+ return { browser: this.browser, page: this.page };
741
+ }
742
+ try {
743
+ const { browser, page, context } = await this.connectToBrowser();
744
+ this.browser = browser;
745
+ this.page = page;
746
+ this.context = context;
747
+ this.isConnected = true;
748
+ return { browser, page };
749
+ }
750
+ catch (error) {
751
+ // Cloud sessions that fail to connect are likely expired VMs.
752
+ // Give a clear error instead of a cryptic WebSocket/connection error.
753
+ if (this.cloudSession && error instanceof Error && isDisconnectionError(error)) {
754
+ throw new Error(CLOUD_SESSION_EXPIRED_ERROR, { cause: error });
755
+ }
756
+ throw error;
757
+ }
758
+ }
759
+ async getCurrentPage(timeout = 10000) {
760
+ if (this.page && !this.page.isClosed()) {
761
+ return this.page;
762
+ }
763
+ if (this.browser) {
764
+ const contexts = this.browser.contexts();
765
+ if (contexts.length > 0) {
766
+ const context = contexts[0];
767
+ this.context = context;
768
+ const pages = context.pages().filter((p) => !p.isClosed());
769
+ if (pages.length > 0) {
770
+ const page = pages[0];
771
+ await page.waitForLoadState('domcontentloaded', { timeout }).catch(() => { });
772
+ this.page = page;
773
+ return page;
774
+ }
775
+ const page = await this.ensurePageForContext({ context, timeout });
776
+ this.page = page;
777
+ return page;
778
+ }
779
+ }
780
+ throw new Error(NO_PAGES_AVAILABLE_ERROR);
781
+ }
782
+ async reset() {
783
+ this.suppressPageCloseWarnings = true;
784
+ try {
785
+ if (this.isHeadlessMode()) {
786
+ // In headless mode, only close this session's context, not the shared browser.
787
+ // Other headless sessions share the same browser instance.
788
+ if (this.context) {
789
+ await this.context.close().catch((e) => {
790
+ this.logger.error('Error closing context:', e);
791
+ });
792
+ }
793
+ }
794
+ else if (this.browser) {
795
+ await this.browser.close();
796
+ }
797
+ }
798
+ catch (e) {
799
+ this.logger.error('Error closing browser:', e);
800
+ }
801
+ finally {
802
+ this.suppressPageCloseWarnings = false;
803
+ }
804
+ this.clearConnectionState();
805
+ this.clearUserState();
806
+ const { browser, page, context } = await this.connectToBrowser();
807
+ this.browser = browser;
808
+ this.page = page;
809
+ this.context = context;
810
+ this.isConnected = true;
811
+ return { page, context };
812
+ }
813
+ async execute(code, timeout = 10000, options = {}) {
814
+ const consoleLogs = [];
815
+ const warningScope = this.beginWarningScope();
816
+ const formatConsoleLogs = (logs, prefix = 'Console output') => {
817
+ if (logs.length === 0) {
818
+ return '';
819
+ }
820
+ let text = `${prefix}:\n`;
821
+ logs.forEach(({ method, args }) => {
822
+ const formattedArgs = args
823
+ .map((arg) => {
824
+ if (typeof arg === 'string')
825
+ return arg;
826
+ return util.inspect(arg, {
827
+ depth: 4,
828
+ colors: false,
829
+ maxArrayLength: 100,
830
+ maxStringLength: 1000,
831
+ breakLength: 80,
832
+ });
833
+ })
834
+ .join(' ');
835
+ text += `[${method}] ${formattedArgs}\n`;
836
+ });
837
+ return text + '\n';
838
+ };
839
+ try {
840
+ // Warn if cloud VM is approaching its hard timeout (deduped by minute bucket)
841
+ if (this.cloudSession?.timeoutAt) {
842
+ const remainingMs = this.cloudSession.timeoutAt - Date.now();
843
+ if (remainingMs <= 0) {
844
+ throw new Error(CLOUD_SESSION_EXPIRED_ERROR);
845
+ }
846
+ if (remainingMs < 5 * 60_000) {
847
+ const mins = Math.ceil(remainingMs / 60_000);
848
+ if (this.lastCloudTimeoutWarningMinute !== mins) {
849
+ this.lastCloudTimeoutWarningMinute = mins;
850
+ this.enqueueWarning(`Cloud browser expires in ~${mins} minute${mins === 1 ? '' : 's'}. ` +
851
+ `Create a new session soon with: tabwright session new --browser cloud`);
852
+ }
853
+ }
854
+ }
855
+ await this.ensureConnection();
856
+ const page = await this.getCurrentPage(timeout);
857
+ const context = this.context || page.context();
858
+ this.logger.log('Executing code:', code);
859
+ const customConsole = {
860
+ log: (...args) => {
861
+ consoleLogs.push({ method: 'log', args });
862
+ },
863
+ info: (...args) => {
864
+ consoleLogs.push({ method: 'info', args });
865
+ },
866
+ warn: (...args) => {
867
+ consoleLogs.push({ method: 'warn', args });
868
+ },
869
+ error: (...args) => {
870
+ consoleLogs.push({ method: 'error', args });
871
+ },
872
+ debug: (...args) => {
873
+ consoleLogs.push({ method: 'debug', args });
874
+ },
875
+ };
876
+ const snapshot = async (options) => {
877
+ const { page: targetPage, frame, locator, search, showDiffSinceLastCall = !search, interactiveOnly = false, } = options;
878
+ const resolvedPage = targetPage || page;
879
+ if (!resolvedPage) {
880
+ throw new Error('snapshot requires a page');
881
+ }
882
+ // Use new in-page implementation via getAriaSnapshot
883
+ const { snapshot: rawSnapshot, refs, getSelectorForRef, } = await getAriaSnapshot({
884
+ page: resolvedPage,
885
+ frame,
886
+ locator,
887
+ interactiveOnly,
888
+ });
889
+ const snapshotStr = rawSnapshot.toWellFormed?.() ?? rawSnapshot;
890
+ const refToLocator = new Map();
891
+ for (const entry of refs) {
892
+ const locatorStr = getSelectorForRef(entry.ref);
893
+ if (locatorStr) {
894
+ refToLocator.set(entry.shortRef, locatorStr);
895
+ }
896
+ }
897
+ this.lastRefToLocator.set(resolvedPage, refToLocator);
898
+ const shouldCacheSnapshot = !frame;
899
+ // Cache keyed by locator selector so full-page and locator-scoped snapshots
900
+ // don't pollute each other's diff baselines
901
+ const snapshotKey = locator ? `locator:${locator.selector()}` : 'page';
902
+ let pageSnapshots = this.lastSnapshots.get(resolvedPage);
903
+ if (!pageSnapshots) {
904
+ pageSnapshots = new Map();
905
+ this.lastSnapshots.set(resolvedPage, pageSnapshots);
906
+ }
907
+ const previousSnapshot = shouldCacheSnapshot ? pageSnapshots.get(snapshotKey) : undefined;
908
+ if (shouldCacheSnapshot) {
909
+ pageSnapshots.set(snapshotKey, snapshotStr);
910
+ }
911
+ // Diff defaults off when search is provided, but agent can explicitly enable both
912
+ if (showDiffSinceLastCall && previousSnapshot && shouldCacheSnapshot) {
913
+ const diffResult = createSmartDiff({
914
+ oldContent: previousSnapshot,
915
+ newContent: snapshotStr,
916
+ label: 'snapshot',
917
+ });
918
+ if (diffResult.type === 'no-change') {
919
+ return 'No changes since last snapshot. Use showDiffSinceLastCall: false to see full content.';
920
+ }
921
+ return diffResult.content;
922
+ }
923
+ if (!search) {
924
+ return `${snapshotStr}\n\nuse refToLocator({ ref: 'e3' }) to get locators for ref strings.`;
925
+ }
926
+ const lines = snapshotStr.split('\n');
927
+ const matchIndices = [];
928
+ for (let i = 0; i < lines.length; i++) {
929
+ const line = lines[i];
930
+ const isMatch = isRegExp(search) ? search.test(line) : line.includes(search);
931
+ if (isMatch) {
932
+ matchIndices.push(i);
933
+ if (matchIndices.length >= 10)
934
+ break;
935
+ }
936
+ }
937
+ if (matchIndices.length === 0) {
938
+ return 'No matches found';
939
+ }
940
+ const CONTEXT_LINES = 5;
941
+ const includedLines = new Set();
942
+ for (const idx of matchIndices) {
943
+ const start = Math.max(0, idx - CONTEXT_LINES);
944
+ const end = Math.min(lines.length - 1, idx + CONTEXT_LINES);
945
+ for (let i = start; i <= end; i++) {
946
+ includedLines.add(i);
947
+ }
948
+ }
949
+ const sortedIndices = [...includedLines].sort((a, b) => a - b);
950
+ const result = [];
951
+ for (let i = 0; i < sortedIndices.length; i++) {
952
+ const lineIdx = sortedIndices[i];
953
+ if (i > 0 && sortedIndices[i - 1] !== lineIdx - 1) {
954
+ result.push('---');
955
+ }
956
+ result.push(lines[lineIdx]);
957
+ }
958
+ return result.join('\n');
959
+ };
960
+ const refToLocator = (options) => {
961
+ const targetPage = options.page || page;
962
+ const map = this.lastRefToLocator.get(targetPage);
963
+ if (!map) {
964
+ return null;
965
+ }
966
+ return map.get(options.ref) ?? null;
967
+ };
968
+ const getLocatorStringForElement = async (element) => {
969
+ if (!element || typeof element.evaluate !== 'function') {
970
+ throw new Error('getLocatorStringForElement: argument must be a Playwright Locator or ElementHandle');
971
+ }
972
+ const elementPage = element.page ? element.page() : page;
973
+ const hasGenerator = await elementPage.evaluate(() => !!globalThis.__selectorGenerator);
974
+ if (!hasGenerator) {
975
+ const scriptPath = path.join(__dirname, '..', 'dist', 'selector-generator.js');
976
+ const scriptContent = fs.readFileSync(scriptPath, 'utf-8');
977
+ const cdp = await getCDPSession({ page: elementPage });
978
+ await cdp.send('Runtime.evaluate', { expression: scriptContent });
979
+ }
980
+ return await element.evaluate((el) => {
981
+ const { createSelectorGenerator, toLocator } = globalThis.__selectorGenerator;
982
+ const generator = createSelectorGenerator(globalThis);
983
+ const result = generator(el);
984
+ return toLocator(result.selector, 'javascript');
985
+ });
986
+ };
987
+ const getLatestLogs = async (options) => {
988
+ const { page: filterPage, count, search, sinceLastCall = false } = options || {};
989
+ let allLogs = [];
990
+ // Collect logs, optionally slicing from cursor when sinceLastCall is set
991
+ const collectLogs = (targetPage) => {
992
+ const logs = this.browserLogs.get(targetPage) || [];
993
+ if (!sinceLastCall) {
994
+ return logs;
995
+ }
996
+ const cursor = this.pageLogCursor.get(targetPage) || 0;
997
+ return logs.slice(cursor);
998
+ };
999
+ if (filterPage) {
1000
+ const relatedPages = this.pagesRelatedToPage(filterPage);
1001
+ allLogs = relatedPages.flatMap((relatedPage) => {
1002
+ return collectLogs(relatedPage);
1003
+ });
1004
+ }
1005
+ else {
1006
+ for (const [p] of this.browserLogs) {
1007
+ allLogs.push(...collectLogs(p));
1008
+ }
1009
+ }
1010
+ // Advance cursors after collecting so next sinceLastCall call starts fresh
1011
+ if (sinceLastCall) {
1012
+ const pagesToAdvance = filterPage
1013
+ ? this.pagesRelatedToPage(filterPage)
1014
+ : [...this.browserLogs.keys()];
1015
+ for (const p of pagesToAdvance) {
1016
+ const logs = this.browserLogs.get(p);
1017
+ if (logs) {
1018
+ this.pageLogCursor.set(p, logs.length);
1019
+ }
1020
+ }
1021
+ }
1022
+ if (search) {
1023
+ const matchIndices = [];
1024
+ for (let i = 0; i < allLogs.length; i++) {
1025
+ const log = allLogs[i];
1026
+ const isMatch = typeof search === 'string' ? log.includes(search) : isRegExp(search) && search.test(log);
1027
+ if (isMatch)
1028
+ matchIndices.push(i);
1029
+ }
1030
+ const CONTEXT_LINES = 5;
1031
+ const includedIndices = new Set();
1032
+ for (const idx of matchIndices) {
1033
+ const start = Math.max(0, idx - CONTEXT_LINES);
1034
+ const end = Math.min(allLogs.length - 1, idx + CONTEXT_LINES);
1035
+ for (let i = start; i <= end; i++) {
1036
+ includedIndices.add(i);
1037
+ }
1038
+ }
1039
+ const sortedIndices = [...includedIndices].sort((a, b) => a - b);
1040
+ const result = [];
1041
+ for (let i = 0; i < sortedIndices.length; i++) {
1042
+ const logIdx = sortedIndices[i];
1043
+ if (i > 0 && sortedIndices[i - 1] !== logIdx - 1) {
1044
+ result.push('---');
1045
+ }
1046
+ result.push(allLogs[logIdx]);
1047
+ }
1048
+ allLogs = result;
1049
+ }
1050
+ return count !== undefined ? allLogs.slice(-count) : allLogs;
1051
+ };
1052
+ const clearAllLogs = () => {
1053
+ this.browserLogs.clear();
1054
+ this.pageLogCursor.clear();
1055
+ };
1056
+ const getCDPSession = async (options) => {
1057
+ if (options.page.isClosed()) {
1058
+ throw new Error('Cannot create CDP session for closed page');
1059
+ }
1060
+ return await getCDPSessionForPage({ page: options.page });
1061
+ };
1062
+ const createDebugger = (options) => new Debugger(options);
1063
+ const createEditor = (options) => new Editor(options);
1064
+ const getStylesForLocatorFn = async (options) => {
1065
+ const cdp = await getCDPSession({ page: options.locator.page() });
1066
+ return getStylesForLocator({ locator: options.locator, cdp });
1067
+ };
1068
+ const getReactSourceFn = async (options) => {
1069
+ const cdp = await getCDPSession({ page: options.locator.page() });
1070
+ return getReactSource({ locator: options.locator, cdp });
1071
+ };
1072
+ const getReactComponentInfoFn = async (options) => {
1073
+ const targetPage = await (async () => {
1074
+ if ('page' in options.locator) {
1075
+ return options.locator.page();
1076
+ }
1077
+ return (await options.locator.ownerFrame())?.page() ?? null;
1078
+ })();
1079
+ if (!targetPage) {
1080
+ throw new Error('Could not get page from locator');
1081
+ }
1082
+ const cdp = await getCDPSession({ page: targetPage });
1083
+ return getReactComponentInfo({ locator: options.locator, cdp });
1084
+ };
1085
+ const inspectPinnedElement = async (pageUrl, elementExpression) => {
1086
+ const targetPage = context.pages().find((candidate) => candidate.url() === pageUrl) || context.pages()[0];
1087
+ if (!targetPage) {
1088
+ throw new Error('No Playwright pages are available');
1089
+ }
1090
+ this.userState.page = targetPage;
1091
+ const handle = (await targetPage.evaluateHandle((expression) => {
1092
+ return Function(`return (${expression})`)();
1093
+ }, elementExpression)).asElement();
1094
+ const result = await (async () => {
1095
+ if (!handle) {
1096
+ return { url: targetPage.url(), outerHTML: null, react: null };
1097
+ }
1098
+ return {
1099
+ url: targetPage.url(),
1100
+ outerHTML: await handle.evaluate((el) => el.outerHTML),
1101
+ react: await getReactComponentInfoFn({ locator: handle }),
1102
+ };
1103
+ })();
1104
+ console.log(result);
1105
+ return result;
1106
+ };
1107
+ const screenshotCollector = [];
1108
+ // Separate collector for images produced by resizeImageForAgent() calls.
1109
+ // These get merged into result.images so the CLI can emit them via Kitty Graphics.
1110
+ const resizedImageCollector = [];
1111
+ const resizeImageForAgentFn = async (options) => {
1112
+ const result = await resizeImageForAgent(options);
1113
+ resizedImageCollector.push({ data: result.buffer.toString('base64'), mimeType: result.mimeType });
1114
+ return result;
1115
+ };
1116
+ const screenshotWithAccessibilityLabelsFn = async (options) => {
1117
+ return screenshotWithAccessibilityLabels({
1118
+ ...options,
1119
+ collector: screenshotCollector,
1120
+ logger: {
1121
+ info: (...args) => {
1122
+ this.logger.error('[tabwright]', ...args);
1123
+ },
1124
+ error: (...args) => {
1125
+ this.logger.error('[tabwright]', ...args);
1126
+ },
1127
+ },
1128
+ });
1129
+ };
1130
+ const relayPort = this.cdpConfig.port || 19988;
1131
+ const self = this;
1132
+ const ghostCursorController = this.ghostCursorController;
1133
+ const showGhostCursor = async (options) => {
1134
+ const targetPage = options?.page || page;
1135
+ const cursorOptions = (() => {
1136
+ if (!options) {
1137
+ return undefined;
1138
+ }
1139
+ const { page: _ignoredPage, ...rest } = options;
1140
+ return rest;
1141
+ })();
1142
+ await ghostCursorController.show({ page: targetPage, cursorOptions });
1143
+ };
1144
+ const hideGhostCursor = async (options) => {
1145
+ const targetPage = options?.page || page;
1146
+ await ghostCursorController.hide({ page: targetPage });
1147
+ };
1148
+ const replayApi = createReplayApi({
1149
+ context,
1150
+ defaultPage: page,
1151
+ relayPort,
1152
+ });
1153
+ const workflow = {
1154
+ saveCapability: (input) => {
1155
+ return saveWorkflowCapability({
1156
+ ...input,
1157
+ cwd: self.sessionCwd || process.cwd(),
1158
+ });
1159
+ },
1160
+ saveFromRecording: (input) => {
1161
+ return saveWorkflowFromRecording({
1162
+ ...input,
1163
+ cwd: self.sessionCwd || process.cwd(),
1164
+ });
1165
+ },
1166
+ };
1167
+ // Ghost Browser API - creates chrome object that mirrors Ghost Browser's APIs
1168
+ // See extension/src/ghost-browser-api.d.ts for full API documentation
1169
+ const chromeGhostBrowser = createGhostBrowserChrome(async (namespace, method, args) => {
1170
+ const cdp = await getCDPSession({ page });
1171
+ const result = await cdp.send('ghost-browser', { namespace, method, args });
1172
+ const typed = result;
1173
+ if (!typed.success) {
1174
+ throw new Error(typed.error || `Ghost Browser API call failed: ${namespace}.${method}`);
1175
+ }
1176
+ return typed.result;
1177
+ });
1178
+ let vmContextObj = {
1179
+ page,
1180
+ context,
1181
+ browser: this.browser,
1182
+ state: this.userState,
1183
+ console: customConsole,
1184
+ snapshot,
1185
+ accessibilitySnapshot: snapshot, // backward compat alias
1186
+ refToLocator,
1187
+ getCleanHTML,
1188
+ getPageMarkdown,
1189
+ getLocatorStringForElement,
1190
+ getLatestLogs,
1191
+ clearAllLogs,
1192
+ waitForPageLoad,
1193
+ getCDPSession,
1194
+ createDebugger,
1195
+ createEditor,
1196
+ getStylesForLocator: getStylesForLocatorFn,
1197
+ formatStylesAsText,
1198
+ getReactSource: getReactSourceFn,
1199
+ getReactComponentInfo: getReactComponentInfoFn,
1200
+ inspectPinnedElement,
1201
+ screenshotWithAccessibilityLabels: screenshotWithAccessibilityLabelsFn,
1202
+ resizeImageForAgent: resizeImageForAgentFn,
1203
+ // Backward-compatible alias for resizeImageForAgent
1204
+ resizeImage: resizeImageForAgentFn,
1205
+ ghostCursor: {
1206
+ show: showGhostCursor,
1207
+ hide: hideGhostCursor,
1208
+ },
1209
+ replay: {
1210
+ start: replayApi.start,
1211
+ stop: replayApi.stop,
1212
+ isRecording: replayApi.isRecording,
1213
+ cancel: replayApi.cancel,
1214
+ list: replayApi.list,
1215
+ events: replayApi.events,
1216
+ },
1217
+ workflow,
1218
+ // Backward-compatible aliases
1219
+ startReplay: replayApi.start,
1220
+ stopReplay: replayApi.stop,
1221
+ isReplayRecording: replayApi.isRecording,
1222
+ cancelReplay: replayApi.cancel,
1223
+ listReplays: replayApi.list,
1224
+ saveWorkflowCapability: workflow.saveCapability,
1225
+ saveWorkflowFromRecording: workflow.saveFromRecording,
1226
+ resetPlaywright: async () => {
1227
+ const { page: newPage, context: newContext } = await self.reset();
1228
+ vmContextObj.page = newPage;
1229
+ vmContextObj.context = newContext;
1230
+ vmContextObj.browser = self.browser;
1231
+ return { page: newPage, context: newContext };
1232
+ },
1233
+ require: this.sandboxedRequire,
1234
+ import: (specifier) => import(specifier),
1235
+ // Ghost Browser API - only works in Ghost Browser, mirrors chrome.ghostPublicAPI etc
1236
+ chrome: chromeGhostBrowser,
1237
+ ...usefulGlobals,
1238
+ // Expose process with safety overrides:
1239
+ // - cwd() returns the session's cwd instead of the relay server's cwd
1240
+ // - exit() is blocked to prevent killing the relay server
1241
+ // - chdir() is blocked to prevent affecting other sessions
1242
+ process: new Proxy(process, {
1243
+ get(target, prop, receiver) {
1244
+ if (prop === 'cwd')
1245
+ return () => self.sessionCwd || target.cwd();
1246
+ if (prop === 'exit')
1247
+ return () => { throw new Error('process.exit() is not allowed in the sandbox'); };
1248
+ if (prop === 'chdir')
1249
+ return () => { throw new Error('process.chdir() is not allowed in the sandbox, use a new session with a different cwd instead'); };
1250
+ return Reflect.get(target, prop, receiver);
1251
+ },
1252
+ }),
1253
+ };
1254
+ const vmContext = vm.createContext(vmContextObj);
1255
+ const autoReturnExpr = getAutoReturnExpression(code);
1256
+ const wrappedCode = autoReturnExpr !== null
1257
+ ? `(async () => { return await (${autoReturnExpr}) })()`
1258
+ : `(async () => { ${code} })()`;
1259
+ const hasExplicitReturn = autoReturnExpr !== null || /\breturn\b/.test(code);
1260
+ const result = await Promise.race([
1261
+ vm.runInContext(wrappedCode, vmContext, { timeout, displayErrors: true }),
1262
+ new Promise((_, reject) => setTimeout(() => reject(new CodeExecutionTimeoutError(timeout)), timeout)),
1263
+ ]);
1264
+ let responseText = formatConsoleLogs(consoleLogs);
1265
+ // Only show return value if user explicitly used return
1266
+ let structuredResult;
1267
+ if (hasExplicitReturn) {
1268
+ const resolvedResult = isPromise(result) ? await result : result;
1269
+ // Auto-returned Playwright handles (Response, Page, Browser, Request,
1270
+ // Frame, etc.) are silently skipped — they're programmatic references,
1271
+ // not useful display data. Users can `console.log(response)` or
1272
+ // return specific fields (`return response.url()`) to see values.
1273
+ // See issue #82.
1274
+ if (resolvedResult !== undefined && !isPlaywrightChannelOwner(resolvedResult)) {
1275
+ if (options.includeStructuredResult) {
1276
+ structuredResult = resolvedResult;
1277
+ }
1278
+ const formatted = typeof resolvedResult === 'string'
1279
+ ? resolvedResult
1280
+ : util.inspect(resolvedResult, {
1281
+ depth: 4,
1282
+ colors: false,
1283
+ maxArrayLength: 100,
1284
+ maxStringLength: 1000,
1285
+ breakLength: 80,
1286
+ });
1287
+ if (formatted.trim()) {
1288
+ responseText += `[return value] ${formatted}\n`;
1289
+ }
1290
+ }
1291
+ }
1292
+ responseText += this.flushWarningsForScope(warningScope);
1293
+ if (!responseText.trim()) {
1294
+ responseText = 'Code executed successfully (no output)';
1295
+ }
1296
+ const MAX_LENGTH = 10000;
1297
+ let finalText = responseText.trim();
1298
+ if (finalText.length > MAX_LENGTH) {
1299
+ finalText =
1300
+ finalText.slice(0, MAX_LENGTH) +
1301
+ `\n\n[Truncated to ${MAX_LENGTH} characters. Use search to find specific content]`;
1302
+ }
1303
+ const images = [
1304
+ ...screenshotCollector.map((s) => ({ data: s.base64, mimeType: s.mimeType })),
1305
+ ...resizedImageCollector,
1306
+ ];
1307
+ const screenshots = screenshotCollector.map((s) => ({
1308
+ path: s.path,
1309
+ base64: s.base64,
1310
+ mimeType: s.mimeType,
1311
+ snapshot: s.snapshot,
1312
+ labelCount: s.labelCount,
1313
+ }));
1314
+ return options.includeStructuredResult
1315
+ ? { text: finalText, images, screenshots, isError: false, structuredResult }
1316
+ : { text: finalText, images, screenshots, isError: false };
1317
+ }
1318
+ catch (error) {
1319
+ const errorStack = error.stack || error.message;
1320
+ const isTimeoutError = error instanceof CodeExecutionTimeoutError || error?.name === 'TimeoutError' || error?.name === 'AbortError';
1321
+ this.logger.error('Error in execute:', errorStack);
1322
+ const logsText = formatConsoleLogs(consoleLogs, 'Console output (before error)');
1323
+ const warningText = this.flushWarningsForScope(warningScope);
1324
+ // Cloud sessions: disconnection errors mean the VM expired or was destroyed.
1325
+ // Give a clear actionable message instead of a generic "call reset" hint.
1326
+ const isDisconnect = error instanceof Error && isDisconnectionError(error);
1327
+ const resetHint = (() => {
1328
+ if (isTimeoutError)
1329
+ return '';
1330
+ if (this.cloudSession && isDisconnect) {
1331
+ return `\n\n[Cloud browser expired or disconnected. Create a new session with: tabwright session new --browser cloud]`;
1332
+ }
1333
+ return '\n\n[HINT: If this is an internal Playwright error, page/browser closed, or connection issue, call reset to reconnect.]';
1334
+ })();
1335
+ // timeout stacks are internal noise (Promise.race / setTimeout); only show the message
1336
+ const errorText = isTimeoutError ? error.message : errorStack;
1337
+ return {
1338
+ text: `${logsText}${warningText}\nError executing code: ${errorText}${resetHint}`,
1339
+ images: [],
1340
+ screenshots: [],
1341
+ isError: true,
1342
+ };
1343
+ }
1344
+ }
1345
+ // When extension is connected but has no pages, auto-create unless PLAYWRITER_AUTO_ENABLE=false disables it.
1346
+ // In direct CDP mode, always create a page (no extension check needed).
1347
+ async ensurePageForContext(options) {
1348
+ const { context, timeout } = options;
1349
+ const pages = context.pages().filter((p) => !p.isClosed());
1350
+ if (pages.length > 0) {
1351
+ return pages[0];
1352
+ }
1353
+ // Direct CDP mode: always create a new page, no extension involved
1354
+ if (this.isDirectCdpMode()) {
1355
+ const page = await context.newPage();
1356
+ this.setupPageListeners(page);
1357
+ await page.waitForLoadState('domcontentloaded', { timeout }).catch(() => { });
1358
+ return page;
1359
+ }
1360
+ const extensionStatus = await this.checkExtensionStatus();
1361
+ if (!extensionStatus.connected) {
1362
+ throw new Error(EXTENSION_NOT_CONNECTED_ERROR);
1363
+ }
1364
+ if (!shouldAutoEnableTabwright()) {
1365
+ const waitTimeoutMs = Math.min(timeout, 1000);
1366
+ const startTime = Date.now();
1367
+ while (Date.now() - startTime < waitTimeoutMs) {
1368
+ const availablePages = context.pages().filter((p) => !p.isClosed());
1369
+ if (availablePages.length > 0) {
1370
+ return availablePages[0];
1371
+ }
1372
+ await new Promise((resolve) => setTimeout(resolve, 100));
1373
+ }
1374
+ throw new Error(NO_PAGES_AVAILABLE_ERROR);
1375
+ }
1376
+ const page = await context.newPage();
1377
+ this.setupPageListeners(page);
1378
+ const pageUrl = page.url();
1379
+ if (pageUrl === 'about:blank') {
1380
+ return page;
1381
+ }
1382
+ // Avoid burning the full timeout on about:blank-like pages.
1383
+ await page.waitForLoadState('domcontentloaded', { timeout }).catch(() => { });
1384
+ return page;
1385
+ }
1386
+ /** Get info about current connection state */
1387
+ getStatus() {
1388
+ return {
1389
+ connected: this.isConnected,
1390
+ pageUrl: this.page?.url() || null,
1391
+ pagesCount: this.context?.pages().length || 0,
1392
+ };
1393
+ }
1394
+ /** Get keys of user-defined state */
1395
+ getStateKeys() {
1396
+ return Object.keys(this.userState);
1397
+ }
1398
+ getSessionMetadata() {
1399
+ return this.sessionMetadata;
1400
+ }
1401
+ getSessionInfo({ id }) {
1402
+ const status = this.getStatus();
1403
+ return {
1404
+ id,
1405
+ stateKeys: this.getStateKeys(),
1406
+ extensionId: this.sessionMetadata.extensionId,
1407
+ browser: this.sessionMetadata.browser,
1408
+ profile: this.sessionMetadata.profile,
1409
+ cwd: this.sessionCwd,
1410
+ connected: status.connected,
1411
+ pageUrl: status.pageUrl,
1412
+ pagesCount: status.pagesCount,
1413
+ };
1414
+ }
1415
+ }
1416
+ /**
1417
+ * Session manager for multiple executors, keyed by session ID.
1418
+ */
1419
+ export class ExecutorManager {
1420
+ executors = new Map();
1421
+ cdpConfig;
1422
+ logger;
1423
+ constructor(options) {
1424
+ this.cdpConfig = options.cdpConfig;
1425
+ this.logger = options.logger || { log: console.log, error: console.error };
1426
+ }
1427
+ getExecutor(options) {
1428
+ const { sessionId, cwd, sessionMetadata } = options;
1429
+ let executor = this.executors.get(sessionId);
1430
+ if (!executor) {
1431
+ const cdpConfig = (() => {
1432
+ // Per-session override takes priority (used for direct CDP sessions)
1433
+ if (options.cdpConfig) {
1434
+ return options.cdpConfig;
1435
+ }
1436
+ const baseConfig = typeof this.cdpConfig === 'function' ? this.cdpConfig(sessionId) : this.cdpConfig;
1437
+ if (sessionMetadata?.extensionId) {
1438
+ return { ...baseConfig, extensionId: sessionMetadata.extensionId };
1439
+ }
1440
+ return baseConfig;
1441
+ })();
1442
+ executor = new PlaywrightExecutor({
1443
+ cdpConfig,
1444
+ sessionMetadata,
1445
+ logger: this.logger,
1446
+ cwd,
1447
+ cloudSession: options.cloudSession,
1448
+ });
1449
+ this.executors.set(sessionId, executor);
1450
+ }
1451
+ return executor;
1452
+ }
1453
+ deleteExecutor(sessionId) {
1454
+ return this.executors.delete(sessionId);
1455
+ }
1456
+ getSession(sessionId) {
1457
+ return this.executors.get(sessionId) || null;
1458
+ }
1459
+ listSessions() {
1460
+ return [...this.executors.entries()].map(([id, executor]) => {
1461
+ return executor.getSessionInfo({ id });
1462
+ });
1463
+ }
1464
+ }
1465
+ //# sourceMappingURL=executor.js.map