tabwright 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (599) hide show
  1. package/LICENSE +21 -0
  2. package/bin.js +4 -0
  3. package/dist/a11y-client.js +204 -0
  4. package/dist/agent-skills/tabwright/SKILL.md +119 -0
  5. package/dist/aria-snapshot.d.ts +262 -0
  6. package/dist/aria-snapshot.d.ts.map +1 -0
  7. package/dist/aria-snapshot.js +1256 -0
  8. package/dist/aria-snapshot.js.map +1 -0
  9. package/dist/aria-snapshot.test.d.ts +2 -0
  10. package/dist/aria-snapshot.test.d.ts.map +1 -0
  11. package/dist/aria-snapshot.test.js +270 -0
  12. package/dist/aria-snapshot.test.js.map +1 -0
  13. package/dist/aria-snapshot.unit.test.d.ts +2 -0
  14. package/dist/aria-snapshot.unit.test.d.ts.map +1 -0
  15. package/dist/aria-snapshot.unit.test.js +542 -0
  16. package/dist/aria-snapshot.unit.test.js.map +1 -0
  17. package/dist/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.d.ts +5 -0
  18. package/dist/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.d.ts.map +1 -0
  19. package/dist/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.js +5 -0
  20. package/dist/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.js.map +1 -0
  21. package/dist/bippy.js +894 -0
  22. package/dist/browser-config.d.ts +13 -0
  23. package/dist/browser-config.d.ts.map +1 -0
  24. package/dist/browser-config.js +126 -0
  25. package/dist/browser-config.js.map +1 -0
  26. package/dist/browser-install.d.ts +16 -0
  27. package/dist/browser-install.d.ts.map +1 -0
  28. package/dist/browser-install.js +238 -0
  29. package/dist/browser-install.js.map +1 -0
  30. package/dist/browser-launch.d.ts +17 -0
  31. package/dist/browser-launch.d.ts.map +1 -0
  32. package/dist/browser-launch.js +44 -0
  33. package/dist/browser-launch.js.map +1 -0
  34. package/dist/builtin-capabilities.d.ts +28 -0
  35. package/dist/builtin-capabilities.d.ts.map +1 -0
  36. package/dist/builtin-capabilities.js +2673 -0
  37. package/dist/builtin-capabilities.js.map +1 -0
  38. package/dist/capability-agent-skill.d.ts +60 -0
  39. package/dist/capability-agent-skill.d.ts.map +1 -0
  40. package/dist/capability-agent-skill.js +225 -0
  41. package/dist/capability-agent-skill.js.map +1 -0
  42. package/dist/capability-auth-state.d.ts +34 -0
  43. package/dist/capability-auth-state.d.ts.map +1 -0
  44. package/dist/capability-auth-state.js +157 -0
  45. package/dist/capability-auth-state.js.map +1 -0
  46. package/dist/capability-auth.d.ts +36 -0
  47. package/dist/capability-auth.d.ts.map +1 -0
  48. package/dist/capability-auth.js +198 -0
  49. package/dist/capability-auth.js.map +1 -0
  50. package/dist/capability-cli-confirmation.test.d.ts +2 -0
  51. package/dist/capability-cli-confirmation.test.d.ts.map +1 -0
  52. package/dist/capability-cli-confirmation.test.js +79 -0
  53. package/dist/capability-cli-confirmation.test.js.map +1 -0
  54. package/dist/capability-options.d.ts +20 -0
  55. package/dist/capability-options.d.ts.map +1 -0
  56. package/dist/capability-options.js +31 -0
  57. package/dist/capability-options.js.map +1 -0
  58. package/dist/capability-options.test.d.ts +2 -0
  59. package/dist/capability-options.test.d.ts.map +1 -0
  60. package/dist/capability-options.test.js +81 -0
  61. package/dist/capability-options.test.js.map +1 -0
  62. package/dist/capability-package.d.ts +27 -0
  63. package/dist/capability-package.d.ts.map +1 -0
  64. package/dist/capability-package.js +444 -0
  65. package/dist/capability-package.js.map +1 -0
  66. package/dist/capability-package.test.d.ts +2 -0
  67. package/dist/capability-package.test.d.ts.map +1 -0
  68. package/dist/capability-package.test.js +179 -0
  69. package/dist/capability-package.test.js.map +1 -0
  70. package/dist/capability-registry.d.ts +262 -0
  71. package/dist/capability-registry.d.ts.map +1 -0
  72. package/dist/capability-registry.js +931 -0
  73. package/dist/capability-registry.js.map +1 -0
  74. package/dist/capability-registry.test.d.ts +2 -0
  75. package/dist/capability-registry.test.d.ts.map +1 -0
  76. package/dist/capability-registry.test.js +1683 -0
  77. package/dist/capability-registry.test.js.map +1 -0
  78. package/dist/capability-runner.d.ts +99 -0
  79. package/dist/capability-runner.d.ts.map +1 -0
  80. package/dist/capability-runner.js +608 -0
  81. package/dist/capability-runner.js.map +1 -0
  82. package/dist/capability-search-relevance.test.d.ts +2 -0
  83. package/dist/capability-search-relevance.test.d.ts.map +1 -0
  84. package/dist/capability-search-relevance.test.js +67 -0
  85. package/dist/capability-search-relevance.test.js.map +1 -0
  86. package/dist/capability-studio.d.ts +11 -0
  87. package/dist/capability-studio.d.ts.map +1 -0
  88. package/dist/capability-studio.js +593 -0
  89. package/dist/capability-studio.js.map +1 -0
  90. package/dist/capability-studio.test.d.ts +2 -0
  91. package/dist/capability-studio.test.d.ts.map +1 -0
  92. package/dist/capability-studio.test.js +41 -0
  93. package/dist/capability-studio.test.js.map +1 -0
  94. package/dist/cdp-log.d.ts +19 -0
  95. package/dist/cdp-log.d.ts.map +1 -0
  96. package/dist/cdp-log.js +83 -0
  97. package/dist/cdp-log.js.map +1 -0
  98. package/dist/cdp-log.test.d.ts +2 -0
  99. package/dist/cdp-log.test.d.ts.map +1 -0
  100. package/dist/cdp-log.test.js +109 -0
  101. package/dist/cdp-log.test.js.map +1 -0
  102. package/dist/cdp-relay.d.ts +19 -0
  103. package/dist/cdp-relay.d.ts.map +1 -0
  104. package/dist/cdp-relay.js +2375 -0
  105. package/dist/cdp-relay.js.map +1 -0
  106. package/dist/cdp-session.d.ts +37 -0
  107. package/dist/cdp-session.d.ts.map +1 -0
  108. package/dist/cdp-session.js +36 -0
  109. package/dist/cdp-session.js.map +1 -0
  110. package/dist/cdp-types.d.ts +65 -0
  111. package/dist/cdp-types.d.ts.map +1 -0
  112. package/dist/cdp-types.js +91 -0
  113. package/dist/cdp-types.js.map +1 -0
  114. package/dist/channel-owner-inspect.test.d.ts +2 -0
  115. package/dist/channel-owner-inspect.test.d.ts.map +1 -0
  116. package/dist/channel-owner-inspect.test.js +75 -0
  117. package/dist/channel-owner-inspect.test.js.map +1 -0
  118. package/dist/chrome-discovery.d.ts +65 -0
  119. package/dist/chrome-discovery.d.ts.map +1 -0
  120. package/dist/chrome-discovery.js +173 -0
  121. package/dist/chrome-discovery.js.map +1 -0
  122. package/dist/chrome-discovery.test.d.ts +2 -0
  123. package/dist/chrome-discovery.test.d.ts.map +1 -0
  124. package/dist/chrome-discovery.test.js +41 -0
  125. package/dist/chrome-discovery.test.js.map +1 -0
  126. package/dist/clean-html.d.ts +11 -0
  127. package/dist/clean-html.d.ts.map +1 -0
  128. package/dist/clean-html.js +106 -0
  129. package/dist/clean-html.js.map +1 -0
  130. package/dist/cli-help.test.d.ts +2 -0
  131. package/dist/cli-help.test.d.ts.map +1 -0
  132. package/dist/cli-help.test.js +98 -0
  133. package/dist/cli-help.test.js.map +1 -0
  134. package/dist/cli.d.ts +3 -0
  135. package/dist/cli.d.ts.map +1 -0
  136. package/dist/cli.js +2709 -0
  137. package/dist/cli.js.map +1 -0
  138. package/dist/cloud-client.d.ts +56 -0
  139. package/dist/cloud-client.d.ts.map +1 -0
  140. package/dist/cloud-client.js +127 -0
  141. package/dist/cloud-client.js.map +1 -0
  142. package/dist/create-logger.d.ts +9 -0
  143. package/dist/create-logger.d.ts.map +1 -0
  144. package/dist/create-logger.js +27 -0
  145. package/dist/create-logger.js.map +1 -0
  146. package/dist/debugger-api.md +458 -0
  147. package/dist/debugger-examples-types.d.ts +24 -0
  148. package/dist/debugger-examples-types.d.ts.map +1 -0
  149. package/dist/debugger-examples-types.js +2 -0
  150. package/dist/debugger-examples-types.js.map +1 -0
  151. package/dist/debugger-examples.d.ts +6 -0
  152. package/dist/debugger-examples.d.ts.map +1 -0
  153. package/dist/debugger-examples.js +53 -0
  154. package/dist/debugger-examples.js.map +1 -0
  155. package/dist/debugger.d.ts +381 -0
  156. package/dist/debugger.d.ts.map +1 -0
  157. package/dist/debugger.js +630 -0
  158. package/dist/debugger.js.map +1 -0
  159. package/dist/diff-utils.d.ts +20 -0
  160. package/dist/diff-utils.d.ts.map +1 -0
  161. package/dist/diff-utils.js +50 -0
  162. package/dist/diff-utils.js.map +1 -0
  163. package/dist/diff-utils.test.d.ts +2 -0
  164. package/dist/diff-utils.test.d.ts.map +1 -0
  165. package/dist/diff-utils.test.js +137 -0
  166. package/dist/diff-utils.test.js.map +1 -0
  167. package/dist/doctor.d.ts +44 -0
  168. package/dist/doctor.d.ts.map +1 -0
  169. package/dist/doctor.js +246 -0
  170. package/dist/doctor.js.map +1 -0
  171. package/dist/doctor.test.d.ts +2 -0
  172. package/dist/doctor.test.d.ts.map +1 -0
  173. package/dist/doctor.test.js +208 -0
  174. package/dist/doctor.test.js.map +1 -0
  175. package/dist/editor-api.md +374 -0
  176. package/dist/editor-examples.d.ts +11 -0
  177. package/dist/editor-examples.d.ts.map +1 -0
  178. package/dist/editor-examples.js +124 -0
  179. package/dist/editor-examples.js.map +1 -0
  180. package/dist/editor.d.ts +203 -0
  181. package/dist/editor.d.ts.map +1 -0
  182. package/dist/editor.js +363 -0
  183. package/dist/editor.js.map +1 -0
  184. package/dist/env-compat.d.ts +2 -0
  185. package/dist/env-compat.d.ts.map +1 -0
  186. package/dist/env-compat.js +27 -0
  187. package/dist/env-compat.js.map +1 -0
  188. package/dist/executor.d.ts +229 -0
  189. package/dist/executor.d.ts.map +1 -0
  190. package/dist/executor.js +1465 -0
  191. package/dist/executor.js.map +1 -0
  192. package/dist/executor.unit.test.d.ts +2 -0
  193. package/dist/executor.unit.test.d.ts.map +1 -0
  194. package/dist/executor.unit.test.js +143 -0
  195. package/dist/executor.unit.test.js.map +1 -0
  196. package/dist/extension/_locales/en/messages.json +278 -0
  197. package/dist/extension/_locales/zh_CN/messages.json +256 -0
  198. package/dist/extension/assets/options-CYH96Lcx.css +1349 -0
  199. package/dist/extension/background.js +3994 -0
  200. package/dist/extension/icons/SolarCursorSquareBold.png +0 -0
  201. package/dist/extension/icons/icon-black-128.png +0 -0
  202. package/dist/extension/icons/icon-black-16.png +0 -0
  203. package/dist/extension/icons/icon-black-32.png +0 -0
  204. package/dist/extension/icons/icon-black-48.png +0 -0
  205. package/dist/extension/icons/icon-gray-128.png +0 -0
  206. package/dist/extension/icons/icon-gray-16.png +0 -0
  207. package/dist/extension/icons/icon-gray-32.png +0 -0
  208. package/dist/extension/icons/icon-gray-48.png +0 -0
  209. package/dist/extension/icons/icon-green-128.png +0 -0
  210. package/dist/extension/icons/icon-green-16.png +0 -0
  211. package/dist/extension/icons/icon-green-32.png +0 -0
  212. package/dist/extension/icons/icon-green-48.png +0 -0
  213. package/dist/extension/icons/tabwright-icon-black.png +0 -0
  214. package/dist/extension/icons/tabwright-icon-gray-disabled.png +0 -0
  215. package/dist/extension/icons/tabwright-icon-gray-disabled.svg +1 -0
  216. package/dist/extension/manifest.json +54 -0
  217. package/dist/extension/options.js +16145 -0
  218. package/dist/extension/rrweb-recorder.js +12952 -0
  219. package/dist/extension/src/options.html +165 -0
  220. package/dist/extension/src/prism-bash.min.js +1 -0
  221. package/dist/extension/src/prism.min.js +1 -0
  222. package/dist/extension/src/welcome.html +396 -0
  223. package/dist/extension-connection.test.d.ts +2 -0
  224. package/dist/extension-connection.test.d.ts.map +1 -0
  225. package/dist/extension-connection.test.js +708 -0
  226. package/dist/extension-connection.test.js.map +1 -0
  227. package/dist/extension-icon-warning.test.d.ts +2 -0
  228. package/dist/extension-icon-warning.test.d.ts.map +1 -0
  229. package/dist/extension-icon-warning.test.js +33 -0
  230. package/dist/extension-icon-warning.test.js.map +1 -0
  231. package/dist/ghost-browser.d.ts +62 -0
  232. package/dist/ghost-browser.d.ts.map +1 -0
  233. package/dist/ghost-browser.js +107 -0
  234. package/dist/ghost-browser.js.map +1 -0
  235. package/dist/ghost-cursor-client.js +374 -0
  236. package/dist/ghost-cursor-controller.d.ts +46 -0
  237. package/dist/ghost-cursor-controller.d.ts.map +1 -0
  238. package/dist/ghost-cursor-controller.js +98 -0
  239. package/dist/ghost-cursor-controller.js.map +1 -0
  240. package/dist/ghost-cursor.d.ts +27 -0
  241. package/dist/ghost-cursor.d.ts.map +1 -0
  242. package/dist/ghost-cursor.js +79 -0
  243. package/dist/ghost-cursor.js.map +1 -0
  244. package/dist/htmlrewrite.d.ts +8 -0
  245. package/dist/htmlrewrite.d.ts.map +1 -0
  246. package/dist/htmlrewrite.js +339 -0
  247. package/dist/htmlrewrite.js.map +1 -0
  248. package/dist/htmlrewrite.test.d.ts +2 -0
  249. package/dist/htmlrewrite.test.d.ts.map +1 -0
  250. package/dist/htmlrewrite.test.js +7975 -0
  251. package/dist/htmlrewrite.test.js.map +1 -0
  252. package/dist/index.d.ts +20 -0
  253. package/dist/index.d.ts.map +1 -0
  254. package/dist/index.js +11 -0
  255. package/dist/index.js.map +1 -0
  256. package/dist/kill-port.d.ts +29 -0
  257. package/dist/kill-port.d.ts.map +1 -0
  258. package/dist/kill-port.js +190 -0
  259. package/dist/kill-port.js.map +1 -0
  260. package/dist/kitty-graphics.d.ts +22 -0
  261. package/dist/kitty-graphics.d.ts.map +1 -0
  262. package/dist/kitty-graphics.js +71 -0
  263. package/dist/kitty-graphics.js.map +1 -0
  264. package/dist/kitty-graphics.test.d.ts +2 -0
  265. package/dist/kitty-graphics.test.d.ts.map +1 -0
  266. package/dist/kitty-graphics.test.js +125 -0
  267. package/dist/kitty-graphics.test.js.map +1 -0
  268. package/dist/locator-selector.test.d.ts +2 -0
  269. package/dist/locator-selector.test.d.ts.map +1 -0
  270. package/dist/locator-selector.test.js +98 -0
  271. package/dist/locator-selector.test.js.map +1 -0
  272. package/dist/mcp-client.d.ts +20 -0
  273. package/dist/mcp-client.d.ts.map +1 -0
  274. package/dist/mcp-client.js +56 -0
  275. package/dist/mcp-client.js.map +1 -0
  276. package/dist/mcp.d.ts +6 -0
  277. package/dist/mcp.d.ts.map +1 -0
  278. package/dist/mcp.js +511 -0
  279. package/dist/mcp.js.map +1 -0
  280. package/dist/on-mouse-action.test.d.ts +2 -0
  281. package/dist/on-mouse-action.test.d.ts.map +1 -0
  282. package/dist/on-mouse-action.test.js +180 -0
  283. package/dist/on-mouse-action.test.js.map +1 -0
  284. package/dist/options-page.test.d.ts +2 -0
  285. package/dist/options-page.test.d.ts.map +1 -0
  286. package/dist/options-page.test.js +773 -0
  287. package/dist/options-page.test.js.map +1 -0
  288. package/dist/package-paths.d.ts +4 -0
  289. package/dist/package-paths.d.ts.map +1 -0
  290. package/dist/package-paths.js +30 -0
  291. package/dist/package-paths.js.map +1 -0
  292. package/dist/page-markdown.d.ts +40 -0
  293. package/dist/page-markdown.d.ts.map +1 -0
  294. package/dist/page-markdown.js +181 -0
  295. package/dist/page-markdown.js.map +1 -0
  296. package/dist/performance-examples.d.ts +5 -0
  297. package/dist/performance-examples.d.ts.map +1 -0
  298. package/dist/performance-examples.js +112 -0
  299. package/dist/performance-examples.js.map +1 -0
  300. package/dist/performance-profiling.md +417 -0
  301. package/dist/playwright-import.d.ts +19 -0
  302. package/dist/playwright-import.d.ts.map +1 -0
  303. package/dist/playwright-import.js +40 -0
  304. package/dist/playwright-import.js.map +1 -0
  305. package/dist/popup-relocation.test.d.ts +7 -0
  306. package/dist/popup-relocation.test.d.ts.map +1 -0
  307. package/dist/popup-relocation.test.js +139 -0
  308. package/dist/popup-relocation.test.js.map +1 -0
  309. package/dist/product-paths.d.ts +9 -0
  310. package/dist/product-paths.d.ts.map +1 -0
  311. package/dist/product-paths.js +24 -0
  312. package/dist/product-paths.js.map +1 -0
  313. package/dist/product-paths.test.d.ts +2 -0
  314. package/dist/product-paths.test.d.ts.map +1 -0
  315. package/dist/product-paths.test.js +39 -0
  316. package/dist/product-paths.test.js.map +1 -0
  317. package/dist/prompt.md +1004 -0
  318. package/dist/protocol.d.ts +248 -0
  319. package/dist/protocol.d.ts.map +1 -0
  320. package/dist/protocol.js +59 -0
  321. package/dist/protocol.js.map +1 -0
  322. package/dist/protocol.test.d.ts +2 -0
  323. package/dist/protocol.test.d.ts.map +1 -0
  324. package/dist/protocol.test.js +31 -0
  325. package/dist/protocol.test.js.map +1 -0
  326. package/dist/react-source.d.ts +57 -0
  327. package/dist/react-source.d.ts.map +1 -0
  328. package/dist/react-source.js +254 -0
  329. package/dist/react-source.js.map +1 -0
  330. package/dist/readability.js +1674 -0
  331. package/dist/relay-client.d.ts +104 -0
  332. package/dist/relay-client.d.ts.map +1 -0
  333. package/dist/relay-client.js +426 -0
  334. package/dist/relay-client.js.map +1 -0
  335. package/dist/relay-client.test.d.ts +2 -0
  336. package/dist/relay-client.test.d.ts.map +1 -0
  337. package/dist/relay-client.test.js +305 -0
  338. package/dist/relay-client.test.js.map +1 -0
  339. package/dist/relay-core.test.d.ts +2 -0
  340. package/dist/relay-core.test.d.ts.map +1 -0
  341. package/dist/relay-core.test.js +1427 -0
  342. package/dist/relay-core.test.js.map +1 -0
  343. package/dist/relay-navigation.test.d.ts +2 -0
  344. package/dist/relay-navigation.test.d.ts.map +1 -0
  345. package/dist/relay-navigation.test.js +661 -0
  346. package/dist/relay-navigation.test.js.map +1 -0
  347. package/dist/relay-session.test.d.ts +2 -0
  348. package/dist/relay-session.test.d.ts.map +1 -0
  349. package/dist/relay-session.test.js +1121 -0
  350. package/dist/relay-session.test.js.map +1 -0
  351. package/dist/relay-state.d.ts +178 -0
  352. package/dist/relay-state.d.ts.map +1 -0
  353. package/dist/relay-state.js +346 -0
  354. package/dist/relay-state.js.map +1 -0
  355. package/dist/relay-state.test.d.ts +2 -0
  356. package/dist/relay-state.test.d.ts.map +1 -0
  357. package/dist/relay-state.test.js +594 -0
  358. package/dist/relay-state.test.js.map +1 -0
  359. package/dist/replay-ai-index.d.ts +99 -0
  360. package/dist/replay-ai-index.d.ts.map +1 -0
  361. package/dist/replay-ai-index.js +677 -0
  362. package/dist/replay-ai-index.js.map +1 -0
  363. package/dist/replay-ai-index.test.d.ts +2 -0
  364. package/dist/replay-ai-index.test.d.ts.map +1 -0
  365. package/dist/replay-ai-index.test.js +255 -0
  366. package/dist/replay-ai-index.test.js.map +1 -0
  367. package/dist/replay-cli.test.d.ts +2 -0
  368. package/dist/replay-cli.test.d.ts.map +1 -0
  369. package/dist/replay-cli.test.js +275 -0
  370. package/dist/replay-cli.test.js.map +1 -0
  371. package/dist/replay-eval.d.ts +44 -0
  372. package/dist/replay-eval.d.ts.map +1 -0
  373. package/dist/replay-eval.js +902 -0
  374. package/dist/replay-eval.js.map +1 -0
  375. package/dist/replay-handoff.d.ts +29 -0
  376. package/dist/replay-handoff.d.ts.map +1 -0
  377. package/dist/replay-handoff.js +83 -0
  378. package/dist/replay-handoff.js.map +1 -0
  379. package/dist/replay-handoff.test.d.ts +2 -0
  380. package/dist/replay-handoff.test.d.ts.map +1 -0
  381. package/dist/replay-handoff.test.js +123 -0
  382. package/dist/replay-handoff.test.js.map +1 -0
  383. package/dist/replay-workflow-compiler.d.ts +44 -0
  384. package/dist/replay-workflow-compiler.d.ts.map +1 -0
  385. package/dist/replay-workflow-compiler.js +601 -0
  386. package/dist/replay-workflow-compiler.js.map +1 -0
  387. package/dist/replay-workflow-compiler.test.d.ts +2 -0
  388. package/dist/replay-workflow-compiler.test.d.ts.map +1 -0
  389. package/dist/replay-workflow-compiler.test.js +223 -0
  390. package/dist/replay-workflow-compiler.test.js.map +1 -0
  391. package/dist/rrweb-recording-relay.d.ts +56 -0
  392. package/dist/rrweb-recording-relay.d.ts.map +1 -0
  393. package/dist/rrweb-recording-relay.js +299 -0
  394. package/dist/rrweb-recording-relay.js.map +1 -0
  395. package/dist/rrweb-recording-relay.test.d.ts +2 -0
  396. package/dist/rrweb-recording-relay.test.d.ts.map +1 -0
  397. package/dist/rrweb-recording-relay.test.js +85 -0
  398. package/dist/rrweb-recording-relay.test.js.map +1 -0
  399. package/dist/rrweb-recording.d.ts +104 -0
  400. package/dist/rrweb-recording.d.ts.map +1 -0
  401. package/dist/rrweb-recording.js +180 -0
  402. package/dist/rrweb-recording.js.map +1 -0
  403. package/dist/scoped-fs.d.ts +95 -0
  404. package/dist/scoped-fs.d.ts.map +1 -0
  405. package/dist/scoped-fs.js +358 -0
  406. package/dist/scoped-fs.js.map +1 -0
  407. package/dist/scoped-fs.test.d.ts +5 -0
  408. package/dist/scoped-fs.test.d.ts.map +1 -0
  409. package/dist/scoped-fs.test.js +73 -0
  410. package/dist/scoped-fs.test.js.map +1 -0
  411. package/dist/selector-generator.js +7378 -0
  412. package/dist/skill-stub.test.d.ts +2 -0
  413. package/dist/skill-stub.test.d.ts.map +1 -0
  414. package/dist/skill-stub.test.js +22 -0
  415. package/dist/skill-stub.test.js.map +1 -0
  416. package/dist/snapshot-tools.test.d.ts +2 -0
  417. package/dist/snapshot-tools.test.d.ts.map +1 -0
  418. package/dist/snapshot-tools.test.js +856 -0
  419. package/dist/snapshot-tools.test.js.map +1 -0
  420. package/dist/start-relay-server.d.ts +6 -0
  421. package/dist/start-relay-server.d.ts.map +1 -0
  422. package/dist/start-relay-server.js +57 -0
  423. package/dist/start-relay-server.js.map +1 -0
  424. package/dist/styles-api.md +124 -0
  425. package/dist/styles-examples.d.ts +8 -0
  426. package/dist/styles-examples.d.ts.map +1 -0
  427. package/dist/styles-examples.js +64 -0
  428. package/dist/styles-examples.js.map +1 -0
  429. package/dist/styles.d.ts +27 -0
  430. package/dist/styles.d.ts.map +1 -0
  431. package/dist/styles.js +231 -0
  432. package/dist/styles.js.map +1 -0
  433. package/dist/tabwright-agent-skill.d.ts +30 -0
  434. package/dist/tabwright-agent-skill.d.ts.map +1 -0
  435. package/dist/tabwright-agent-skill.js +74 -0
  436. package/dist/tabwright-agent-skill.js.map +1 -0
  437. package/dist/tabwright-agent-skill.test.d.ts +2 -0
  438. package/dist/tabwright-agent-skill.test.d.ts.map +1 -0
  439. package/dist/tabwright-agent-skill.test.js +51 -0
  440. package/dist/tabwright-agent-skill.test.js.map +1 -0
  441. package/dist/test-declarations.d.ts +13 -0
  442. package/dist/test-declarations.d.ts.map +1 -0
  443. package/dist/test-declarations.js +2 -0
  444. package/dist/test-declarations.js.map +1 -0
  445. package/dist/test-utils.d.ts +69 -0
  446. package/dist/test-utils.d.ts.map +1 -0
  447. package/dist/test-utils.js +448 -0
  448. package/dist/test-utils.js.map +1 -0
  449. package/dist/test-utils.test.d.ts +2 -0
  450. package/dist/test-utils.test.d.ts.map +1 -0
  451. package/dist/test-utils.test.js +55 -0
  452. package/dist/test-utils.test.js.map +1 -0
  453. package/dist/utils.d.ts +26 -0
  454. package/dist/utils.d.ts.map +1 -0
  455. package/dist/utils.js +60 -0
  456. package/dist/utils.js.map +1 -0
  457. package/dist/wait-for-page-load.d.ts +16 -0
  458. package/dist/wait-for-page-load.d.ts.map +1 -0
  459. package/dist/wait-for-page-load.js +146 -0
  460. package/dist/wait-for-page-load.js.map +1 -0
  461. package/dist/workflow-capability.d.ts +72 -0
  462. package/dist/workflow-capability.d.ts.map +1 -0
  463. package/dist/workflow-capability.js +316 -0
  464. package/dist/workflow-capability.js.map +1 -0
  465. package/dist/workflow-script-flow.test.d.ts +2 -0
  466. package/dist/workflow-script-flow.test.d.ts.map +1 -0
  467. package/dist/workflow-script-flow.test.js +285 -0
  468. package/dist/workflow-script-flow.test.js.map +1 -0
  469. package/package.json +80 -0
  470. package/src/__snapshots__/x.com.processed.html +1003 -0
  471. package/src/__snapshots__/x.com.processed.withStyles.html +2720 -0
  472. package/src/a11y-client.ts +241 -0
  473. package/src/agent-skills/conan-config/SKILL.md +141 -0
  474. package/src/agent-skills/conan-config/agents/openai.yaml +4 -0
  475. package/src/aria-snapshot.test.ts +311 -0
  476. package/src/aria-snapshot.ts +1681 -0
  477. package/src/aria-snapshot.unit.test.ts +615 -0
  478. package/src/aria-snapshots/github-interactive.txt +193 -0
  479. package/src/aria-snapshots/github-raw.txt +309 -0
  480. package/src/aria-snapshots/hackernews-interactive.txt +477 -0
  481. package/src/aria-snapshots/hackernews-raw.txt +428 -0
  482. package/src/aria-snapshots/prosemirror-interactive.txt +66 -0
  483. package/src/aria-snapshots/prosemirror-raw.txt +145 -0
  484. package/src/assets/aria-labels-hacker-news.png +0 -0
  485. package/src/assets/aria-labels-old-reddit.png +0 -0
  486. package/src/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.ts +5 -0
  487. package/src/assets/cursors/screen-studio/pointer-macos-tahoe.svg +18 -0
  488. package/src/assets/x.com.html +32946 -0
  489. package/src/browser-config.ts +179 -0
  490. package/src/browser-install.ts +284 -0
  491. package/src/browser-launch.ts +75 -0
  492. package/src/builtin-capabilities.ts +2765 -0
  493. package/src/capability-agent-skill.ts +305 -0
  494. package/src/capability-auth-state.ts +197 -0
  495. package/src/capability-auth.ts +249 -0
  496. package/src/capability-cli-confirmation.test.ts +90 -0
  497. package/src/capability-options.test.ts +89 -0
  498. package/src/capability-options.ts +57 -0
  499. package/src/capability-package.test.ts +204 -0
  500. package/src/capability-package.ts +555 -0
  501. package/src/capability-registry.test.ts +1905 -0
  502. package/src/capability-registry.ts +1234 -0
  503. package/src/capability-runner.ts +801 -0
  504. package/src/capability-search-relevance.test.ts +73 -0
  505. package/src/capability-studio.test.ts +45 -0
  506. package/src/capability-studio.ts +641 -0
  507. package/src/cdp-log.test.ts +131 -0
  508. package/src/cdp-log.ts +112 -0
  509. package/src/cdp-relay.ts +2944 -0
  510. package/src/cdp-session.ts +77 -0
  511. package/src/cdp-types.ts +158 -0
  512. package/src/channel-owner-inspect.test.ts +94 -0
  513. package/src/chrome-discovery.test.ts +50 -0
  514. package/src/chrome-discovery.ts +220 -0
  515. package/src/clean-html.ts +141 -0
  516. package/src/cli-help.test.ts +123 -0
  517. package/src/cli.ts +3181 -0
  518. package/src/cloud-client.ts +179 -0
  519. package/src/create-logger.ts +38 -0
  520. package/src/debugger-examples-types.ts +16 -0
  521. package/src/debugger-examples.ts +66 -0
  522. package/src/debugger.ts +708 -0
  523. package/src/diff-utils.test.ts +152 -0
  524. package/src/diff-utils.ts +70 -0
  525. package/src/doctor.test.ts +244 -0
  526. package/src/doctor.ts +321 -0
  527. package/src/editor-examples.ts +158 -0
  528. package/src/editor.ts +426 -0
  529. package/src/env-compat.ts +28 -0
  530. package/src/executor.ts +1824 -0
  531. package/src/executor.unit.test.ts +168 -0
  532. package/src/extension-connection.test.ts +855 -0
  533. package/src/extension-icon-warning.test.ts +45 -0
  534. package/src/ghost-browser.ts +149 -0
  535. package/src/ghost-cursor-client.ts +494 -0
  536. package/src/ghost-cursor-controller.ts +129 -0
  537. package/src/ghost-cursor.ts +123 -0
  538. package/src/htmlrewrite.test.ts +8008 -0
  539. package/src/htmlrewrite.ts +390 -0
  540. package/src/index.ts +53 -0
  541. package/src/kill-port.ts +219 -0
  542. package/src/kitty-graphics.test.ts +139 -0
  543. package/src/kitty-graphics.ts +79 -0
  544. package/src/locator-selector.test.ts +119 -0
  545. package/src/mcp-client.ts +78 -0
  546. package/src/mcp.ts +629 -0
  547. package/src/on-mouse-action.test.ts +226 -0
  548. package/src/options-page.test.ts +839 -0
  549. package/src/package-paths.ts +38 -0
  550. package/src/page-markdown.ts +240 -0
  551. package/src/performance-examples.ts +186 -0
  552. package/src/playwright-import.ts +59 -0
  553. package/src/popup-relocation.test.ts +163 -0
  554. package/src/product-paths.test.ts +47 -0
  555. package/src/product-paths.ts +28 -0
  556. package/src/protocol.test.ts +52 -0
  557. package/src/protocol.ts +351 -0
  558. package/src/react-source.ts +379 -0
  559. package/src/relay-client.test.ts +375 -0
  560. package/src/relay-client.ts +572 -0
  561. package/src/relay-core.test.ts +1603 -0
  562. package/src/relay-navigation.test.ts +790 -0
  563. package/src/relay-session.test.ts +1375 -0
  564. package/src/relay-state.test.ts +729 -0
  565. package/src/relay-state.ts +561 -0
  566. package/src/replay-ai-index.test.ts +278 -0
  567. package/src/replay-ai-index.ts +872 -0
  568. package/src/replay-cli.test.ts +325 -0
  569. package/src/replay-eval.ts +1039 -0
  570. package/src/replay-handoff.test.ts +163 -0
  571. package/src/replay-handoff.ts +109 -0
  572. package/src/replay-workflow-compiler.test.ts +250 -0
  573. package/src/replay-workflow-compiler.ts +700 -0
  574. package/src/resource.md +409 -0
  575. package/src/rrweb-recording-relay.test.ts +99 -0
  576. package/src/rrweb-recording-relay.ts +400 -0
  577. package/src/rrweb-recording.ts +322 -0
  578. package/src/scoped-fs.test.ts +80 -0
  579. package/src/scoped-fs.ts +420 -0
  580. package/src/skill-stub.test.ts +24 -0
  581. package/src/skill.md +1413 -0
  582. package/src/snapshot-tools.test.ts +1017 -0
  583. package/src/snapshots/hacker-news-accessibility-full.md +81 -0
  584. package/src/snapshots/hacker-news-accessibility-interactive.md +43 -0
  585. package/src/snapshots/page-markdown-output.txt +16 -0
  586. package/src/snapshots/shadcn-ui-accessibility-full.md +201 -0
  587. package/src/snapshots/shadcn-ui-accessibility-interactive.md +109 -0
  588. package/src/start-relay-server.ts +69 -0
  589. package/src/styles-examples.ts +84 -0
  590. package/src/styles.ts +343 -0
  591. package/src/tabwright-agent-skill.test.ts +61 -0
  592. package/src/tabwright-agent-skill.ts +105 -0
  593. package/src/test-declarations.ts +13 -0
  594. package/src/test-utils.test.ts +70 -0
  595. package/src/test-utils.ts +566 -0
  596. package/src/utils.ts +78 -0
  597. package/src/wait-for-page-load.ts +198 -0
  598. package/src/workflow-capability.ts +417 -0
  599. package/src/workflow-script-flow.test.ts +388 -0
