tabwright 1.0.0 → 1.0.1

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