@@ -0,0 +1,1256 @@
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { Sema } from 'async-sema';
6
+ import { getCDPSessionForPage } from './cdp-session.js';
7
+ // Import sharp at module level - resolves to null if not available
8
+ const sharpPromise = import('sharp')
9
+ .then((m) => {
10
+ return m.default;
11
+ })
12
+ .catch(() => {
13
+ return null;
14
+ });
15
+ export const DEFAULT_SNAPSHOT_FORMAT = 'raw';
16
+ // ============================================================================
17
+ // A11y Client Code Loading
18
+ // ============================================================================
19
+ let a11yClientCode = null;
20
+ function getA11yClientCode() {
21
+ if (a11yClientCode) {
22
+ return a11yClientCode;
23
+ }
24
+ const currentDir = path.dirname(fileURLToPath(import.meta.url));
25
+ const a11yClientPath = path.join(currentDir, '..', 'dist', 'a11y-client.js');
26
+ a11yClientCode = fs.readFileSync(a11yClientPath, 'utf-8');
27
+ return a11yClientCode;
28
+ }
29
+ async function ensureA11yClient(page) {
30
+ const hasA11y = await page.evaluate(() => !!globalThis.__a11y);
31
+ if (!hasA11y) {
32
+ const code = getA11yClientCode();
33
+ await page.evaluate(code);
34
+ }
35
+ }
36
+ // ============================================================================
37
+ // Image Resize Utility
38
+ // ============================================================================
39
+ /**
40
+ * LLM-optimal max dimension. Claude auto-resizes images larger than 1568px
41
+ * on any edge, adding latency. Token cost: (width * height) / 750.
42
+ */
43
+ export const LLM_MAX_DIMENSION = 1568;
44
+ /**
45
+ * Resize an image using sharp. Useful for shrinking screenshots before reading
46
+ * them back into context so they consume fewer tokens.
47
+ *
48
+ * Default behavior (no width/height): fits within 1568×1568px, preserving
49
+ * aspect ratio, never upscales. This is optimal for Claude vision.
50
+ *
51
+ * Explicit width/height: resizes to those dimensions using the fit strategy.
52
+ */
53
+ export async function resizeImageForAgent(options) {
54
+ const sharp = await sharpPromise;
55
+ if (!sharp) {
56
+ throw new Error('sharp is not installed — install it with: pnpm add sharp');
57
+ }
58
+ const inputBuffer = (() => {
59
+ if (Buffer.isBuffer(options.input)) {
60
+ return options.input;
61
+ }
62
+ return fs.readFileSync(options.input);
63
+ })();
64
+ const quality = options.quality ?? 80;
65
+ const hasExplicitDimensions = options.width !== undefined || options.height !== undefined;
66
+ const fit = options.fit ?? 'inside';
67
+ const resizeOpts = (() => {
68
+ if (hasExplicitDimensions) {
69
+ return {
70
+ width: options.width,
71
+ height: options.height,
72
+ fit,
73
+ withoutEnlargement: false,
74
+ };
75
+ }
76
+ const max = options.maxDimension ?? LLM_MAX_DIMENSION;
77
+ return {
78
+ width: max,
79
+ height: max,
80
+ fit,
81
+ withoutEnlargement: true,
82
+ };
83
+ })();
84
+ const fmt = options.format ?? 'png';
85
+ const pipeline = sharp(inputBuffer).resize(resizeOpts);
86
+ const buffer = await (fmt === 'png' ? pipeline.png() : pipeline.jpeg({ quality })).toBuffer();
87
+ // Default: overwrite input file. When input is a Buffer, no file is written
88
+ // unless output is explicitly set.
89
+ const outputPath = (() => {
90
+ if (options.output) {
91
+ return options.output;
92
+ }
93
+ if (typeof options.input === 'string') {
94
+ return options.input;
95
+ }
96
+ return undefined;
97
+ })();
98
+ if (outputPath) {
99
+ fs.writeFileSync(outputPath, buffer);
100
+ }
101
+ const mimeType = fmt === 'png' ? 'image/png' : 'image/jpeg';
102
+ return {
103
+ buffer,
104
+ mimeType,
105
+ ...(outputPath ? { path: outputPath } : {}),
106
+ };
107
+ }
108
+ export function buildShortRefMap({ refs }) {
109
+ const map = new Map();
110
+ refs.forEach((entry, index) => {
111
+ map.set(entry.ref, `e${index + 1}`);
112
+ });
113
+ return map;
114
+ }
115
+ // Roles that represent interactive elements
116
+ const INTERACTIVE_ROLES = new Set([
117
+ 'button',
118
+ 'link',
119
+ 'textbox',
120
+ 'combobox',
121
+ 'searchbox',
122
+ 'checkbox',
123
+ 'radio',
124
+ 'slider',
125
+ 'spinbutton',
126
+ 'switch',
127
+ 'menuitem',
128
+ 'menuitemcheckbox',
129
+ 'menuitemradio',
130
+ 'option',
131
+ 'tab',
132
+ 'treeitem',
133
+ 'img',
134
+ 'video',
135
+ 'audio',
136
+ ]);
137
+ const LABEL_ROLES = new Set(['labeltext']);
138
+ const MAX_LABEL_POSITION_CONCURRENCY = 24;
139
+ const BOX_MODEL_TIMEOUT_MS = 5000;
140
+ const CONTEXT_ROLES = new Set([
141
+ 'navigation',
142
+ 'main',
143
+ 'contentinfo',
144
+ 'banner',
145
+ 'form',
146
+ 'section',
147
+ 'region',
148
+ 'list',
149
+ 'listitem',
150
+ 'table',
151
+ 'rowgroup',
152
+ 'row',
153
+ 'cell',
154
+ ]);
155
+ const SKIP_WRAPPER_ROLES = new Set(['generic', 'group', 'none', 'presentation']);
156
+ const TEST_ID_ATTRS = [
157
+ 'data-testid',
158
+ 'data-test-id',
159
+ 'data-test',
160
+ 'data-cy',
161
+ 'data-pw',
162
+ 'data-qa',
163
+ 'data-e2e',
164
+ 'data-automation-id',
165
+ ];
166
+ function toAttributeMap(attributes) {
167
+ const result = new Map();
168
+ if (!attributes) {
169
+ return result;
170
+ }
171
+ for (let i = 0; i < attributes.length; i += 2) {
172
+ const name = attributes[i];
173
+ const value = attributes[i + 1];
174
+ if (name) {
175
+ result.set(name, value ?? '');
176
+ }
177
+ }
178
+ return result;
179
+ }
180
+ function getStableRefFromAttributes(attributes) {
181
+ // Test IDs first: they are explicitly placed by developers for automation
182
+ // and more stable than id which is often auto-generated by frameworks
183
+ for (const attr of TEST_ID_ATTRS) {
184
+ const value = attributes.get(attr);
185
+ if (value) {
186
+ return { value, attr };
187
+ }
188
+ }
189
+ const id = attributes.get('id');
190
+ if (id) {
191
+ return { value: id, attr: 'id' };
192
+ }
193
+ return null;
194
+ }
195
+ function escapeLocatorValue(value) {
196
+ return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
197
+ }
198
+ function buildLocatorFromStable(stable) {
199
+ const escaped = escapeLocatorValue(stable.value);
200
+ return `[${stable.attr}="${escaped}"]`;
201
+ }
202
+ function buildBaseLocator({ role, name, stable, isPromotedContentEditable, }) {
203
+ if (stable) {
204
+ return buildLocatorFromStable(stable);
205
+ }
206
+ // For promoted contenteditable elements (bare <div contenteditable="true"> without
207
+ // role="textbox"), use CSS attribute selector. Playwright's role=textbox selector
208
+ // won't match these because Chrome doesn't assign an implicit textbox role to
209
+ // contenteditable divs. Elements that already had role="textbox" use the normal
210
+ // role-based locator since Playwright matches those correctly.
211
+ if (isPromotedContentEditable) {
212
+ return `[contenteditable="true"]`;
213
+ }
214
+ const trimmedName = name.trim();
215
+ if (trimmedName.length > 0) {
216
+ const escapedName = escapeLocatorValue(trimmedName);
217
+ return `role=${role}[name="${escapedName}"]`;
218
+ }
219
+ return `role=${role}`;
220
+ }
221
+ function getAxValueString(value) {
222
+ if (!value) {
223
+ return '';
224
+ }
225
+ const raw = value.value;
226
+ if (typeof raw === 'string') {
227
+ return raw;
228
+ }
229
+ if (raw === undefined || raw === null) {
230
+ return '';
231
+ }
232
+ return String(raw);
233
+ }
234
+ function getAxRole(node) {
235
+ const role = getAxValueString(node.role);
236
+ return role.toLowerCase();
237
+ }
238
+ function buildSnapshotLine({ role, name, baseLocator, indent, hasChildren, }) {
239
+ const prefix = ' '.repeat(indent);
240
+ let text = `${prefix}- ${role}`;
241
+ if (name) {
242
+ const escapedName = name.replace(/"/g, '\\"');
243
+ text += ` "${escapedName}"`;
244
+ }
245
+ return { text, baseLocator, hasChildren, role, name, indent };
246
+ }
247
+ function buildTextLine(text, indent) {
248
+ const prefix = ' '.repeat(indent);
249
+ const escaped = text.replace(/"/g, '\\"');
250
+ return { text: `${prefix}- text: "${escaped}"` };
251
+ }
252
+ export function buildSnapshotLines(nodes, indent = 0) {
253
+ return nodes.flatMap((node) => {
254
+ const nodeIndent = indent + (node.indentOffset ?? 0);
255
+ const line = node.role === 'text'
256
+ ? buildTextLine(node.name, nodeIndent)
257
+ : buildSnapshotLine({
258
+ role: node.role,
259
+ name: node.name,
260
+ baseLocator: node.baseLocator,
261
+ indent: nodeIndent,
262
+ hasChildren: node.children.length > 0,
263
+ });
264
+ return [line, ...buildSnapshotLines(node.children, nodeIndent + 1)];
265
+ });
266
+ }
267
+ function shiftIndent(nodes, offset) {
268
+ return nodes.map((node) => {
269
+ return { ...node, indentOffset: (node.indentOffset ?? 0) + offset };
270
+ });
271
+ }
272
+ export function buildRawSnapshotTree(options) {
273
+ const node = options.axById.get(options.nodeId);
274
+ if (!node) {
275
+ return null;
276
+ }
277
+ const role = getAxRole(node);
278
+ const name = getAxValueString(node.name).trim();
279
+ const children = (node.childIds ?? [])
280
+ .map((childId) => {
281
+ return buildRawSnapshotTree({
282
+ nodeId: childId,
283
+ axById: options.axById,
284
+ isNodeInScope: options.isNodeInScope,
285
+ });
286
+ })
287
+ .filter(isTruthy);
288
+ const inScope = options.isNodeInScope(node) || children.length > 0;
289
+ if (!inScope) {
290
+ return null;
291
+ }
292
+ return {
293
+ role,
294
+ name,
295
+ backendNodeId: node.backendDOMNodeId,
296
+ ignored: node.ignored,
297
+ children,
298
+ };
299
+ }
300
+ export function filterInteractiveSnapshotTree(options) {
301
+ const role = options.node.role;
302
+ const name = options.node.name;
303
+ const hasName = name.length > 0;
304
+ const nextAncestors = hasName ? [...options.ancestorNames, name] : options.ancestorNames;
305
+ const isLabel = LABEL_ROLES.has(role);
306
+ const nextLabelContext = options.labelContext || isLabel;
307
+ const childResults = options.node.children.map((child) => {
308
+ return filterInteractiveSnapshotTree({
309
+ node: child,
310
+ ancestorNames: nextAncestors,
311
+ labelContext: nextLabelContext,
312
+ refFilter: options.refFilter,
313
+ domByBackendId: options.domByBackendId,
314
+ promotedContentEditableIds: options.promotedContentEditableIds,
315
+ createRefForNode: options.createRefForNode,
316
+ });
317
+ });
318
+ const childNodes = childResults.flatMap((result) => {
319
+ return result.nodes;
320
+ });
321
+ const childNames = childResults.reduce((acc, result) => {
322
+ result.names.forEach((childName) => {
323
+ acc.add(childName);
324
+ });
325
+ return acc;
326
+ }, new Set());
327
+ if (options.node.ignored) {
328
+ return { nodes: shiftIndent(childNodes, 1), names: childNames };
329
+ }
330
+ if (isTextRole(role)) {
331
+ if (!hasName) {
332
+ return { nodes: childNodes, names: childNames };
333
+ }
334
+ if (!options.labelContext) {
335
+ return { nodes: childNodes, names: childNames };
336
+ }
337
+ const isRedundantText = options.ancestorNames.some((ancestor) => {
338
+ return ancestor.includes(name) || name.includes(ancestor);
339
+ });
340
+ if (isRedundantText) {
341
+ return { nodes: childNodes, names: childNames };
342
+ }
343
+ const names = new Set(childNames);
344
+ names.add(name);
345
+ const textNode = { role: 'text', name, children: [] };
346
+ return { nodes: [textNode], names };
347
+ }
348
+ const hasChildren = childNodes.length > 0;
349
+ const nameToUse = hasName && (childNames.has(name) || isSubstringOfAny(name, childNames)) ? '' : name;
350
+ const hasNameToUse = nameToUse.length > 0;
351
+ const isWrapper = SKIP_WRAPPER_ROLES.has(role);
352
+ const isInteractive = INTERACTIVE_ROLES.has(role);
353
+ const isContext = CONTEXT_ROLES.has(role);
354
+ const passesRefFilter = !options.refFilter || options.refFilter({ role, name });
355
+ const includeInteractive = isInteractive && passesRefFilter;
356
+ const shouldInclude = includeInteractive || isLabel || isContext || hasChildren;
357
+ if (!shouldInclude) {
358
+ return { nodes: childNodes, names: childNames };
359
+ }
360
+ if (!includeInteractive && !isLabel && !isContext) {
361
+ if (!hasChildren) {
362
+ return { nodes: [], names: childNames };
363
+ }
364
+ return { nodes: childNodes, names: childNames };
365
+ }
366
+ if (isWrapper && !hasNameToUse) {
367
+ if (!hasChildren) {
368
+ return { nodes: [], names: childNames };
369
+ }
370
+ return { nodes: childNodes, names: childNames };
371
+ }
372
+ let baseLocator;
373
+ let ref = null;
374
+ if (includeInteractive) {
375
+ const domInfo = options.node.backendNodeId ? options.domByBackendId.get(options.node.backendNodeId) : undefined;
376
+ const stable = domInfo ? getStableRefFromAttributes(domInfo.attributes) : null;
377
+ const isPromoted = options.node.backendNodeId != null && (options.promotedContentEditableIds?.has(options.node.backendNodeId) ?? false);
378
+ baseLocator = buildBaseLocator({ role, name, stable, isPromotedContentEditable: isPromoted });
379
+ ref = options.createRefForNode({ backendNodeId: options.node.backendNodeId, role, name });
380
+ }
381
+ const nodeEntry = {
382
+ role,
383
+ name: nameToUse,
384
+ baseLocator,
385
+ ref: ref ?? undefined,
386
+ backendNodeId: options.node.backendNodeId,
387
+ children: childNodes,
388
+ };
389
+ const names = new Set(childNames);
390
+ if (hasNameToUse) {
391
+ names.add(nameToUse);
392
+ }
393
+ return { nodes: [nodeEntry], names };
394
+ }
395
+ export function filterFullSnapshotTree(options) {
396
+ const role = options.node.role;
397
+ const name = options.node.name;
398
+ const hasName = name.length > 0;
399
+ const nextAncestors = hasName ? [...options.ancestorNames, name] : options.ancestorNames;
400
+ const childResults = options.node.children.map((child) => {
401
+ return filterFullSnapshotTree({
402
+ node: child,
403
+ ancestorNames: nextAncestors,
404
+ refFilter: options.refFilter,
405
+ domByBackendId: options.domByBackendId,
406
+ promotedContentEditableIds: options.promotedContentEditableIds,
407
+ createRefForNode: options.createRefForNode,
408
+ });
409
+ });
410
+ const childNodes = childResults.flatMap((result) => {
411
+ return result.nodes;
412
+ });
413
+ const childNames = childResults.reduce((acc, result) => {
414
+ result.names.forEach((childName) => {
415
+ acc.add(childName);
416
+ });
417
+ return acc;
418
+ }, new Set());
419
+ if (options.node.ignored) {
420
+ return { nodes: shiftIndent(childNodes, 1), names: childNames };
421
+ }
422
+ if (isTextRole(role)) {
423
+ if (!hasName) {
424
+ return { nodes: childNodes, names: childNames };
425
+ }
426
+ const isRedundantText = options.ancestorNames.some((ancestor) => {
427
+ return ancestor.includes(name) || name.includes(ancestor);
428
+ });
429
+ if (isRedundantText) {
430
+ return { nodes: childNodes, names: childNames };
431
+ }
432
+ const names = new Set(childNames);
433
+ names.add(name);
434
+ const textNode = { role: 'text', name, children: [] };
435
+ return { nodes: [textNode], names };
436
+ }
437
+ const hasChildren = childNodes.length > 0;
438
+ const nameToUse = hasName && (childNames.has(name) || isSubstringOfAny(name, childNames)) ? '' : name;
439
+ const hasNameToUse = nameToUse.length > 0;
440
+ const isWrapper = SKIP_WRAPPER_ROLES.has(role);
441
+ const isInteractive = INTERACTIVE_ROLES.has(role);
442
+ const passesRefFilter = !options.refFilter || options.refFilter({ role, name });
443
+ const includeInteractive = isInteractive && passesRefFilter;
444
+ const shouldInclude = includeInteractive || hasNameToUse || hasChildren;
445
+ if (!shouldInclude) {
446
+ return { nodes: childNodes, names: childNames };
447
+ }
448
+ if (isWrapper && !hasNameToUse) {
449
+ if (!hasChildren) {
450
+ return { nodes: [], names: childNames };
451
+ }
452
+ return { nodes: childNodes, names: childNames };
453
+ }
454
+ let baseLocator;
455
+ let ref = null;
456
+ if (includeInteractive) {
457
+ const domInfo = options.node.backendNodeId ? options.domByBackendId.get(options.node.backendNodeId) : undefined;
458
+ const stable = domInfo ? getStableRefFromAttributes(domInfo.attributes) : null;
459
+ const isPromoted = options.node.backendNodeId != null && (options.promotedContentEditableIds?.has(options.node.backendNodeId) ?? false);
460
+ baseLocator = buildBaseLocator({ role, name, stable, isPromotedContentEditable: isPromoted });
461
+ ref = options.createRefForNode({ backendNodeId: options.node.backendNodeId, role, name });
462
+ }
463
+ const nodeEntry = {
464
+ role,
465
+ name: nameToUse,
466
+ baseLocator,
467
+ ref: ref ?? undefined,
468
+ backendNodeId: options.node.backendNodeId,
469
+ children: childNodes,
470
+ };
471
+ const names = new Set(childNames);
472
+ if (hasNameToUse) {
473
+ names.add(nameToUse);
474
+ }
475
+ return { nodes: [nodeEntry], names };
476
+ }
477
+ function buildLocatorLineText({ line, locator }) {
478
+ const prefix = ' '.repeat(line.indent ?? 0);
479
+ const role = line.role ?? '';
480
+ const name = line.name ?? '';
481
+ const escapedName = name.replace(/"/g, '\\"');
482
+ const hasRoleInLocator = role ? locator.includes(role) : false;
483
+ const hasNameInLocator = name ? locator.includes(escapedName) : false;
484
+ const parts = [];
485
+ if (role && !hasRoleInLocator) {
486
+ parts.push(role);
487
+ }
488
+ if (name && !hasNameInLocator) {
489
+ parts.push(`"${escapedName}"`);
490
+ }
491
+ const base = parts.length > 0 ? `${prefix}- ${parts.join(' ')}` : `${prefix}-`;
492
+ return `${base} ${locator}`;
493
+ }
494
+ export function finalizeSnapshotOutput(lines, nodes, shortRefMap) {
495
+ const locatorCounts = lines.reduce((acc, line) => {
496
+ if (!line.baseLocator) {
497
+ return acc;
498
+ }
499
+ acc.set(line.baseLocator, (acc.get(line.baseLocator) ?? 0) + 1);
500
+ return acc;
501
+ }, new Map());
502
+ const locatorIndices = new Map();
503
+ const locatorSequence = lines.reduce((acc, line) => {
504
+ if (!line.baseLocator) {
505
+ return acc;
506
+ }
507
+ const count = locatorCounts.get(line.baseLocator) ?? 0;
508
+ const index = locatorIndices.get(line.baseLocator) ?? 0;
509
+ locatorIndices.set(line.baseLocator, index + 1);
510
+ const locator = count > 1 ? `${line.baseLocator} >> nth=${index}` : line.baseLocator;
511
+ acc.push(locator);
512
+ return acc;
513
+ }, []);
514
+ let lineLocatorIndex = 0;
515
+ const snapshot = lines
516
+ .map((line) => {
517
+ let text = line.text;
518
+ if (line.baseLocator) {
519
+ const locator = locatorSequence[lineLocatorIndex];
520
+ lineLocatorIndex += 1;
521
+ text = buildLocatorLineText({ line, locator });
522
+ }
523
+ if (line.hasChildren) {
524
+ text += ':';
525
+ }
526
+ return text;
527
+ })
528
+ .join('\n');
529
+ let nodeLocatorIndex = 0;
530
+ const applyLocators = (items) => {
531
+ return items.map((item) => {
532
+ const locator = item.baseLocator ? locatorSequence[nodeLocatorIndex++] : undefined;
533
+ const children = applyLocators(item.children);
534
+ return {
535
+ role: item.role,
536
+ name: item.name,
537
+ locator,
538
+ ref: item.ref,
539
+ shortRef: item.ref ? (shortRefMap.get(item.ref) ?? item.ref) : undefined,
540
+ backendNodeId: item.backendNodeId,
541
+ children,
542
+ };
543
+ });
544
+ };
545
+ return { snapshot, tree: applyLocators(nodes) };
546
+ }
547
+ function buildDomIndex(nodes) {
548
+ const domById = new Map();
549
+ const domByBackendId = new Map();
550
+ const childrenByParent = new Map();
551
+ for (const node of nodes) {
552
+ const info = {
553
+ nodeId: node.nodeId,
554
+ parentId: node.parentId,
555
+ backendNodeId: node.backendNodeId,
556
+ nodeName: node.nodeName,
557
+ attributes: toAttributeMap(node.attributes),
558
+ };
559
+ domById.set(node.nodeId, info);
560
+ domByBackendId.set(node.backendNodeId, info);
561
+ if (node.parentId) {
562
+ if (!childrenByParent.has(node.parentId)) {
563
+ childrenByParent.set(node.parentId, []);
564
+ }
565
+ childrenByParent.get(node.parentId).push(node.nodeId);
566
+ }
567
+ }
568
+ return { domById, domByBackendId, childrenByParent };
569
+ }
570
+ function findScopeRootNodeId(nodes, attrName, attrValue) {
571
+ for (const node of nodes) {
572
+ if (!node.attributes) {
573
+ continue;
574
+ }
575
+ for (let i = 0; i < node.attributes.length; i += 2) {
576
+ const name = node.attributes[i];
577
+ const value = node.attributes[i + 1];
578
+ if (name === attrName && value === attrValue) {
579
+ return node.nodeId;
580
+ }
581
+ }
582
+ }
583
+ return null;
584
+ }
585
+ function buildBackendIdSet(rootNodeId, childrenByParent, domById) {
586
+ const result = new Set();
587
+ const stack = [rootNodeId];
588
+ while (stack.length > 0) {
589
+ const current = stack.pop();
590
+ if (current === undefined) {
591
+ continue;
592
+ }
593
+ const node = domById.get(current);
594
+ if (node) {
595
+ result.add(node.backendNodeId);
596
+ }
597
+ const children = childrenByParent.get(current);
598
+ if (children && children.length > 0) {
599
+ stack.push(...children);
600
+ }
601
+ }
602
+ return result;
603
+ }
604
+ function isTextRole(role) {
605
+ return role === 'statictext' || role === 'inlinetextbox';
606
+ }
607
+ // Per HTML spec, contenteditable is editable when the attribute is present
608
+ // with value "true", "" (empty string), or "plaintext-only". Bare attribute
609
+ // (no value) is also treated as empty string by the browser DOM parser.
610
+ function isContentEditable(value) {
611
+ if (value == null) {
612
+ return false;
613
+ }
614
+ const v = value.trim().toLowerCase();
615
+ return v === '' || v === 'true' || v === 'plaintext-only';
616
+ }
617
+ function isSubstringOfAny(needle, haystack) {
618
+ for (const str of haystack) {
619
+ if (str.includes(needle)) {
620
+ return true;
621
+ }
622
+ }
623
+ return false;
624
+ }
625
+ // ============================================================================
626
+ // Frame Resolution
627
+ // ============================================================================
628
+ /**
629
+ * Resolve a FrameLocator to an actual Frame object. FrameLocator (returned by
630
+ * locator.contentFrame()) is a scoping helper that doesn't have CDP methods like
631
+ * frameId(). We need the real Frame from page.frames() for OOPIF session attachment.
632
+ *
633
+ * If a Frame is passed directly, it's returned as-is.
634
+ * If a FrameLocator is passed, we find the matching Frame by locating the iframe
635
+ * element and matching its src URL against page.frames().
636
+ */
637
+ async function resolveFrame({ frame, page }) {
638
+ if (!frame) {
639
+ return undefined;
640
+ }
641
+ // Frame has frameId(), FrameLocator does not
642
+ if (typeof frame.frameId === 'function') {
643
+ return frame;
644
+ }
645
+ // It's a FrameLocator — resolve to a Frame via the owner iframe element.
646
+ // Use elementHandle().contentFrame() which returns the actual Frame object
647
+ // directly from the <iframe> element, avoiding ambiguity when multiple
648
+ // frames share the same origin (e.g. framer plugin iframes).
649
+ const frameLocator = frame;
650
+ const owner = frameLocator.owner();
651
+ const handle = await owner.elementHandle();
652
+ if (!handle) {
653
+ throw new Error('Could not resolve FrameLocator to a Frame: iframe element not found');
654
+ }
655
+ const resolved = await handle.contentFrame();
656
+ if (!resolved) {
657
+ throw new Error('Could not resolve FrameLocator to a Frame: contentFrame() returned null');
658
+ }
659
+ return resolved;
660
+ }
661
+ // ============================================================================
662
+ // Main Functions
663
+ // ============================================================================
664
+ /**
665
+ * Get an accessibility snapshot with utilities to look up refs for elements.
666
+ * Uses the browser accessibility tree (CDP) and maps nodes to DOM attributes.
667
+ *
668
+ * Refs are generated from stable test IDs when available (data-testid, data-test-id, etc.)
669
+ * or fall back to e1, e2, e3...
670
+ *
671
+ * @param page - Playwright page
672
+ * @param locator - Optional locator to scope the snapshot to a subtree
673
+ * @param refFilter - Optional filter for which elements get refs
674
+ *
675
+ * @example
676
+ * ```ts
677
+ * const { snapshot, getSelectorForRef } = await getAriaSnapshot({ page })
678
+ * // Snapshot shows locators like [id="submit-btn"] or role=button[name="Submit"]
679
+ * const selector = getSelectorForRef('submit-btn')
680
+ * await page.locator(selector).click()
681
+ * ```
682
+ */
683
+ export async function getAriaSnapshot({ page, frame, locator, refFilter, interactiveOnly = false, cdp, }) {
684
+ const session = cdp || (await getCDPSessionForPage({ page }));
685
+ // Resolve FrameLocator to an actual Frame. FrameLocator (from locator.contentFrame())
686
+ // is a scoping helper without CDP access. We need the real Frame from page.frames()
687
+ // which has frameId() for OOPIF session attachment.
688
+ const resolvedFrame = await resolveFrame({ frame, page });
689
+ // For cross-origin iframes (OOPIFs), we need to attach to the iframe's target
690
+ // to get a separate CDP session. Same-origin iframes can use frameId directly.
691
+ let oopifSessionId = null;
692
+ const frameId = resolvedFrame?.frameId() ?? null;
693
+ if (frameId) {
694
+ const { targetInfos } = (await session.send('Target.getTargets'));
695
+ const frameUrl = resolvedFrame.url();
696
+ const iframeTarget = targetInfos.find((t) => {
697
+ return t.type === 'iframe' && t.url === frameUrl;
698
+ });
699
+ if (iframeTarget) {
700
+ const { sessionId } = (await session.send('Target.attachToTarget', {
701
+ targetId: iframeTarget.targetId,
702
+ flatten: true,
703
+ }));
704
+ oopifSessionId = sessionId;
705
+ await session.send('Runtime.runIfWaitingForDebugger', undefined, oopifSessionId);
706
+ }
707
+ }
708
+ await session.send('DOM.enable', undefined, oopifSessionId);
709
+ await session.send('Accessibility.enable', undefined, oopifSessionId);
710
+ const scopeAttr = 'data-pw-scope';
711
+ const scopeValue = crypto.randomUUID();
712
+ let scopeApplied = false;
713
+ const scopeLocator = locator;
714
+ try {
715
+ if (scopeLocator) {
716
+ await scopeLocator.evaluate((element, data) => {
717
+ element.setAttribute(data.attr, data.value);
718
+ }, { attr: scopeAttr, value: scopeValue });
719
+ scopeApplied = true;
720
+ }
721
+ const { nodes: domNodes } = (await session.send('DOM.getFlattenedDocument', { depth: -1, pierce: true }, oopifSessionId));
722
+ const { domById, domByBackendId, childrenByParent } = buildDomIndex(domNodes);
723
+ let scopeRootNodeId = null;
724
+ let scopeRootBackendId = null;
725
+ if (scopeLocator) {
726
+ scopeRootNodeId = findScopeRootNodeId(domNodes, scopeAttr, scopeValue);
727
+ if (scopeRootNodeId) {
728
+ const scopeNode = domById.get(scopeRootNodeId);
729
+ if (scopeNode) {
730
+ scopeRootBackendId = scopeNode.backendNodeId;
731
+ }
732
+ }
733
+ }
734
+ const allowedBackendIds = scopeRootNodeId ? buildBackendIdSet(scopeRootNodeId, childrenByParent, domById) : null;
735
+ const axParams = !oopifSessionId && frameId ? { frameId } : undefined;
736
+ const { nodes: axNodes } = (await session.send('Accessibility.getFullAXTree', axParams, oopifSessionId));
737
+ const axById = new Map();
738
+ for (const node of axNodes) {
739
+ axById.set(node.nodeId, node);
740
+ }
741
+ // Index AX nodes by backendDOMNodeId for O(1) lookups during promotion
742
+ // and root finding (instead of repeated O(n) axNodes.find() calls)
743
+ const axByBackendId = new Map();
744
+ for (const node of axNodes) {
745
+ if (node.backendDOMNodeId) {
746
+ axByBackendId.set(node.backendDOMNodeId, node);
747
+ }
748
+ }
749
+ // Promote contenteditable elements that Chrome's AX tree doesn't classify as
750
+ // interactive. Rich text editors (ProseMirror, Tiptap, Slate, Lexical, etc.) use
751
+ // bare <div contenteditable="true"> without role="textbox", so Chrome reports
752
+ // them as "generic" and they become invisible in the snapshot. We detect these
753
+ // via the DOM tree and override the AX role to "textbox" so they appear as
754
+ // interactive elements the AI can target.
755
+ const promotedContentEditableIds = new Set();
756
+ for (const [, domInfo] of domByBackendId) {
757
+ if (!isContentEditable(domInfo.attributes.get('contenteditable'))) {
758
+ continue;
759
+ }
760
+ const axNode = axByBackendId.get(domInfo.backendNodeId);
761
+ if (!axNode) {
762
+ continue;
763
+ }
764
+ const currentRole = getAxRole(axNode);
765
+ if (INTERACTIVE_ROLES.has(currentRole)) {
766
+ continue;
767
+ }
768
+ axNode.role = { type: 'role', value: 'textbox' };
769
+ promotedContentEditableIds.add(domInfo.backendNodeId);
770
+ }
771
+ const findRootAxNodeId = () => {
772
+ if (scopeRootBackendId) {
773
+ const scoped = axByBackendId.get(scopeRootBackendId);
774
+ if (scoped) {
775
+ return scoped.nodeId;
776
+ }
777
+ }
778
+ const rootWebArea = axNodes.find((node) => {
779
+ return getAxRole(node) === 'rootwebarea';
780
+ });
781
+ if (rootWebArea) {
782
+ return rootWebArea.nodeId;
783
+ }
784
+ const webArea = axNodes.find((node) => {
785
+ return getAxRole(node) === 'webarea';
786
+ });
787
+ if (webArea) {
788
+ return webArea.nodeId;
789
+ }
790
+ const topLevel = axNodes.find((node) => {
791
+ return !node.parentId;
792
+ });
793
+ return topLevel ? topLevel.nodeId : null;
794
+ };
795
+ const rootAxNodeId = findRootAxNodeId();
796
+ const refCounts = new Map();
797
+ let fallbackCounter = 0;
798
+ const refs = [];
799
+ const createRefForNode = (options) => {
800
+ if (!INTERACTIVE_ROLES.has(options.role)) {
801
+ return null;
802
+ }
803
+ const domInfo = options.backendNodeId ? domByBackendId.get(options.backendNodeId) : undefined;
804
+ const stable = domInfo ? getStableRefFromAttributes(domInfo.attributes) : null;
805
+ let baseRef = stable?.value;
806
+ if (!baseRef) {
807
+ fallbackCounter += 1;
808
+ baseRef = `e${fallbackCounter}`;
809
+ }
810
+ const count = refCounts.get(baseRef) ?? 0;
811
+ refCounts.set(baseRef, count + 1);
812
+ const ref = count === 0 ? baseRef : `${baseRef}-${count + 1}`;
813
+ let selector;
814
+ if (stable && count === 0) {
815
+ selector = buildLocatorFromStable(stable);
816
+ }
817
+ // For promoted contenteditable elements without a stable selector, store
818
+ // [contenteditable="true"] so getSelectorForRef() doesn't fall back to
819
+ // role=textbox which Playwright can't match on bare contenteditable divs.
820
+ if (!selector && options.backendNodeId != null && promotedContentEditableIds.has(options.backendNodeId)) {
821
+ selector = '[contenteditable="true"]';
822
+ }
823
+ refs.push({ ref, role: options.role, name: options.name, selector, backendNodeId: options.backendNodeId });
824
+ return ref;
825
+ };
826
+ const isNodeInScope = (node) => {
827
+ if (!allowedBackendIds) {
828
+ return true;
829
+ }
830
+ if (!node.backendDOMNodeId) {
831
+ return false;
832
+ }
833
+ return allowedBackendIds.has(node.backendDOMNodeId);
834
+ };
835
+ let snapshotNodes = [];
836
+ if (rootAxNodeId) {
837
+ const rootNode = axById.get(rootAxNodeId);
838
+ const rootRole = rootNode ? getAxRole(rootNode) : '';
839
+ const rawRoots = rootNode && (rootRole === 'rootwebarea' || rootRole === 'webarea') && rootNode.childIds
840
+ ? rootNode.childIds
841
+ .map((childId) => {
842
+ return buildRawSnapshotTree({ nodeId: childId, axById, isNodeInScope });
843
+ })
844
+ .filter(isTruthy)
845
+ : [buildRawSnapshotTree({ nodeId: rootAxNodeId, axById, isNodeInScope })].filter(isTruthy);
846
+ const filtered = rawRoots.flatMap((rawNode) => {
847
+ if (interactiveOnly) {
848
+ return filterInteractiveSnapshotTree({
849
+ node: rawNode,
850
+ ancestorNames: [],
851
+ labelContext: false,
852
+ refFilter,
853
+ domByBackendId,
854
+ promotedContentEditableIds,
855
+ createRefForNode,
856
+ }).nodes;
857
+ }
858
+ return filterFullSnapshotTree({
859
+ node: rawNode,
860
+ ancestorNames: [],
861
+ refFilter,
862
+ domByBackendId,
863
+ promotedContentEditableIds,
864
+ createRefForNode,
865
+ }).nodes;
866
+ });
867
+ snapshotNodes = filtered;
868
+ }
869
+ const snapshotLines = buildSnapshotLines(snapshotNodes);
870
+ const shortRefMap = buildShortRefMap({ refs });
871
+ const finalized = finalizeSnapshotOutput(snapshotLines, snapshotNodes, shortRefMap);
872
+ const refsWithShortRef = refs.map((entry) => {
873
+ return {
874
+ ...entry,
875
+ shortRef: shortRefMap.get(entry.ref) ?? entry.ref,
876
+ };
877
+ });
878
+ const result = { snapshot: finalized.snapshot, tree: finalized.tree, refs: refsWithShortRef };
879
+ // Build refToElement map
880
+ const refToElement = new Map();
881
+ const refToSelector = new Map();
882
+ for (const { ref, role, name, shortRef } of result.refs) {
883
+ if (!refFilter || refFilter({ role, name })) {
884
+ refToElement.set(ref, { role, name, shortRef });
885
+ }
886
+ }
887
+ for (const { ref, selector } of result.refs) {
888
+ if (!selector) {
889
+ continue;
890
+ }
891
+ refToSelector.set(ref, selector);
892
+ }
893
+ const snapshot = result.snapshot;
894
+ const getSelectorForRef = (ref) => {
895
+ const mapped = refToSelector.get(ref);
896
+ if (mapped) {
897
+ return mapped;
898
+ }
899
+ const info = refToElement.get(ref);
900
+ if (!info) {
901
+ return null;
902
+ }
903
+ const escapedName = info.name.replace(/"/g, '\\"');
904
+ return `role=${info.role}[name="${escapedName}"]`;
905
+ };
906
+ const getRefsForLocators = async (locators) => {
907
+ if (locators.length === 0) {
908
+ return [];
909
+ }
910
+ const targetHandles = await Promise.all(locators.map(async (loc) => {
911
+ try {
912
+ return 'elementHandle' in loc
913
+ ? await loc.elementHandle({ timeout: 1000 })
914
+ : loc;
915
+ }
916
+ catch {
917
+ return null;
918
+ }
919
+ }));
920
+ const matchingRefs = await page.evaluate(({ targets, refData }) => {
921
+ return targets.map((target) => {
922
+ if (!target) {
923
+ return null;
924
+ }
925
+ const testIdAttrs = [
926
+ 'data-testid',
927
+ 'data-test-id',
928
+ 'data-test',
929
+ 'data-cy',
930
+ 'data-pw',
931
+ 'data-qa',
932
+ 'data-e2e',
933
+ 'data-automation-id',
934
+ ];
935
+ for (const attr of testIdAttrs) {
936
+ const value = target.getAttribute(attr);
937
+ if (value) {
938
+ const match = refData.find((ref) => {
939
+ return ref.ref === value || ref.ref.startsWith(value);
940
+ });
941
+ if (match) {
942
+ return match.ref;
943
+ }
944
+ }
945
+ }
946
+ const id = target.getAttribute('id');
947
+ if (id) {
948
+ const match = refData.find((ref) => {
949
+ return ref.ref === id || ref.ref.startsWith(id);
950
+ });
951
+ if (match) {
952
+ return match.ref;
953
+ }
954
+ }
955
+ return null;
956
+ });
957
+ }, {
958
+ targets: targetHandles,
959
+ refData: result.refs,
960
+ });
961
+ return matchingRefs.map((ref) => {
962
+ if (!ref) {
963
+ return null;
964
+ }
965
+ const info = refToElement.get(ref);
966
+ return info ? { ...info, ref } : null;
967
+ });
968
+ };
969
+ return {
970
+ snapshot,
971
+ tree: result.tree,
972
+ refs: result.refs,
973
+ refToElement,
974
+ refToSelector,
975
+ getSelectorForRef,
976
+ getRefsForLocators,
977
+ getRefForLocator: async (loc) => (await getRefsForLocators([loc]))[0],
978
+ getRefStringForLocator: async (loc) => (await getRefsForLocators([loc]))[0]?.ref ?? null,
979
+ };
980
+ }
981
+ finally {
982
+ if (scopeApplied && scopeLocator) {
983
+ await scopeLocator.evaluate((element, attr) => {
984
+ element.removeAttribute(attr);
985
+ }, scopeAttr);
986
+ }
987
+ if (oopifSessionId) {
988
+ await session.send('Target.detachFromTarget', { sessionId: oopifSessionId }).catch((e) => {
989
+ console.error('[aria-snapshot] Failed to detach OOPIF session:', oopifSessionId, e);
990
+ });
991
+ }
992
+ if (!cdp) {
993
+ await session.detach();
994
+ }
995
+ }
996
+ }
997
+ function buildBoxFromQuad(quad) {
998
+ if (!quad || quad.length < 8) {
999
+ return null;
1000
+ }
1001
+ const xs = [quad[0], quad[2], quad[4], quad[6]];
1002
+ const ys = [quad[1], quad[3], quad[5], quad[7]];
1003
+ const left = Math.min(...xs);
1004
+ const right = Math.max(...xs);
1005
+ const top = Math.min(...ys);
1006
+ const bottom = Math.max(...ys);
1007
+ return {
1008
+ x: left,
1009
+ y: top,
1010
+ width: Math.max(0, right - left),
1011
+ height: Math.max(0, bottom - top),
1012
+ };
1013
+ }
1014
+ function isTruthy(value) {
1015
+ return Boolean(value);
1016
+ }
1017
+ async function getLabelBoxesForRefs({ page, refs, maxConcurrency = MAX_LABEL_POSITION_CONCURRENCY, logger, cdp, }) {
1018
+ const log = logger?.info ?? logger?.error ?? console.error;
1019
+ const session = cdp || (await getCDPSessionForPage({ page }));
1020
+ const sema = new Sema(maxConcurrency);
1021
+ const labelRefs = refs.filter((ref) => {
1022
+ return Boolean(ref.backendNodeId) && INTERACTIVE_ROLES.has(ref.role);
1023
+ });
1024
+ log(`[getLabelBoxesForRefs] processing ${labelRefs.length} interactive refs (concurrency: ${maxConcurrency})`);
1025
+ const startTime = Date.now();
1026
+ let completed = 0;
1027
+ let timedOut = 0;
1028
+ let failed = 0;
1029
+ try {
1030
+ const labels = await Promise.all(labelRefs.map(async (ref) => {
1031
+ if (!ref.backendNodeId) {
1032
+ return null;
1033
+ }
1034
+ await sema.acquire();
1035
+ try {
1036
+ const response = await Promise.race([
1037
+ session.send('DOM.getBoxModel', {
1038
+ backendNodeId: ref.backendNodeId,
1039
+ }),
1040
+ new Promise((resolve) => {
1041
+ setTimeout(() => {
1042
+ resolve(null);
1043
+ }, BOX_MODEL_TIMEOUT_MS);
1044
+ }),
1045
+ ]);
1046
+ completed++;
1047
+ if (completed % 50 === 0 || completed === labelRefs.length) {
1048
+ log(`[getLabelBoxesForRefs] progress: ${completed}/${labelRefs.length} (${timedOut} timeouts, ${failed} errors) - ${Date.now() - startTime}ms`);
1049
+ }
1050
+ if (!response) {
1051
+ timedOut++;
1052
+ return null;
1053
+ }
1054
+ const box = buildBoxFromQuad(response.model.border);
1055
+ if (!box) {
1056
+ return null;
1057
+ }
1058
+ return { ref: ref.ref, role: ref.role, box };
1059
+ }
1060
+ catch (error) {
1061
+ completed++;
1062
+ failed++;
1063
+ return null;
1064
+ }
1065
+ finally {
1066
+ sema.release();
1067
+ }
1068
+ }));
1069
+ log(`[getLabelBoxesForRefs] done: ${completed} completed, ${timedOut} timeouts, ${failed} errors - ${Date.now() - startTime}ms`);
1070
+ return labels.filter(isTruthy);
1071
+ }
1072
+ finally {
1073
+ if (!cdp) {
1074
+ await session.detach();
1075
+ }
1076
+ }
1077
+ }
1078
+ /**
1079
+ * Show Vimium-style labels on interactive elements.
1080
+ * Labels are colored badges positioned above each element showing the ref.
1081
+ * Use with screenshots so agents can see which elements are interactive.
1082
+ *
1083
+ * Labels auto-hide after 30 seconds to prevent stale labels.
1084
+ * Call this function again if the page HTML changes to get fresh labels.
1085
+ *
1086
+ * @param page - Playwright page
1087
+ * @param locator - Optional locator to scope labels to a subtree
1088
+ * @param interactiveOnly - Only show labels for interactive elements (default: true)
1089
+ *
1090
+ * @example
1091
+ * ```ts
1092
+ * const { snapshot, labelCount } = await showAriaRefLabels({ page })
1093
+ * await page.screenshot({ path: '/tmp/screenshot.png' })
1094
+ * // Agent sees [submit-btn] label on "Submit" button
1095
+ * await page.locator('[data-testid="submit-btn"]').click()
1096
+ * ```
1097
+ */
1098
+ export async function showAriaRefLabels({ page, locator, interactiveOnly = true, logger, }) {
1099
+ const startTime = Date.now();
1100
+ const log = logger?.info ?? logger?.error ?? console.error;
1101
+ log(`[showAriaRefLabels] starting...`);
1102
+ await ensureA11yClient(page);
1103
+ log(`[showAriaRefLabels] ensureA11yClient: ${Date.now() - startTime}ms`);
1104
+ const cdpStart = Date.now();
1105
+ const cdp = await getCDPSessionForPage({ page });
1106
+ log(`[showAriaRefLabels] getCDPSessionForPage: ${Date.now() - cdpStart}ms`);
1107
+ try {
1108
+ const snapshotStart = Date.now();
1109
+ const { snapshot, refs } = await getAriaSnapshot({ page, locator, interactiveOnly, cdp });
1110
+ const shortRefMap = new Map(refs.map((entry) => {
1111
+ return [entry.ref, entry.shortRef];
1112
+ }));
1113
+ const interactiveRefs = refs.filter((ref) => Boolean(ref.backendNodeId) && INTERACTIVE_ROLES.has(ref.role));
1114
+ log(`[showAriaRefLabels] getAriaSnapshot: ${Date.now() - snapshotStart}ms (${refs.length} refs, ${interactiveRefs.length} interactive)`);
1115
+ const rootHandle = locator ? await locator.elementHandle() : null;
1116
+ const labelsStart = Date.now();
1117
+ const labels = await getLabelBoxesForRefs({ page, refs, logger, cdp });
1118
+ const shortLabels = labels.map((label) => {
1119
+ return {
1120
+ ...label,
1121
+ ref: shortRefMap.get(label.ref) ?? label.ref,
1122
+ };
1123
+ });
1124
+ log(`[showAriaRefLabels] getLabelBoxesForRefs: ${Date.now() - labelsStart}ms (${labels.length} boxes)`);
1125
+ const renderStart = Date.now();
1126
+ const labelCount = await page.evaluate(({ entries, root, interactiveOnly: intOnly }) => {
1127
+ const a11y = globalThis.__a11y;
1128
+ if (a11y?.renderA11yLabels) {
1129
+ return a11y.renderA11yLabels(entries);
1130
+ }
1131
+ if (a11y?.computeA11ySnapshot) {
1132
+ const rootElement = root || document.body;
1133
+ return a11y.computeA11ySnapshot({ root: rootElement, interactiveOnly: intOnly, renderLabels: true })
1134
+ .labelCount;
1135
+ }
1136
+ throw new Error('a11y client not loaded');
1137
+ }, { entries: shortLabels, root: rootHandle, interactiveOnly });
1138
+ log(`[showAriaRefLabels] renderA11yLabels: ${Date.now() - renderStart}ms (${labelCount} labels)`);
1139
+ log(`[showAriaRefLabels] total: ${Date.now() - startTime}ms`);
1140
+ return { snapshot, labelCount };
1141
+ }
1142
+ finally {
1143
+ await cdp.detach();
1144
+ }
1145
+ }
1146
+ /**
1147
+ * Remove all aria ref labels from the page.
1148
+ */
1149
+ export async function hideAriaRefLabels({ page }) {
1150
+ await page.evaluate(() => {
1151
+ const a11y = globalThis.__a11y;
1152
+ if (a11y) {
1153
+ a11y.hideA11yLabels();
1154
+ }
1155
+ else {
1156
+ // Fallback if client not loaded
1157
+ const doc = document;
1158
+ const win = window;
1159
+ const timerKey = '__tabwright_labels_timer__';
1160
+ if (win[timerKey]) {
1161
+ win.clearTimeout(win[timerKey]);
1162
+ win[timerKey] = null;
1163
+ }
1164
+ doc.getElementById('__tabwright_labels__')?.remove();
1165
+ }
1166
+ });
1167
+ }
1168
+ /**
1169
+ * Take a screenshot with accessibility labels overlaid on interactive elements.
1170
+ * Shows Vimium-style labels, captures the screenshot, then removes the labels.
1171
+ * The screenshot is automatically included in the MCP response.
1172
+ *
1173
+ * @param page - Playwright page
1174
+ * @param locator - Optional locator to scope labels to a subtree
1175
+ * @param collector - Array to collect screenshots (passed by MCP execute tool)
1176
+ *
1177
+ * @example
1178
+ * ```ts
1179
+ * await screenshotWithAccessibilityLabels({ page })
1180
+ * // Screenshot is automatically included in the MCP response
1181
+ * // Use ref from the snapshot to interact with elements
1182
+ * await page.locator('[data-testid="submit-btn"]').click()
1183
+ * ```
1184
+ */
1185
+ export async function screenshotWithAccessibilityLabels({ page, locator, interactiveOnly = true, collector, logger, }) {
1186
+ const log = logger?.info ?? logger?.error;
1187
+ const showLabelsStart = Date.now();
1188
+ const { snapshot, labelCount } = await showAriaRefLabels({ page, locator, interactiveOnly, logger });
1189
+ if (log) {
1190
+ log(`showAriaRefLabels: ${Date.now() - showLabelsStart}ms`);
1191
+ }
1192
+ // Generate unique filename with timestamp
1193
+ const timestamp = Date.now();
1194
+ const random = Math.random().toString(36).slice(2, 6);
1195
+ const filename = `tabwright-screenshot-${timestamp}-${random}.png`;
1196
+ // Use ./tmp folder (gitignored) instead of system temp
1197
+ const tmpDir = path.join(process.cwd(), 'tmp');
1198
+ if (!fs.existsSync(tmpDir)) {
1199
+ fs.mkdirSync(tmpDir, { recursive: true });
1200
+ }
1201
+ const screenshotPath = path.join(tmpDir, filename);
1202
+ // Get viewport size to clip screenshot to visible area
1203
+ const viewport = (await page.evaluate('({ width: window.innerWidth, height: window.innerHeight })'));
1204
+ // Check if sharp is available for resizing
1205
+ const sharp = await sharpPromise;
1206
+ // Clip dimensions: if sharp unavailable, limit capture area to LLM_MAX_DIMENSION
1207
+ const clipWidth = sharp ? viewport.width : Math.min(viewport.width, LLM_MAX_DIMENSION);
1208
+ const clipHeight = sharp ? viewport.height : Math.min(viewport.height, LLM_MAX_DIMENSION);
1209
+ // Take viewport screenshot as PNG for Kitty Graphics Protocol compatibility.
1210
+ // PNG is lossless and the only format extracted by kitty-graphics-agent (f=100).
1211
+ const screenshotStart = Date.now();
1212
+ const rawBuffer = await page.screenshot({
1213
+ type: 'png',
1214
+ scale: 'css',
1215
+ clip: { x: 0, y: 0, width: clipWidth, height: clipHeight },
1216
+ });
1217
+ if (log) {
1218
+ log(`page.screenshot: ${Date.now() - screenshotStart}ms`);
1219
+ }
1220
+ // Resize with resizeImage if sharp available, otherwise use clipped raw buffer
1221
+ const resizeStart = Date.now();
1222
+ const buffer = await (async () => {
1223
+ if (!sharp) {
1224
+ logger?.error?.('[tabwright] sharp not available, using clipped screenshot (max', LLM_MAX_DIMENSION, 'px)');
1225
+ return rawBuffer;
1226
+ }
1227
+ try {
1228
+ const result = await resizeImageForAgent({ input: rawBuffer, format: 'png' });
1229
+ return result.buffer;
1230
+ }
1231
+ catch (err) {
1232
+ logger?.error?.('[tabwright] sharp resize failed, using raw buffer:', err);
1233
+ return rawBuffer;
1234
+ }
1235
+ })();
1236
+ if (log) {
1237
+ log(`screenshot resize: ${Date.now() - resizeStart}ms`);
1238
+ }
1239
+ // Save to file
1240
+ fs.writeFileSync(screenshotPath, buffer);
1241
+ // Convert to base64
1242
+ const base64 = buffer.toString('base64');
1243
+ // Hide labels
1244
+ await hideAriaRefLabels({ page });
1245
+ // Add to collector array
1246
+ collector.push({
1247
+ path: screenshotPath,
1248
+ base64,
1249
+ mimeType: 'image/png',
1250
+ snapshot,
1251
+ labelCount,
1252
+ });
1253
+ }
1254
+ // Re-export for backward compatibility
1255
+ export { getAriaSnapshot as getAriaSnapshotWithRefs };
1256
+ //# sourceMappingURL=aria-snapshot.js.map