opendevbrowser 0.0.16 → 0.0.18

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 (616) hide show
  1. package/README.md +201 -79
  2. package/dist/annotate/agent-inbox-store.d.ts +58 -0
  3. package/dist/annotate/agent-inbox-store.d.ts.map +1 -0
  4. package/dist/annotate/agent-inbox.d.ts +25 -0
  5. package/dist/annotate/agent-inbox.d.ts.map +1 -0
  6. package/dist/annotate/direct-annotator.d.ts.map +1 -1
  7. package/dist/annotate/timeout-messages.d.ts +4 -0
  8. package/dist/annotate/timeout-messages.d.ts.map +1 -0
  9. package/dist/automation/coordinator.d.ts +55 -0
  10. package/dist/automation/coordinator.d.ts.map +1 -0
  11. package/dist/browser/annotation-manager.d.ts +7 -1
  12. package/dist/browser/annotation-manager.d.ts.map +1 -1
  13. package/dist/browser/browser-manager.d.ts +153 -48
  14. package/dist/browser/browser-manager.d.ts.map +1 -1
  15. package/dist/browser/canvas-client.d.ts +54 -0
  16. package/dist/browser/canvas-client.d.ts.map +1 -0
  17. package/dist/browser/canvas-code-sync-manager.d.ts +87 -0
  18. package/dist/browser/canvas-code-sync-manager.d.ts.map +1 -0
  19. package/dist/browser/canvas-manager.d.ts +122 -0
  20. package/dist/browser/canvas-manager.d.ts.map +1 -0
  21. package/dist/browser/canvas-runtime-preview-bridge.d.ts +20 -0
  22. package/dist/browser/canvas-runtime-preview-bridge.d.ts.map +1 -0
  23. package/dist/browser/canvas-session-sync-manager.d.ts +21 -0
  24. package/dist/browser/canvas-session-sync-manager.d.ts.map +1 -0
  25. package/dist/browser/global-challenge-coordinator.d.ts +27 -0
  26. package/dist/browser/global-challenge-coordinator.d.ts.map +1 -0
  27. package/dist/browser/manager-types.d.ts +179 -1
  28. package/dist/browser/manager-types.d.ts.map +1 -1
  29. package/dist/browser/ops-browser-manager.d.ts +114 -4
  30. package/dist/browser/ops-browser-manager.d.ts.map +1 -1
  31. package/dist/browser/ops-client.d.ts +17 -1
  32. package/dist/browser/ops-client.d.ts.map +1 -1
  33. package/dist/browser/playwright-runtime.d.ts +4 -0
  34. package/dist/browser/playwright-runtime.d.ts.map +1 -0
  35. package/dist/browser/review-surface.d.ts +9 -0
  36. package/dist/browser/review-surface.d.ts.map +1 -0
  37. package/dist/browser/screencast-recorder.d.ts +57 -0
  38. package/dist/browser/screencast-recorder.d.ts.map +1 -0
  39. package/dist/browser/session-inspector.d.ts +71 -0
  40. package/dist/browser/session-inspector.d.ts.map +1 -0
  41. package/dist/browser/session-store.d.ts +5 -1
  42. package/dist/browser/session-store.d.ts.map +1 -1
  43. package/dist/browser/system-chrome-cookies.d.ts +46 -0
  44. package/dist/browser/system-chrome-cookies.d.ts.map +1 -0
  45. package/dist/browser/target-manager.d.ts +1 -0
  46. package/dist/browser/target-manager.d.ts.map +1 -1
  47. package/dist/cache/chrome-locator.d.ts.map +1 -1
  48. package/dist/cache/chrome-user-data.d.ts +17 -0
  49. package/dist/cache/chrome-user-data.d.ts.map +1 -0
  50. package/dist/canvas/adapter-plugins/loader.d.ts +13 -0
  51. package/dist/canvas/adapter-plugins/loader.d.ts.map +1 -0
  52. package/dist/canvas/adapter-plugins/manifest.d.ts +146 -0
  53. package/dist/canvas/adapter-plugins/manifest.d.ts.map +1 -0
  54. package/dist/canvas/adapter-plugins/types.d.ts +83 -0
  55. package/dist/canvas/adapter-plugins/types.d.ts.map +1 -0
  56. package/dist/canvas/adapter-plugins/validator.d.ts +10 -0
  57. package/dist/canvas/adapter-plugins/validator.d.ts.map +1 -0
  58. package/dist/canvas/code-sync/apply-tsx.d.ts +25 -0
  59. package/dist/canvas/code-sync/apply-tsx.d.ts.map +1 -0
  60. package/dist/canvas/code-sync/graph.d.ts +5 -0
  61. package/dist/canvas/code-sync/graph.d.ts.map +1 -0
  62. package/dist/canvas/code-sync/hash.d.ts +3 -0
  63. package/dist/canvas/code-sync/hash.d.ts.map +1 -0
  64. package/dist/canvas/code-sync/import.d.ts +19 -0
  65. package/dist/canvas/code-sync/import.d.ts.map +1 -0
  66. package/dist/canvas/code-sync/manifest.d.ts +6 -0
  67. package/dist/canvas/code-sync/manifest.d.ts.map +1 -0
  68. package/dist/canvas/code-sync/tsx-adapter.d.ts +8 -0
  69. package/dist/canvas/code-sync/tsx-adapter.d.ts.map +1 -0
  70. package/dist/canvas/code-sync/types.d.ts +244 -0
  71. package/dist/canvas/code-sync/types.d.ts.map +1 -0
  72. package/dist/canvas/code-sync/write.d.ts +9 -0
  73. package/dist/canvas/code-sync/write.d.ts.map +1 -0
  74. package/dist/canvas/document-store.d.ts +91 -0
  75. package/dist/canvas/document-store.d.ts.map +1 -0
  76. package/dist/canvas/export.d.ts +12 -0
  77. package/dist/canvas/export.d.ts.map +1 -0
  78. package/dist/canvas/framework-adapters/custom-elements-v1.d.ts +3 -0
  79. package/dist/canvas/framework-adapters/custom-elements-v1.d.ts.map +1 -0
  80. package/dist/canvas/framework-adapters/html-static-v1.d.ts +3 -0
  81. package/dist/canvas/framework-adapters/html-static-v1.d.ts.map +1 -0
  82. package/dist/canvas/framework-adapters/markup.d.ts +9 -0
  83. package/dist/canvas/framework-adapters/markup.d.ts.map +1 -0
  84. package/dist/canvas/framework-adapters/react-tsx-v2.d.ts +3 -0
  85. package/dist/canvas/framework-adapters/react-tsx-v2.d.ts.map +1 -0
  86. package/dist/canvas/framework-adapters/registry.d.ts +12 -0
  87. package/dist/canvas/framework-adapters/registry.d.ts.map +1 -0
  88. package/dist/canvas/framework-adapters/svelte-sfc-v1.d.ts +3 -0
  89. package/dist/canvas/framework-adapters/svelte-sfc-v1.d.ts.map +1 -0
  90. package/dist/canvas/framework-adapters/types.d.ts +57 -0
  91. package/dist/canvas/framework-adapters/types.d.ts.map +1 -0
  92. package/dist/canvas/framework-adapters/vue-sfc-v1.d.ts +3 -0
  93. package/dist/canvas/framework-adapters/vue-sfc-v1.d.ts.map +1 -0
  94. package/dist/canvas/kits/catalog.d.ts +5 -0
  95. package/dist/canvas/kits/catalog.d.ts.map +1 -0
  96. package/dist/canvas/library-adapters/react/index.d.ts +3 -0
  97. package/dist/canvas/library-adapters/react/index.d.ts.map +1 -0
  98. package/dist/canvas/library-adapters/registry.d.ts +11 -0
  99. package/dist/canvas/library-adapters/registry.d.ts.map +1 -0
  100. package/dist/canvas/library-adapters/types.d.ts +43 -0
  101. package/dist/canvas/library-adapters/types.d.ts.map +1 -0
  102. package/dist/canvas/repo-store.d.ts +12 -0
  103. package/dist/canvas/repo-store.d.ts.map +1 -0
  104. package/dist/canvas/starters/catalog.d.ts +34 -0
  105. package/dist/canvas/starters/catalog.d.ts.map +1 -0
  106. package/dist/canvas/surface-palette.d.ts +15 -0
  107. package/dist/canvas/surface-palette.d.ts.map +1 -0
  108. package/dist/canvas/token-references.d.ts +22 -0
  109. package/dist/canvas/token-references.d.ts.map +1 -0
  110. package/dist/canvas/types.d.ts +594 -0
  111. package/dist/canvas/types.d.ts.map +1 -0
  112. package/dist/canvas-runtime-preview-bridge-HBEHXM4T.js +7 -0
  113. package/dist/canvas-runtime-preview-bridge-HBEHXM4T.js.map +1 -0
  114. package/dist/challenges/action-loop.d.ts +13 -0
  115. package/dist/challenges/action-loop.d.ts.map +1 -0
  116. package/dist/challenges/capability-matrix.d.ts +3 -0
  117. package/dist/challenges/capability-matrix.d.ts.map +1 -0
  118. package/dist/challenges/evidence-bundle.d.ts +48 -0
  119. package/dist/challenges/evidence-bundle.d.ts.map +1 -0
  120. package/dist/challenges/governed-adapter-gateway.d.ts +4 -0
  121. package/dist/challenges/governed-adapter-gateway.d.ts.map +1 -0
  122. package/dist/challenges/human-yield-gate.d.ts +20 -0
  123. package/dist/challenges/human-yield-gate.d.ts.map +1 -0
  124. package/dist/challenges/index.d.ts +15 -0
  125. package/dist/challenges/index.d.ts.map +1 -0
  126. package/dist/challenges/interpreter.d.ts +3 -0
  127. package/dist/challenges/interpreter.d.ts.map +1 -0
  128. package/dist/challenges/optional-computer-use-bridge.d.ts +9 -0
  129. package/dist/challenges/optional-computer-use-bridge.d.ts.map +1 -0
  130. package/dist/challenges/orchestrator.d.ts +32 -0
  131. package/dist/challenges/orchestrator.d.ts.map +1 -0
  132. package/dist/challenges/outcome-recorder.d.ts +8 -0
  133. package/dist/challenges/outcome-recorder.d.ts.map +1 -0
  134. package/dist/challenges/owned-environment-lane.d.ts +3 -0
  135. package/dist/challenges/owned-environment-lane.d.ts.map +1 -0
  136. package/dist/challenges/policy-gate.d.ts +9 -0
  137. package/dist/challenges/policy-gate.d.ts.map +1 -0
  138. package/dist/challenges/sanctioned-identity-lane.d.ts +3 -0
  139. package/dist/challenges/sanctioned-identity-lane.d.ts.map +1 -0
  140. package/dist/challenges/service-adapter-lane.d.ts +3 -0
  141. package/dist/challenges/service-adapter-lane.d.ts.map +1 -0
  142. package/dist/challenges/strategy-selector.d.ts +10 -0
  143. package/dist/challenges/strategy-selector.d.ts.map +1 -0
  144. package/dist/challenges/types.d.ts +277 -0
  145. package/dist/challenges/types.d.ts.map +1 -0
  146. package/dist/challenges/verification-gate.d.ts +15 -0
  147. package/dist/challenges/verification-gate.d.ts.map +1 -0
  148. package/dist/chunk-5FZQJRBQ.js +15256 -0
  149. package/dist/chunk-5FZQJRBQ.js.map +1 -0
  150. package/dist/{chunk-7W3SPXIB.js → chunk-FUSXMW3G.js} +4 -1
  151. package/dist/chunk-L57D35TB.js +33513 -0
  152. package/dist/chunk-L57D35TB.js.map +1 -0
  153. package/dist/chunk-TBUCZX4A.js +34 -0
  154. package/dist/chunk-TBUCZX4A.js.map +1 -0
  155. package/dist/chunk-Y2KL55OG.js +59 -0
  156. package/dist/chunk-Y2KL55OG.js.map +1 -0
  157. package/dist/chunk-YBQECXZX.js +409 -0
  158. package/dist/chunk-YBQECXZX.js.map +1 -0
  159. package/dist/cli/args.d.ts +4 -4
  160. package/dist/cli/args.d.ts.map +1 -1
  161. package/dist/cli/commands/annotate.d.ts +11 -0
  162. package/dist/cli/commands/annotate.d.ts.map +1 -1
  163. package/dist/cli/commands/artifacts.d.ts.map +1 -1
  164. package/dist/cli/commands/canvas.d.ts +45 -0
  165. package/dist/cli/commands/canvas.d.ts.map +1 -0
  166. package/dist/cli/commands/daemon.d.ts +7 -0
  167. package/dist/cli/commands/daemon.d.ts.map +1 -1
  168. package/dist/cli/commands/desktop/accessibility-snapshot.d.ts +3 -0
  169. package/dist/cli/commands/desktop/accessibility-snapshot.d.ts.map +1 -0
  170. package/dist/cli/commands/desktop/active-window.d.ts +3 -0
  171. package/dist/cli/commands/desktop/active-window.d.ts.map +1 -0
  172. package/dist/cli/commands/desktop/capture-desktop.d.ts +3 -0
  173. package/dist/cli/commands/desktop/capture-desktop.d.ts.map +1 -0
  174. package/dist/cli/commands/desktop/capture-window.d.ts +3 -0
  175. package/dist/cli/commands/desktop/capture-window.d.ts.map +1 -0
  176. package/dist/cli/commands/desktop/shared.d.ts +19 -0
  177. package/dist/cli/commands/desktop/shared.d.ts.map +1 -0
  178. package/dist/cli/commands/desktop/status.d.ts +3 -0
  179. package/dist/cli/commands/desktop/status.d.ts.map +1 -0
  180. package/dist/cli/commands/desktop/windows.d.ts +3 -0
  181. package/dist/cli/commands/desktop/windows.d.ts.map +1 -0
  182. package/dist/cli/commands/devtools/dialog.d.ts +19 -0
  183. package/dist/cli/commands/devtools/dialog.d.ts.map +1 -0
  184. package/dist/cli/commands/devtools/perf.d.ts.map +1 -1
  185. package/dist/cli/commands/devtools/screencast-start.d.ts +20 -0
  186. package/dist/cli/commands/devtools/screencast-start.d.ts.map +1 -0
  187. package/dist/cli/commands/devtools/screencast-stop.d.ts +17 -0
  188. package/dist/cli/commands/devtools/screencast-stop.d.ts.map +1 -0
  189. package/dist/cli/commands/devtools/screenshot.d.ts +3 -0
  190. package/dist/cli/commands/devtools/screenshot.d.ts.map +1 -1
  191. package/dist/cli/commands/dom/attr.d.ts.map +1 -1
  192. package/dist/cli/commands/dom/checked.d.ts.map +1 -1
  193. package/dist/cli/commands/dom/enabled.d.ts.map +1 -1
  194. package/dist/cli/commands/dom/html.d.ts.map +1 -1
  195. package/dist/cli/commands/dom/text.d.ts.map +1 -1
  196. package/dist/cli/commands/dom/value.d.ts.map +1 -1
  197. package/dist/cli/commands/dom/visible.d.ts.map +1 -1
  198. package/dist/cli/commands/export/clone-component.d.ts +9 -0
  199. package/dist/cli/commands/export/clone-component.d.ts.map +1 -1
  200. package/dist/cli/commands/export/clone-page.d.ts +8 -0
  201. package/dist/cli/commands/export/clone-page.d.ts.map +1 -1
  202. package/dist/cli/commands/interact/check.d.ts.map +1 -1
  203. package/dist/cli/commands/interact/click.d.ts.map +1 -1
  204. package/dist/cli/commands/interact/hover.d.ts.map +1 -1
  205. package/dist/cli/commands/interact/pointer-down.d.ts +7 -0
  206. package/dist/cli/commands/interact/pointer-down.d.ts.map +1 -0
  207. package/dist/cli/commands/interact/pointer-drag.d.ts +7 -0
  208. package/dist/cli/commands/interact/pointer-drag.d.ts.map +1 -0
  209. package/dist/cli/commands/interact/pointer-move.d.ts +7 -0
  210. package/dist/cli/commands/interact/pointer-move.d.ts.map +1 -0
  211. package/dist/cli/commands/interact/pointer-shared.d.ts +6 -0
  212. package/dist/cli/commands/interact/pointer-shared.d.ts.map +1 -0
  213. package/dist/cli/commands/interact/pointer-up.d.ts +7 -0
  214. package/dist/cli/commands/interact/pointer-up.d.ts.map +1 -0
  215. package/dist/cli/commands/interact/press.d.ts.map +1 -1
  216. package/dist/cli/commands/interact/scroll-into-view.d.ts.map +1 -1
  217. package/dist/cli/commands/interact/scroll.d.ts.map +1 -1
  218. package/dist/cli/commands/interact/select.d.ts.map +1 -1
  219. package/dist/cli/commands/interact/type.d.ts.map +1 -1
  220. package/dist/cli/commands/interact/uncheck.d.ts.map +1 -1
  221. package/dist/cli/commands/interact/upload.d.ts +18 -0
  222. package/dist/cli/commands/interact/upload.d.ts.map +1 -0
  223. package/dist/cli/commands/macro-resolve.d.ts +2 -0
  224. package/dist/cli/commands/macro-resolve.d.ts.map +1 -1
  225. package/dist/cli/commands/native.d.ts +22 -8
  226. package/dist/cli/commands/native.d.ts.map +1 -1
  227. package/dist/cli/commands/nav/goto.d.ts.map +1 -1
  228. package/dist/cli/commands/nav/review.d.ts +7 -0
  229. package/dist/cli/commands/nav/review.d.ts.map +1 -0
  230. package/dist/cli/commands/nav/snapshot.d.ts.map +1 -1
  231. package/dist/cli/commands/nav/wait.d.ts.map +1 -1
  232. package/dist/cli/commands/pages/open.d.ts.map +1 -1
  233. package/dist/cli/commands/product-video.d.ts +2 -0
  234. package/dist/cli/commands/product-video.d.ts.map +1 -1
  235. package/dist/cli/commands/research.d.ts +3 -0
  236. package/dist/cli/commands/research.d.ts.map +1 -1
  237. package/dist/cli/commands/run.d.ts +14 -0
  238. package/dist/cli/commands/run.d.ts.map +1 -1
  239. package/dist/cli/commands/serve.d.ts +1 -21
  240. package/dist/cli/commands/serve.d.ts.map +1 -1
  241. package/dist/cli/commands/session/connect.d.ts.map +1 -1
  242. package/dist/cli/commands/session/disconnect.d.ts.map +1 -1
  243. package/dist/cli/commands/session/inspector.d.ts +21 -0
  244. package/dist/cli/commands/session/inspector.d.ts.map +1 -0
  245. package/dist/cli/commands/session/launch.d.ts.map +1 -1
  246. package/dist/cli/commands/shopping.d.ts +5 -0
  247. package/dist/cli/commands/shopping.d.ts.map +1 -1
  248. package/dist/cli/commands/status.d.ts +2 -9
  249. package/dist/cli/commands/status.d.ts.map +1 -1
  250. package/dist/cli/commands/targets/new.d.ts.map +1 -1
  251. package/dist/cli/daemon-autostart.d.ts +11 -0
  252. package/dist/cli/daemon-autostart.d.ts.map +1 -1
  253. package/dist/cli/daemon-client.d.ts +3 -0
  254. package/dist/cli/daemon-client.d.ts.map +1 -1
  255. package/dist/cli/daemon-commands.d.ts.map +1 -1
  256. package/dist/cli/daemon-state.d.ts +16 -0
  257. package/dist/cli/daemon-state.d.ts.map +1 -1
  258. package/dist/cli/daemon-status.d.ts +7 -2
  259. package/dist/cli/daemon-status.d.ts.map +1 -1
  260. package/dist/cli/daemon.d.ts +1 -0
  261. package/dist/cli/daemon.d.ts.map +1 -1
  262. package/dist/cli/help.d.ts +19 -3
  263. package/dist/cli/help.d.ts.map +1 -1
  264. package/dist/cli/index.js +2927 -932
  265. package/dist/cli/index.js.map +1 -1
  266. package/dist/cli/install-autostart-output.d.ts +6 -0
  267. package/dist/cli/install-autostart-output.d.ts.map +1 -0
  268. package/dist/cli/install-autostart-reconciliation.d.ts +23 -0
  269. package/dist/cli/install-autostart-reconciliation.d.ts.map +1 -0
  270. package/dist/cli/installers/skills.d.ts +42 -6
  271. package/dist/cli/installers/skills.d.ts.map +1 -1
  272. package/dist/cli/output.d.ts +3 -0
  273. package/dist/cli/output.d.ts.map +1 -1
  274. package/dist/cli/remote-canvas-manager.d.ts +8 -0
  275. package/dist/cli/remote-canvas-manager.d.ts.map +1 -0
  276. package/dist/cli/remote-desktop-runtime.d.ts +15 -0
  277. package/dist/cli/remote-desktop-runtime.d.ts.map +1 -0
  278. package/dist/cli/remote-manager.d.ts +27 -3
  279. package/dist/cli/remote-manager.d.ts.map +1 -1
  280. package/dist/cli/remote-relay.d.ts +2 -0
  281. package/dist/cli/remote-relay.d.ts.map +1 -1
  282. package/dist/cli/transport-timeouts.d.ts +8 -0
  283. package/dist/cli/transport-timeouts.d.ts.map +1 -0
  284. package/dist/cli/utils/http.d.ts +9 -0
  285. package/dist/cli/utils/http.d.ts.map +1 -1
  286. package/dist/cli/utils/parse.d.ts +3 -0
  287. package/dist/cli/utils/parse.d.ts.map +1 -1
  288. package/dist/cli/utils/skills.d.ts +1 -2
  289. package/dist/cli/utils/skills.d.ts.map +1 -1
  290. package/dist/cli/utils/workflow-message.d.ts +2 -0
  291. package/dist/cli/utils/workflow-message.d.ts.map +1 -0
  292. package/dist/config.d.ts +47 -0
  293. package/dist/config.d.ts.map +1 -1
  294. package/dist/core/bootstrap.d.ts.map +1 -1
  295. package/dist/core/index.d.ts +1 -0
  296. package/dist/core/index.d.ts.map +1 -1
  297. package/dist/core/logging.d.ts +3 -1
  298. package/dist/core/logging.d.ts.map +1 -1
  299. package/dist/core/runtime-assemblies.d.ts +22 -0
  300. package/dist/core/runtime-assemblies.d.ts.map +1 -0
  301. package/dist/core/types.d.ts +17 -0
  302. package/dist/core/types.d.ts.map +1 -1
  303. package/dist/desktop/audit.d.ts +37 -0
  304. package/dist/desktop/audit.d.ts.map +1 -0
  305. package/dist/desktop/errors.d.ts +7 -0
  306. package/dist/desktop/errors.d.ts.map +1 -0
  307. package/dist/desktop/index.d.ts +6 -0
  308. package/dist/desktop/index.d.ts.map +1 -0
  309. package/dist/desktop/runtime.d.ts +26 -0
  310. package/dist/desktop/runtime.d.ts.map +1 -0
  311. package/dist/desktop/types.d.ts +76 -0
  312. package/dist/desktop/types.d.ts.map +1 -0
  313. package/dist/extension-extractor.d.ts +6 -0
  314. package/dist/extension-extractor.d.ts.map +1 -1
  315. package/dist/fs-UMRKOBNN.js +7 -0
  316. package/dist/fs-UMRKOBNN.js.map +1 -0
  317. package/dist/index.d.ts.map +1 -1
  318. package/dist/index.js +1221 -460
  319. package/dist/index.js.map +1 -1
  320. package/dist/integrations/figma/assets.d.ts +13 -0
  321. package/dist/integrations/figma/assets.d.ts.map +1 -0
  322. package/dist/integrations/figma/auth.d.ts +3 -0
  323. package/dist/integrations/figma/auth.d.ts.map +1 -0
  324. package/dist/integrations/figma/client.d.ts +42 -0
  325. package/dist/integrations/figma/client.d.ts.map +1 -0
  326. package/dist/integrations/figma/mappers.d.ts +23 -0
  327. package/dist/integrations/figma/mappers.d.ts.map +1 -0
  328. package/dist/integrations/figma/normalize.d.ts +99 -0
  329. package/dist/integrations/figma/normalize.d.ts.map +1 -0
  330. package/dist/integrations/figma/url.d.ts +17 -0
  331. package/dist/integrations/figma/url.d.ts.map +1 -0
  332. package/dist/integrations/figma/variables.d.ts +21 -0
  333. package/dist/integrations/figma/variables.d.ts.map +1 -0
  334. package/dist/macros/execute-runtime.d.ts +19 -0
  335. package/dist/macros/execute-runtime.d.ts.map +1 -0
  336. package/dist/macros/execute.d.ts +3 -1
  337. package/dist/macros/execute.d.ts.map +1 -1
  338. package/dist/{macros-NUBRM44Y.js → macros-ND2M7LWU.js} +2 -2
  339. package/dist/opendevbrowser.d.ts.map +1 -1
  340. package/dist/opendevbrowser.js +1221 -460
  341. package/dist/opendevbrowser.js.map +1 -1
  342. package/dist/providers/blocker.d.ts.map +1 -1
  343. package/dist/providers/browser-fallback.d.ts +30 -0
  344. package/dist/providers/browser-fallback.d.ts.map +1 -0
  345. package/dist/providers/constraint.d.ts +45 -0
  346. package/dist/providers/constraint.d.ts.map +1 -0
  347. package/dist/providers/index.d.ts +11 -2
  348. package/dist/providers/index.d.ts.map +1 -1
  349. package/dist/providers/policy.d.ts.map +1 -1
  350. package/dist/providers/product-video-compiler.d.ts +92 -0
  351. package/dist/providers/product-video-compiler.d.ts.map +1 -0
  352. package/dist/providers/registry.d.ts +37 -1
  353. package/dist/providers/registry.d.ts.map +1 -1
  354. package/dist/providers/renderer.d.ts.map +1 -1
  355. package/dist/providers/research-compiler.d.ts +64 -0
  356. package/dist/providers/research-compiler.d.ts.map +1 -0
  357. package/dist/providers/research-executor.d.ts +27 -0
  358. package/dist/providers/research-executor.d.ts.map +1 -0
  359. package/dist/providers/runtime-bundle.d.ts +26 -0
  360. package/dist/providers/runtime-bundle.d.ts.map +1 -0
  361. package/dist/providers/runtime-factory.d.ts +6 -1
  362. package/dist/providers/runtime-factory.d.ts.map +1 -1
  363. package/dist/providers/runtime-policy.d.ts +24 -0
  364. package/dist/providers/runtime-policy.d.ts.map +1 -0
  365. package/dist/providers/shared/anti-bot-policy.d.ts +3 -2
  366. package/dist/providers/shared/anti-bot-policy.d.ts.map +1 -1
  367. package/dist/providers/shopping/index.d.ts +11 -1
  368. package/dist/providers/shopping/index.d.ts.map +1 -1
  369. package/dist/providers/shopping-compiler.d.ts +51 -0
  370. package/dist/providers/shopping-compiler.d.ts.map +1 -0
  371. package/dist/providers/shopping-executor.d.ts +18 -0
  372. package/dist/providers/shopping-executor.d.ts.map +1 -0
  373. package/dist/providers/shopping-postprocess.d.ts +46 -0
  374. package/dist/providers/shopping-postprocess.d.ts.map +1 -0
  375. package/dist/providers/shopping-workflow.d.ts +33 -0
  376. package/dist/providers/shopping-workflow.d.ts.map +1 -0
  377. package/dist/providers/social/platform.d.ts +2 -1
  378. package/dist/providers/social/platform.d.ts.map +1 -1
  379. package/dist/providers/social/search-quality.d.ts +16 -0
  380. package/dist/providers/social/search-quality.d.ts.map +1 -0
  381. package/dist/providers/social/youtube-resolver.d.ts +2 -1
  382. package/dist/providers/social/youtube-resolver.d.ts.map +1 -1
  383. package/dist/providers/social/youtube.d.ts.map +1 -1
  384. package/dist/providers/types.d.ts +116 -4
  385. package/dist/providers/types.d.ts.map +1 -1
  386. package/dist/providers/web/crawl-worker.d.ts.map +1 -1
  387. package/dist/providers/web/extract.d.ts +16 -0
  388. package/dist/providers/web/extract.d.ts.map +1 -1
  389. package/dist/providers/web/index.d.ts.map +1 -1
  390. package/dist/providers/workflow-contracts.d.ts +53 -0
  391. package/dist/providers/workflow-contracts.d.ts.map +1 -0
  392. package/dist/providers/workflows.d.ts +30 -6
  393. package/dist/providers/workflows.d.ts.map +1 -1
  394. package/dist/providers-G36AM3Z2.js +121 -0
  395. package/dist/providers-G36AM3Z2.js.map +1 -0
  396. package/dist/public-surface/generated-manifest.d.ts +1168 -0
  397. package/dist/public-surface/generated-manifest.d.ts.map +1 -0
  398. package/dist/public-surface/source.d.ts +437 -0
  399. package/dist/public-surface/source.d.ts.map +1 -0
  400. package/dist/relay/protocol.d.ts +108 -4
  401. package/dist/relay/protocol.d.ts.map +1 -1
  402. package/dist/relay/relay-endpoints.d.ts +21 -0
  403. package/dist/relay/relay-endpoints.d.ts.map +1 -1
  404. package/dist/relay/relay-server.d.ts +32 -1
  405. package/dist/relay/relay-server.d.ts.map +1 -1
  406. package/dist/relay/relay-types.d.ts +3 -0
  407. package/dist/relay/relay-types.d.ts.map +1 -1
  408. package/dist/skills/bundled-skill-directories.d.ts +8 -0
  409. package/dist/skills/bundled-skill-directories.d.ts.map +1 -0
  410. package/dist/skills/skill-loader.d.ts +9 -1
  411. package/dist/skills/skill-loader.d.ts.map +1 -1
  412. package/dist/skills/skill-loader.js +7 -0
  413. package/dist/skills/skill-loader.js.map +1 -0
  414. package/dist/skills/skill-nudge.d.ts.map +1 -1
  415. package/dist/skills/types.d.ts +31 -0
  416. package/dist/skills/types.d.ts.map +1 -1
  417. package/dist/snapshot/ops-snapshot.d.ts +1 -1
  418. package/dist/snapshot/ops-snapshot.d.ts.map +1 -1
  419. package/dist/snapshot/refs.d.ts +6 -1
  420. package/dist/snapshot/refs.d.ts.map +1 -1
  421. package/dist/snapshot/snapshotter.d.ts.map +1 -1
  422. package/dist/tools/annotate.d.ts.map +1 -1
  423. package/dist/tools/canvas.d.ts +4 -0
  424. package/dist/tools/canvas.d.ts.map +1 -0
  425. package/dist/tools/check.d.ts.map +1 -1
  426. package/dist/tools/click.d.ts.map +1 -1
  427. package/dist/tools/clone_component.d.ts.map +1 -1
  428. package/dist/tools/clone_page.d.ts.map +1 -1
  429. package/dist/tools/connect.d.ts.map +1 -1
  430. package/dist/tools/deps.d.ts +6 -0
  431. package/dist/tools/deps.d.ts.map +1 -1
  432. package/dist/tools/desktop-shared.d.ts +6 -0
  433. package/dist/tools/desktop-shared.d.ts.map +1 -0
  434. package/dist/tools/desktop_accessibility_snapshot.d.ts +4 -0
  435. package/dist/tools/desktop_accessibility_snapshot.d.ts.map +1 -0
  436. package/dist/tools/desktop_active_window.d.ts +4 -0
  437. package/dist/tools/desktop_active_window.d.ts.map +1 -0
  438. package/dist/tools/desktop_capture_desktop.d.ts +4 -0
  439. package/dist/tools/desktop_capture_desktop.d.ts.map +1 -0
  440. package/dist/tools/desktop_capture_window.d.ts +4 -0
  441. package/dist/tools/desktop_capture_window.d.ts.map +1 -0
  442. package/dist/tools/desktop_status.d.ts +4 -0
  443. package/dist/tools/desktop_status.d.ts.map +1 -0
  444. package/dist/tools/desktop_windows.d.ts +4 -0
  445. package/dist/tools/desktop_windows.d.ts.map +1 -0
  446. package/dist/tools/dialog.d.ts +4 -0
  447. package/dist/tools/dialog.d.ts.map +1 -0
  448. package/dist/tools/dom_get_html.d.ts.map +1 -1
  449. package/dist/tools/dom_get_text.d.ts.map +1 -1
  450. package/dist/tools/get_attr.d.ts.map +1 -1
  451. package/dist/tools/get_value.d.ts.map +1 -1
  452. package/dist/tools/goto.d.ts.map +1 -1
  453. package/dist/tools/hover.d.ts.map +1 -1
  454. package/dist/tools/index.d.ts +3 -0
  455. package/dist/tools/index.d.ts.map +1 -1
  456. package/dist/tools/is_checked.d.ts.map +1 -1
  457. package/dist/tools/is_enabled.d.ts.map +1 -1
  458. package/dist/tools/is_visible.d.ts.map +1 -1
  459. package/dist/tools/launch.d.ts.map +1 -1
  460. package/dist/tools/macro_resolve.d.ts.map +1 -1
  461. package/dist/tools/perf.d.ts.map +1 -1
  462. package/dist/tools/pointer_down.d.ts +4 -0
  463. package/dist/tools/pointer_down.d.ts.map +1 -0
  464. package/dist/tools/pointer_drag.d.ts +4 -0
  465. package/dist/tools/pointer_drag.d.ts.map +1 -0
  466. package/dist/tools/pointer_move.d.ts +4 -0
  467. package/dist/tools/pointer_move.d.ts.map +1 -0
  468. package/dist/tools/pointer_up.d.ts +4 -0
  469. package/dist/tools/pointer_up.d.ts.map +1 -0
  470. package/dist/tools/press.d.ts.map +1 -1
  471. package/dist/tools/product_video_run.d.ts.map +1 -1
  472. package/dist/tools/prompting_guide.d.ts.map +1 -1
  473. package/dist/tools/research_run.d.ts.map +1 -1
  474. package/dist/tools/response.d.ts +4 -1
  475. package/dist/tools/response.d.ts.map +1 -1
  476. package/dist/tools/review.d.ts +4 -0
  477. package/dist/tools/review.d.ts.map +1 -0
  478. package/dist/tools/screencast_start.d.ts +4 -0
  479. package/dist/tools/screencast_start.d.ts.map +1 -0
  480. package/dist/tools/screencast_stop.d.ts +4 -0
  481. package/dist/tools/screencast_stop.d.ts.map +1 -0
  482. package/dist/tools/screenshot.d.ts.map +1 -1
  483. package/dist/tools/scroll.d.ts.map +1 -1
  484. package/dist/tools/scroll_into_view.d.ts.map +1 -1
  485. package/dist/tools/select.d.ts.map +1 -1
  486. package/dist/tools/session_inspector.d.ts +4 -0
  487. package/dist/tools/session_inspector.d.ts.map +1 -0
  488. package/dist/tools/shopping_run.d.ts.map +1 -1
  489. package/dist/tools/skill_list.d.ts.map +1 -1
  490. package/dist/tools/skill_load.d.ts.map +1 -1
  491. package/dist/tools/snapshot.d.ts.map +1 -1
  492. package/dist/tools/type.d.ts.map +1 -1
  493. package/dist/tools/uncheck.d.ts.map +1 -1
  494. package/dist/tools/upload.d.ts +4 -0
  495. package/dist/tools/upload.d.ts.map +1 -0
  496. package/dist/tools/wait.d.ts.map +1 -1
  497. package/dist/tools/workflow-runtime.d.ts +4 -2
  498. package/dist/tools/workflow-runtime.d.ts.map +1 -1
  499. package/dist/utils/package-assets.d.ts +4 -0
  500. package/dist/utils/package-assets.d.ts.map +1 -0
  501. package/extension/canvas.html +1006 -0
  502. package/extension/dist/annotate-content.css +15 -6
  503. package/extension/dist/annotate-content.js +175 -35
  504. package/extension/dist/annotation-payload.js +199 -0
  505. package/extension/dist/background.js +544 -69
  506. package/extension/dist/canvas/canvas-runtime.js +1490 -0
  507. package/extension/dist/canvas/model.js +341 -0
  508. package/extension/dist/canvas/viewport-fit.js +67 -0
  509. package/extension/dist/canvas-page.js +3609 -0
  510. package/extension/dist/ops/dom-bridge.js +255 -3
  511. package/extension/dist/ops/ops-runtime.js +3324 -301
  512. package/extension/dist/ops/ops-session-store.js +97 -112
  513. package/extension/dist/ops/snapshot-builder.js +2 -2
  514. package/extension/dist/ops/snapshot-shared.js +2 -2
  515. package/extension/dist/ops/target-session-coordinator.js +159 -0
  516. package/extension/dist/popup.js +201 -42
  517. package/extension/dist/services/CDPRouter.js +1567 -63
  518. package/extension/dist/services/ConnectionManager.js +453 -78
  519. package/extension/dist/services/RelayClient.js +79 -30
  520. package/extension/dist/services/TabManager.js +118 -22
  521. package/extension/dist/services/TargetSessionMap.js +127 -3
  522. package/extension/dist/services/attach-errors.js +20 -0
  523. package/extension/dist/services/cdp-router-commands.js +135 -8
  524. package/extension/dist/services/url-restrictions.js +9 -13
  525. package/extension/dist/types.js +2 -0
  526. package/extension/manifest.json +2 -2
  527. package/extension/popup.html +59 -6
  528. package/package.json +19 -9
  529. package/skills/AGENTS.md +8 -4
  530. package/skills/opendevbrowser-best-practices/SKILL.md +183 -6
  531. package/skills/opendevbrowser-best-practices/artifacts/browser-agent-known-issues-matrix.md +1 -0
  532. package/skills/opendevbrowser-best-practices/artifacts/canvas-governance-playbook.md +141 -0
  533. package/skills/opendevbrowser-best-practices/artifacts/command-channel-reference.md +129 -19
  534. package/skills/opendevbrowser-best-practices/artifacts/parity-gates.md +9 -2
  535. package/skills/opendevbrowser-best-practices/artifacts/provider-workflows.md +6 -0
  536. package/skills/opendevbrowser-best-practices/artifacts/skill-runtime-surface-matrix.md +58 -0
  537. package/skills/opendevbrowser-best-practices/assets/templates/canvas-blocker-checklist.json +70 -0
  538. package/skills/opendevbrowser-best-practices/assets/templates/canvas-feedback-eval.json +73 -0
  539. package/skills/opendevbrowser-best-practices/assets/templates/canvas-generation-plan.v1.json +67 -0
  540. package/skills/opendevbrowser-best-practices/assets/templates/canvas-handshake-example.json +126 -0
  541. package/skills/opendevbrowser-best-practices/assets/templates/robustness-checklist.json +57 -0
  542. package/skills/opendevbrowser-best-practices/assets/templates/skill-runtime-pack-matrix.json +674 -0
  543. package/skills/opendevbrowser-best-practices/assets/templates/surface-audit-checklist.json +12 -3
  544. package/skills/opendevbrowser-best-practices/scripts/odb-workflow.sh +107 -12
  545. package/skills/opendevbrowser-best-practices/scripts/resolve-odb-cli.sh +100 -0
  546. package/skills/opendevbrowser-best-practices/scripts/run-robustness-audit.sh +83 -1
  547. package/skills/opendevbrowser-best-practices/scripts/validate-skill-assets.sh +365 -84
  548. package/skills/opendevbrowser-best-practices/scripts/validator-fixture-cli.sh +208 -0
  549. package/skills/opendevbrowser-continuity-ledger/SKILL.md +14 -1
  550. package/skills/opendevbrowser-continuity-ledger/scripts/validate-skill-assets.sh +61 -0
  551. package/skills/opendevbrowser-data-extraction/SKILL.md +6 -0
  552. package/skills/opendevbrowser-data-extraction/scripts/validate-skill-assets.sh +112 -0
  553. package/skills/opendevbrowser-design-agent/SKILL.md +275 -0
  554. package/skills/opendevbrowser-design-agent/artifacts/app-shell-and-state-wiring.md +84 -0
  555. package/skills/opendevbrowser-design-agent/artifacts/async-search-state-ownership.md +58 -0
  556. package/skills/opendevbrowser-design-agent/artifacts/component-pattern-index.md +130 -0
  557. package/skills/opendevbrowser-design-agent/artifacts/design-contract-playbook.md +157 -0
  558. package/skills/opendevbrowser-design-agent/artifacts/design-release-gate.md +40 -0
  559. package/skills/opendevbrowser-design-agent/artifacts/design-workflows.md +153 -0
  560. package/skills/opendevbrowser-design-agent/artifacts/existing-surface-adaptation.md +56 -0
  561. package/skills/opendevbrowser-design-agent/artifacts/external-pattern-synthesis.md +103 -0
  562. package/skills/opendevbrowser-design-agent/artifacts/frontend-evaluation-rubric.md +61 -0
  563. package/skills/opendevbrowser-design-agent/artifacts/implementation-anti-patterns.md +163 -0
  564. package/skills/opendevbrowser-design-agent/artifacts/isolated-preview-validation.md +68 -0
  565. package/skills/opendevbrowser-design-agent/artifacts/loading-and-feedback-surfaces.md +56 -0
  566. package/skills/opendevbrowser-design-agent/artifacts/opendevbrowser-ui-example-map.md +44 -0
  567. package/skills/opendevbrowser-design-agent/artifacts/performance-audit-playbook.md +70 -0
  568. package/skills/opendevbrowser-design-agent/artifacts/research-harvest-workflow.md +81 -0
  569. package/skills/opendevbrowser-design-agent/artifacts/scroll-reveal-surface-planning.md +64 -0
  570. package/skills/opendevbrowser-design-agent/artifacts/state-ownership-matrix.md +36 -0
  571. package/skills/opendevbrowser-design-agent/artifacts/theming-and-token-ownership.md +43 -0
  572. package/skills/opendevbrowser-design-agent/assets/templates/canvas-generation-plan.design.v1.json +58 -0
  573. package/skills/opendevbrowser-design-agent/assets/templates/design-audit-report.v1.md +34 -0
  574. package/skills/opendevbrowser-design-agent/assets/templates/design-brief.v1.md +40 -0
  575. package/skills/opendevbrowser-design-agent/assets/templates/design-contract.v1.json +226 -0
  576. package/skills/opendevbrowser-design-agent/assets/templates/design-release-gate.v1.json +35 -0
  577. package/skills/opendevbrowser-design-agent/assets/templates/design-review-checklist.json +57 -0
  578. package/skills/opendevbrowser-design-agent/assets/templates/real-surface-design-matrix.json +32 -0
  579. package/skills/opendevbrowser-design-agent/assets/templates/reference-pattern-board.v1.json +31 -0
  580. package/skills/opendevbrowser-design-agent/scripts/design-workflow.sh +171 -0
  581. package/skills/opendevbrowser-design-agent/scripts/extract-canvas-plan.sh +56 -0
  582. package/skills/opendevbrowser-design-agent/scripts/validate-skill-assets.sh +223 -0
  583. package/skills/opendevbrowser-form-testing/SKILL.md +19 -3
  584. package/skills/opendevbrowser-form-testing/artifacts/form-workflows.md +5 -4
  585. package/skills/opendevbrowser-form-testing/assets/templates/challenge-decision-tree.json +2 -0
  586. package/skills/opendevbrowser-form-testing/scripts/validate-skill-assets.sh +109 -0
  587. package/skills/opendevbrowser-login-automation/SKILL.md +21 -3
  588. package/skills/opendevbrowser-login-automation/artifacts/login-workflows.md +5 -4
  589. package/skills/opendevbrowser-login-automation/assets/templates/auth-signals.json +5 -0
  590. package/skills/opendevbrowser-login-automation/assets/templates/login-scenario-matrix.json +3 -2
  591. package/skills/opendevbrowser-login-automation/scripts/run-login-workflow.sh +17 -1
  592. package/skills/opendevbrowser-login-automation/scripts/validate-skill-assets.sh +133 -0
  593. package/skills/opendevbrowser-product-presentation-asset/SKILL.md +23 -11
  594. package/skills/opendevbrowser-product-presentation-asset/artifacts/asset-pack-assembly.md +5 -3
  595. package/skills/opendevbrowser-product-presentation-asset/assets/templates/shot-list.md +2 -0
  596. package/skills/opendevbrowser-product-presentation-asset/assets/templates/video-assembly.md +3 -2
  597. package/skills/opendevbrowser-product-presentation-asset/scripts/capture-screenshots.sh +5 -1
  598. package/skills/opendevbrowser-product-presentation-asset/scripts/collect-product.sh +6 -2
  599. package/skills/opendevbrowser-product-presentation-asset/scripts/download-images.sh +5 -1
  600. package/skills/opendevbrowser-product-presentation-asset/scripts/render-video-brief.sh +20 -7
  601. package/skills/opendevbrowser-product-presentation-asset/scripts/validate-skill-assets.sh +39 -0
  602. package/skills/opendevbrowser-product-presentation-asset/scripts/write-manifest.sh +5 -1
  603. package/skills/opendevbrowser-research/SKILL.md +14 -6
  604. package/skills/opendevbrowser-research/scripts/render-output.sh +5 -1
  605. package/skills/opendevbrowser-research/scripts/run-research.sh +5 -1
  606. package/skills/opendevbrowser-research/scripts/validate-skill-assets.sh +45 -0
  607. package/skills/opendevbrowser-research/scripts/write-artifacts.sh +5 -1
  608. package/skills/opendevbrowser-shopping/SKILL.md +20 -1
  609. package/skills/opendevbrowser-shopping/scripts/normalize-offers.sh +6 -2
  610. package/skills/opendevbrowser-shopping/scripts/run-deal-hunt.sh +5 -1
  611. package/skills/opendevbrowser-shopping/scripts/run-shopping.sh +5 -1
  612. package/skills/opendevbrowser-shopping/scripts/validate-skill-assets.sh +54 -0
  613. package/dist/chunk-ST7CO5FA.js +0 -18668
  614. package/dist/chunk-ST7CO5FA.js.map +0 -1
  615. /package/dist/{chunk-7W3SPXIB.js.map → chunk-FUSXMW3G.js.map} +0 -0
  616. /package/dist/{macros-NUBRM44Y.js.map → macros-ND2M7LWU.js.map} +0 -0
@@ -1,4 +1,5 @@
1
1
  import { MAX_OPS_PAYLOAD_BYTES, MAX_SNAPSHOT_BYTES, OPS_PROTOCOL_VERSION } from "../types.js";
2
+ import { isAttachBlockedError } from "../services/attach-errors.js";
2
3
  import { TabManager } from "../services/TabManager.js";
3
4
  import { getRestrictionMessage, isRestrictedUrl } from "../services/url-restrictions.js";
4
5
  import { logError } from "../logging.js";
@@ -12,12 +13,195 @@ const MAX_NETWORK_EVENTS = 300;
12
13
  const SESSION_TTL_MS = 20_000;
13
14
  const SCREENSHOT_TIMEOUT_MS = 8000;
14
15
  const TAB_CLOSE_TIMEOUT_MS = 5000;
16
+ const POPUP_ATTACH_RETRY_DELAY_MS = 100;
17
+ const STALE_REF_ERROR_SUFFIX = "Take a new snapshot first.";
18
+ const DOM_OUTER_HTML_DECLARATION = `
19
+ function() {
20
+ if (!(this instanceof Element)) return "";
21
+ return this.outerHTML;
22
+ }
23
+ `;
24
+ const DOM_INNER_TEXT_DECLARATION = `
25
+ function() {
26
+ if (!(this instanceof Element)) return "";
27
+ return this instanceof HTMLElement ? (this.innerText || this.textContent || "") : (this.textContent || "");
28
+ }
29
+ `;
30
+ const DOM_GET_ATTR_DECLARATION = `
31
+ function(name) {
32
+ if (!(this instanceof Element)) return null;
33
+ const value = this.getAttribute(name);
34
+ return value === null ? null : String(value);
35
+ }
36
+ `;
37
+ const DOM_GET_VALUE_DECLARATION = `
38
+ function() {
39
+ if (this instanceof HTMLInputElement || this instanceof HTMLTextAreaElement || this instanceof HTMLSelectElement) {
40
+ return this.value;
41
+ }
42
+ if (!(this instanceof Element)) return null;
43
+ const value = this.getAttribute("value");
44
+ return value === null ? null : String(value);
45
+ }
46
+ `;
47
+ const DOM_IS_VISIBLE_DECLARATION = `
48
+ function() {
49
+ if (!(this instanceof Element)) return false;
50
+ const style = window.getComputedStyle(this);
51
+ if (!style || style.display === "none" || style.visibility === "hidden" || style.opacity === "0") {
52
+ return false;
53
+ }
54
+ const rect = this.getBoundingClientRect();
55
+ return rect.width > 0 && rect.height > 0;
56
+ }
57
+ `;
58
+ const DOM_IS_ENABLED_DECLARATION = `
59
+ function() {
60
+ if (!(this instanceof Element)) return false;
61
+ return !this.hasAttribute("disabled") && this.getAttribute("aria-disabled") !== "true";
62
+ }
63
+ `;
64
+ const DOM_IS_CHECKED_DECLARATION = `
65
+ function() {
66
+ if (this instanceof HTMLInputElement && (this.type === "checkbox" || this.type === "radio")) {
67
+ return this.checked;
68
+ }
69
+ if (!(this instanceof Element)) return false;
70
+ return this.getAttribute("aria-checked") === "true";
71
+ }
72
+ `;
73
+ const DOM_SELECTOR_STATE_DECLARATION = `
74
+ function() {
75
+ if (!(this instanceof Element)) {
76
+ return { attached: false, visible: false };
77
+ }
78
+ const style = window.getComputedStyle(this);
79
+ const rect = this.getBoundingClientRect();
80
+ return {
81
+ attached: true,
82
+ visible: Boolean(style && style.display !== "none" && style.visibility !== "hidden" && style.opacity !== "0" && rect.width > 0 && rect.height > 0)
83
+ };
84
+ }
85
+ `;
86
+ const DOM_HOVER_DECLARATION = `
87
+ function() {
88
+ if (!(this instanceof Element)) return;
89
+ const init = { bubbles: true, cancelable: true, view: window };
90
+ this.dispatchEvent(new MouseEvent("mouseenter", init));
91
+ this.dispatchEvent(new MouseEvent("mouseover", init));
92
+ this.dispatchEvent(new MouseEvent("mousemove", init));
93
+ }
94
+ `;
95
+ const DOM_FOCUS_DECLARATION = `
96
+ function() {
97
+ if (this instanceof HTMLElement) {
98
+ this.focus();
99
+ }
100
+ }
101
+ `;
102
+ const DOM_SET_CHECKED_DECLARATION = `
103
+ function(checked) {
104
+ if (this instanceof HTMLInputElement && (this.type === "checkbox" || this.type === "radio")) {
105
+ this.checked = Boolean(checked);
106
+ this.dispatchEvent(new Event("input", { bubbles: true }));
107
+ this.dispatchEvent(new Event("change", { bubbles: true }));
108
+ return;
109
+ }
110
+ if (this instanceof Element) {
111
+ this.setAttribute("aria-checked", checked ? "true" : "false");
112
+ }
113
+ }
114
+ `;
115
+ const DOM_TYPE_DECLARATION = `
116
+ function(value, clear, submit) {
117
+ if (!(this instanceof Element)) return;
118
+ if (this instanceof HTMLElement) {
119
+ this.focus();
120
+ }
121
+ if (this instanceof HTMLInputElement || this instanceof HTMLTextAreaElement) {
122
+ this.value = clear ? "" : this.value;
123
+ this.value = String(value);
124
+ this.dispatchEvent(new Event("input", { bubbles: true }));
125
+ this.dispatchEvent(new Event("change", { bubbles: true }));
126
+ if (submit) {
127
+ this.form?.requestSubmit?.();
128
+ }
129
+ return;
130
+ }
131
+ if (this instanceof HTMLSelectElement) {
132
+ this.value = String(value);
133
+ this.dispatchEvent(new Event("input", { bubbles: true }));
134
+ this.dispatchEvent(new Event("change", { bubbles: true }));
135
+ }
136
+ }
137
+ `;
138
+ const DOM_SELECT_DECLARATION = `
139
+ function(values) {
140
+ if (!(this instanceof HTMLSelectElement)) return;
141
+ const nextValues = Array.isArray(values) ? values.map((value) => String(value)) : [];
142
+ for (const option of Array.from(this.options)) {
143
+ option.selected = nextValues.includes(option.value);
144
+ }
145
+ this.dispatchEvent(new Event("input", { bubbles: true }));
146
+ this.dispatchEvent(new Event("change", { bubbles: true }));
147
+ }
148
+ `;
149
+ const DOM_SCROLL_BY_DECLARATION = `
150
+ function(dy) {
151
+ if (!(this instanceof HTMLElement)) return;
152
+ this.scrollBy(0, Number(dy) || 0);
153
+ }
154
+ `;
155
+ const DOM_SCROLL_INTO_VIEW_DECLARATION = `
156
+ function() {
157
+ if (this instanceof Element) {
158
+ this.scrollIntoView({ block: "center", inline: "center", behavior: "auto" });
159
+ }
160
+ }
161
+ `;
162
+ const DOM_REF_POINT_DECLARATION = `
163
+ function() {
164
+ if (!(this instanceof Element)) return null;
165
+ const rect = this.getBoundingClientRect();
166
+ return {
167
+ x: rect.left + rect.width / 2,
168
+ y: rect.top + rect.height / 2
169
+ };
170
+ }
171
+ `;
172
+ const DOM_SCREENSHOT_CLIP_DECLARATION = `
173
+ function() {
174
+ /* odb-dom-screenshot-clip */
175
+ if (!(this instanceof Element)) return null;
176
+ const rect = this.getBoundingClientRect();
177
+ if (!Number.isFinite(rect.width) || !Number.isFinite(rect.height) || rect.width <= 0 || rect.height <= 0) {
178
+ return null;
179
+ }
180
+ return {
181
+ x: rect.left + window.scrollX,
182
+ y: rect.top + window.scrollY,
183
+ width: rect.width,
184
+ height: rect.height
185
+ };
186
+ }
187
+ `;
188
+ const DOM_FILE_INPUT_INFO_DECLARATION = `
189
+ function() {
190
+ /* odb-dom-file-input-info */
191
+ const isFileInput = this instanceof HTMLInputElement && this.type === "file";
192
+ return {
193
+ isFileInput,
194
+ disabled: isFileInput ? this.disabled : false
195
+ };
196
+ }
197
+ `;
15
198
  const TARGET_SCOPED_COMMANDS = new Set([
16
199
  "storage.setCookies",
17
200
  "storage.getCookies",
18
201
  "nav.goto",
19
202
  "nav.wait",
20
203
  "nav.snapshot",
204
+ "nav.review",
21
205
  "interact.click",
22
206
  "interact.hover",
23
207
  "interact.press",
@@ -27,6 +211,11 @@ const TARGET_SCOPED_COMMANDS = new Set([
27
211
  "interact.select",
28
212
  "interact.scroll",
29
213
  "interact.scrollIntoView",
214
+ "interact.upload",
215
+ "pointer.move",
216
+ "pointer.down",
217
+ "pointer.up",
218
+ "pointer.drag",
30
219
  "dom.getHtml",
31
220
  "dom.getText",
32
221
  "dom.getAttr",
@@ -34,27 +223,63 @@ const TARGET_SCOPED_COMMANDS = new Set([
34
223
  "dom.isVisible",
35
224
  "dom.isEnabled",
36
225
  "dom.isChecked",
226
+ "dom.refPoint",
227
+ "canvas.overlay.mount",
228
+ "canvas.overlay.unmount",
229
+ "canvas.overlay.select",
230
+ "canvas.overlay.sync",
231
+ "canvas.applyRuntimePreviewBridge",
37
232
  "export.clonePage",
38
233
  "export.cloneComponent",
39
234
  "devtools.perf",
40
235
  "page.screenshot"
41
236
  ]);
237
+ const DIALOG_SCOPED_COMMANDS = new Set([
238
+ "page.dialog"
239
+ ]);
42
240
  export class OpsRuntime {
43
241
  sendEnvelope;
44
242
  cdp;
243
+ getCanvasPageState;
244
+ performCanvasPageAction;
45
245
  tabs = new TabManager();
46
246
  dom = new DomBridge();
47
247
  sessions = new OpsSessionStore();
48
248
  encoder = new TextEncoder();
249
+ popupOpenerTabIds = new Map();
250
+ popupAttachDiagnostics = new Map();
251
+ commandCreatedTabs = new Map();
252
+ dialogQueues = new Map();
49
253
  closingTimers = new Map();
50
254
  parallelWaiters = new Map();
51
255
  constructor(options) {
52
256
  this.sendEnvelope = options.send;
53
257
  this.cdp = options.cdp;
258
+ this.getCanvasPageState = options.getCanvasPageState;
259
+ this.performCanvasPageAction = options.performCanvasPageAction;
260
+ chrome.tabs.onCreated.addListener(this.handleTabCreated);
54
261
  chrome.tabs.onRemoved.addListener(this.handleTabRemoved);
55
262
  chrome.tabs.onUpdated.addListener(this.handleTabUpdated);
263
+ chrome.webNavigation?.onCreatedNavigationTarget?.addListener?.(this.handleCreatedNavigationTarget);
56
264
  chrome.debugger.onEvent.addListener(this.handleDebuggerEvent);
57
265
  chrome.debugger.onDetach.addListener(this.handleDebuggerDetach);
266
+ if (typeof this.cdp.addEventListener === "function") {
267
+ this.cdp.addEventListener(this.handleCdpRouterEvent);
268
+ }
269
+ }
270
+ async registerCanvasTargetForSession(opsSessionId, targetId) {
271
+ const session = this.sessions.get(opsSessionId);
272
+ if (!session) {
273
+ return null;
274
+ }
275
+ return await this.registerCanvasTarget(session, targetId);
276
+ }
277
+ unregisterCanvasTargetForSession(opsSessionId, targetId) {
278
+ const session = this.sessions.get(opsSessionId);
279
+ if (!session || targetId === session.targetId) {
280
+ return false;
281
+ }
282
+ return this.sessions.removeTarget(session.id, targetId) !== null;
58
283
  }
59
284
  handleMessage(message) {
60
285
  if (message.type === "ops_hello") {
@@ -117,18 +342,125 @@ export class OpsRuntime {
117
342
  const clientId = message.clientId;
118
343
  if (!clientId)
119
344
  return;
345
+ this.cdp.markClientClosed();
120
346
  const sessions = this.sessions.listOwnedBy(clientId);
121
347
  for (const session of sessions) {
122
- this.markSessionClosing(session, "ops_session_expired");
348
+ if (this.markSessionClosing(session, "ops_session_expired")) {
349
+ this.emitSessionEvent(session, "ops_session_released");
350
+ }
123
351
  }
124
352
  }
125
353
  handleTabRemoved = (tabId) => {
354
+ this.forgetCommandCreatedTab(tabId);
355
+ this.popupOpenerTabIds.delete(tabId);
126
356
  this.handleClosedTarget(tabId, "ops_tab_closed");
127
357
  };
358
+ handleCreatedNavigationTarget = (details) => {
359
+ const tabId = typeof details.tabId === "number" ? details.tabId : null;
360
+ const openerTabId = typeof details.sourceTabId === "number" ? details.sourceTabId : null;
361
+ if (tabId === null || openerTabId === null) {
362
+ return;
363
+ }
364
+ this.popupOpenerTabIds.set(tabId, openerTabId);
365
+ };
366
+ handleTabCreated = (tab) => {
367
+ const tabId = typeof tab.id === "number" ? tab.id : null;
368
+ if (tabId === null) {
369
+ return;
370
+ }
371
+ if (this.isCommandCreatedTab(tabId)) {
372
+ return;
373
+ }
374
+ const openerTabId = typeof tab.openerTabId === "number" ? tab.openerTabId : null;
375
+ if (openerTabId !== null) {
376
+ const session = this.sessions.getByTabId(openerTabId);
377
+ if (!session) {
378
+ return;
379
+ }
380
+ this.finishCreatedTab(session, this.sessions.getTargetIdByTabId(session.id, openerTabId) ?? session.targetId, tab, tabId);
381
+ return;
382
+ }
383
+ void this.handleCreatedTab(tab, tabId);
384
+ };
385
+ async handleCreatedTab(tab, tabId) {
386
+ const opener = await this.resolvePopupOpenerContext(tabId, typeof tab.openerTabId === "number" ? tab.openerTabId : null);
387
+ if (!opener) {
388
+ return;
389
+ }
390
+ this.finishCreatedTab(opener.session, opener.openerTargetId, tab, tabId);
391
+ }
392
+ finishCreatedTab(session, openerTargetId, tab, tabId) {
393
+ this.popupOpenerTabIds.delete(tabId);
394
+ const existingTargetId = this.updateKnownTabTarget(session, tab);
395
+ if (existingTargetId) {
396
+ const existingTarget = session.targets.get(existingTargetId) ?? null;
397
+ if (existingTarget && !existingTarget.openerTargetId) {
398
+ existingTarget.openerTargetId = openerTargetId;
399
+ }
400
+ const resolvedTarget = this.resolveTargetContext(session, existingTargetId);
401
+ if (resolvedTarget
402
+ && this.shouldPromotePopupTarget(session, openerTargetId, resolvedTarget)) {
403
+ session.activeTargetId = existingTargetId;
404
+ }
405
+ return;
406
+ }
407
+ const target = this.sessions.addTarget(session.id, tabId, {
408
+ url: tab.url ?? undefined,
409
+ title: tab.title ?? undefined,
410
+ openerTargetId: openerTargetId
411
+ });
412
+ void this.attachCreatedTab(session, target.targetId, tabId);
413
+ }
414
+ async resolvePopupOpenerContext(tabId, openerTabId) {
415
+ let resolvedOpenerTabId = openerTabId ?? this.popupOpenerTabIds.get(tabId) ?? null;
416
+ if (resolvedOpenerTabId === null && typeof this.cdp.resolveTabOpenerTargetId === "function") {
417
+ const openerTargetId = await this.cdp.resolveTabOpenerTargetId(tabId).catch(() => null);
418
+ resolvedOpenerTabId = parseTargetAliasTabId(openerTargetId ?? undefined);
419
+ }
420
+ if (resolvedOpenerTabId === null) {
421
+ return null;
422
+ }
423
+ const session = this.sessions.getByTabId(resolvedOpenerTabId);
424
+ if (!session) {
425
+ return null;
426
+ }
427
+ return {
428
+ session,
429
+ openerTargetId: this.sessions.getTargetIdByTabId(session.id, resolvedOpenerTabId) ?? session.targetId
430
+ };
431
+ }
432
+ async hydratePopupOpenerTarget(session, targetId) {
433
+ const target = session.targets.get(targetId) ?? null;
434
+ if (!target || target.openerTargetId) {
435
+ return target;
436
+ }
437
+ const opener = await this.resolvePopupOpenerContext(target.tabId, null);
438
+ if (!opener || opener.session.id !== session.id) {
439
+ return target;
440
+ }
441
+ target.openerTargetId = opener.openerTargetId;
442
+ return target;
443
+ }
128
444
  handleTabUpdated = (tabId, changeInfo, tab) => {
129
445
  const session = this.sessions.getByTabId(tabId);
130
- if (!session)
446
+ if (!session) {
447
+ if (this.isCommandCreatedTab(tabId)) {
448
+ return;
449
+ }
450
+ if (changeInfo.status === "complete" || tab.status === "complete" || typeof tab.openerTabId === "number") {
451
+ void this.handleCreatedTab(tab, tabId);
452
+ }
131
453
  return;
454
+ }
455
+ const targetId = this.updateKnownTabTarget(session, tab);
456
+ if (targetId
457
+ && tab.active === true
458
+ && (changeInfo.status === "complete" || tab.status === "complete")) {
459
+ const target = this.resolveTargetContext(session, targetId);
460
+ if (target && (!target.openerTargetId || this.hasUsableDebuggee(target))) {
461
+ session.activeTargetId = targetId;
462
+ }
463
+ }
132
464
  if (changeInfo.discarded === true || tab.discarded === true) {
133
465
  session.discardedSignals += 1;
134
466
  }
@@ -143,10 +475,60 @@ export class OpsRuntime {
143
475
  return;
144
476
  void this.handleDebuggerDetachForTab(source.tabId);
145
477
  };
478
+ updateKnownTabTarget(session, tab) {
479
+ const tabId = typeof tab.id === "number" ? tab.id : null;
480
+ if (tabId === null) {
481
+ return null;
482
+ }
483
+ const targetId = this.sessions.getTargetIdByTabId(session.id, tabId);
484
+ if (!targetId) {
485
+ return null;
486
+ }
487
+ const target = session.targets.get(targetId);
488
+ if (!target) {
489
+ return targetId;
490
+ }
491
+ const nextUrl = getReportedTabUrl(tab);
492
+ const nextTitle = getReportedTabTitle(tab);
493
+ if (typeof nextUrl === "string" && nextUrl.length > 0) {
494
+ target.url = nextUrl;
495
+ }
496
+ if (typeof nextTitle === "string" && nextTitle.length > 0) {
497
+ target.title = nextTitle;
498
+ }
499
+ return targetId;
500
+ }
501
+ async attachCreatedTab(session, targetId, tabId) {
502
+ const target = await this.hydratePopupOpenerTarget(session, targetId);
503
+ if (target?.openerTargetId) {
504
+ // Keep the opener root stable and attach popup tabs only when the caller explicitly targets them.
505
+ return;
506
+ }
507
+ await this.tabs.waitForTabComplete(tabId, 5000).catch(() => undefined);
508
+ try {
509
+ await this.attachTargetTab(tabId);
510
+ await this.enableTargetDomains(tabId, true);
511
+ this.promotePopupTarget(session, targetId);
512
+ }
513
+ catch (error) {
514
+ if (target && isAttachBlockedError(error)) {
515
+ const bridged = await this.attachTargetViaOpenerSession(session, target).catch(() => false);
516
+ if (bridged) {
517
+ this.promotePopupTarget(session, targetId);
518
+ return;
519
+ }
520
+ }
521
+ logError("ops.popup_tab_attach", error, {
522
+ code: "popup_tab_attach_failed",
523
+ extra: { tabId }
524
+ });
525
+ }
526
+ }
146
527
  handleDebuggerEvent = (source, method, params) => {
147
- if (typeof source.tabId !== "number")
528
+ const eventTabId = this.cdp.resolveSourceTabId(source);
529
+ if (eventTabId === null)
148
530
  return;
149
- const session = this.sessions.getByTabId(source.tabId);
531
+ const session = this.sessions.getByTabId(eventTabId);
150
532
  if (!session)
151
533
  return;
152
534
  if (method === "Runtime.consoleAPICalled") {
@@ -175,6 +557,29 @@ export class OpsRuntime {
175
557
  }
176
558
  return;
177
559
  }
560
+ if (method === "Page.javascriptDialogOpening") {
561
+ const targetId = this.resolveDebuggerEventTargetId(session, source, eventTabId);
562
+ if (!targetId) {
563
+ return;
564
+ }
565
+ this.applyDialogOpening(session, targetId, params);
566
+ return;
567
+ }
568
+ if (method === "Page.javascriptDialogClosed") {
569
+ const targetId = this.resolveDebuggerEventTargetId(session, source, eventTabId);
570
+ if (targetId) {
571
+ this.applyDialogClosed(session, targetId);
572
+ }
573
+ return;
574
+ }
575
+ if (method === "Page.fileChooserOpened") {
576
+ const targetId = this.resolveDebuggerEventTargetId(session, source, eventTabId);
577
+ if (!targetId) {
578
+ return;
579
+ }
580
+ this.applyFileChooserOpened(session, targetId, params);
581
+ return;
582
+ }
178
583
  if (method === "Network.requestWillBeSent") {
179
584
  const payload = params;
180
585
  const requestId = payload.requestId;
@@ -223,6 +628,130 @@ export class OpsRuntime {
223
628
  }
224
629
  }
225
630
  };
631
+ handleCdpRouterEvent = (event) => {
632
+ const session = this.sessions.getByTabId(event.tabId);
633
+ if (!session) {
634
+ return;
635
+ }
636
+ switch (event.method) {
637
+ case "Page.javascriptDialogOpening": {
638
+ const targetId = this.resolveRouterEventTargetId(session, event);
639
+ if (!targetId) {
640
+ return;
641
+ }
642
+ this.applyDialogOpening(session, targetId, event.params);
643
+ return;
644
+ }
645
+ case "Page.javascriptDialogClosed": {
646
+ const targetId = this.resolveRouterEventTargetId(session, event);
647
+ if (!targetId) {
648
+ return;
649
+ }
650
+ this.applyDialogClosed(session, targetId);
651
+ return;
652
+ }
653
+ case "Page.fileChooserOpened": {
654
+ const targetId = this.resolveRouterEventTargetId(session, event);
655
+ if (!targetId) {
656
+ return;
657
+ }
658
+ this.applyFileChooserOpened(session, targetId, event.params);
659
+ return;
660
+ }
661
+ case "Target.targetCreated":
662
+ this.handleSyntheticTargetCreated(session, event);
663
+ return;
664
+ case "Target.attachedToTarget":
665
+ this.handleSyntheticTargetAttached(session, event);
666
+ return;
667
+ case "Target.targetDestroyed":
668
+ this.handleSyntheticTargetDestroyed(session, event);
669
+ return;
670
+ case "Target.detachedFromTarget":
671
+ this.handleSyntheticTargetDetached(session, event);
672
+ return;
673
+ default:
674
+ return;
675
+ }
676
+ };
677
+ handleSyntheticTargetCreated(session, event) {
678
+ const targetInfo = extractTargetInfo(event.params);
679
+ if (!targetInfo || !isSyntheticPageTarget(session, targetInfo.targetId, targetInfo.type)) {
680
+ return;
681
+ }
682
+ const resolvedTabId = parseTabTargetId(targetInfo.targetId) ?? event.tabId;
683
+ this.sessions.upsertSyntheticTarget(session.id, {
684
+ targetId: targetInfo.targetId,
685
+ tabId: resolvedTabId,
686
+ type: targetInfo.type,
687
+ ...(typeof targetInfo.url === "string" ? { url: targetInfo.url } : {}),
688
+ ...(typeof targetInfo.title === "string" ? { title: targetInfo.title } : {}),
689
+ ...(typeof targetInfo.openerId === "string" ? { openerTargetId: targetInfo.openerId } : {}),
690
+ attachedAt: Date.now()
691
+ });
692
+ }
693
+ handleSyntheticTargetAttached(session, event) {
694
+ const payload = isRecord(event.params) ? event.params : null;
695
+ const targetInfo = extractTargetInfo(payload);
696
+ const childSessionId = payload && typeof payload.sessionId === "string" ? payload.sessionId : undefined;
697
+ if (!targetInfo || !isSyntheticPageTarget(session, targetInfo.targetId, targetInfo.type)) {
698
+ return;
699
+ }
700
+ const resolvedTabId = parseTabTargetId(targetInfo.targetId) ?? event.tabId;
701
+ const synthetic = this.sessions.upsertSyntheticTarget(session.id, {
702
+ targetId: targetInfo.targetId,
703
+ tabId: resolvedTabId,
704
+ type: targetInfo.type,
705
+ ...(typeof targetInfo.url === "string" ? { url: targetInfo.url } : {}),
706
+ ...(typeof targetInfo.title === "string" ? { title: targetInfo.title } : {}),
707
+ ...(childSessionId ? { sessionId: childSessionId } : {}),
708
+ ...(typeof targetInfo.openerId === "string" ? { openerTargetId: targetInfo.openerId } : {}),
709
+ attachedAt: Date.now()
710
+ });
711
+ if (!session.activeTargetId
712
+ || session.activeTargetId === session.targetId
713
+ || session.activeTargetId === synthetic.openerTargetId) {
714
+ session.activeTargetId = synthetic.targetId;
715
+ }
716
+ }
717
+ handleSyntheticTargetDestroyed(session, event) {
718
+ const payload = isRecord(event.params) ? event.params : null;
719
+ const targetId = payload && typeof payload.targetId === "string" ? payload.targetId : null;
720
+ if (!targetId) {
721
+ return;
722
+ }
723
+ const removed = this.sessions.removeSyntheticTarget(session.id, targetId);
724
+ if (!removed) {
725
+ return;
726
+ }
727
+ this.restoreSyntheticFallbackTarget(session, removed);
728
+ }
729
+ handleSyntheticTargetDetached(session, event) {
730
+ const payload = isRecord(event.params) ? event.params : null;
731
+ const targetId = payload && typeof payload.targetId === "string" ? payload.targetId : null;
732
+ const sessionId = payload && typeof payload.sessionId === "string" ? payload.sessionId : null;
733
+ const removed = targetId
734
+ ? this.sessions.removeSyntheticTarget(session.id, targetId)
735
+ : (sessionId ? this.sessions.findSyntheticTargetBySessionId(session.id, sessionId) : null);
736
+ if (!removed) {
737
+ return;
738
+ }
739
+ if (!targetId && sessionId) {
740
+ this.sessions.removeSyntheticTarget(session.id, removed.targetId);
741
+ }
742
+ this.restoreSyntheticFallbackTarget(session, removed);
743
+ }
744
+ restoreSyntheticFallbackTarget(session, removed) {
745
+ if (session.activeTargetId !== removed.targetId) {
746
+ return;
747
+ }
748
+ if (removed.openerTargetId && this.hasOpsTarget(session, removed.openerTargetId)) {
749
+ session.activeTargetId = removed.openerTargetId;
750
+ return;
751
+ }
752
+ const firstSynthetic = this.sessions.listSyntheticTargets(session.id)[0];
753
+ session.activeTargetId = firstSynthetic?.targetId ?? session.targetId;
754
+ }
226
755
  async handleRequest(message) {
227
756
  const clientId = message.clientId;
228
757
  if (!clientId) {
@@ -252,6 +781,9 @@ export class OpsRuntime {
252
781
  case "targets.use":
253
782
  await this.withSession(message, clientId, (session) => this.handleTargetsUse(message, session));
254
783
  return;
784
+ case "targets.registerCanvas":
785
+ await this.withSession(message, clientId, (session) => this.handleTargetsRegisterCanvas(message, session));
786
+ return;
255
787
  case "targets.new":
256
788
  await this.withSession(message, clientId, (session) => this.handleTargetsNew(message, session));
257
789
  return;
@@ -276,6 +808,9 @@ export class OpsRuntime {
276
808
  case "nav.snapshot":
277
809
  await this.withSession(message, clientId, (session) => this.handleSnapshot(message, session));
278
810
  return;
811
+ case "nav.review":
812
+ await this.withSession(message, clientId, (session) => this.handleReview(message, session));
813
+ return;
279
814
  case "interact.click":
280
815
  await this.withSession(message, clientId, (session) => this.handleClick(message, session));
281
816
  return;
@@ -303,6 +838,21 @@ export class OpsRuntime {
303
838
  case "interact.scrollIntoView":
304
839
  await this.withSession(message, clientId, (session) => this.handleScrollIntoView(message, session));
305
840
  return;
841
+ case "interact.upload":
842
+ await this.withSession(message, clientId, (session) => this.handleUpload(message, session));
843
+ return;
844
+ case "pointer.move":
845
+ await this.withSession(message, clientId, (session) => this.handlePointerMove(message, session));
846
+ return;
847
+ case "pointer.down":
848
+ await this.withSession(message, clientId, (session) => this.handlePointerDown(message, session));
849
+ return;
850
+ case "pointer.up":
851
+ await this.withSession(message, clientId, (session) => this.handlePointerUp(message, session));
852
+ return;
853
+ case "pointer.drag":
854
+ await this.withSession(message, clientId, (session) => this.handlePointerDrag(message, session));
855
+ return;
306
856
  case "dom.getHtml":
307
857
  await this.withSession(message, clientId, (session) => this.handleDomGetHtml(message, session));
308
858
  return;
@@ -324,6 +874,24 @@ export class OpsRuntime {
324
874
  case "dom.isChecked":
325
875
  await this.withSession(message, clientId, (session) => this.handleDomIsChecked(message, session));
326
876
  return;
877
+ case "dom.refPoint":
878
+ await this.withSession(message, clientId, (session) => this.handleDomRefPoint(message, session));
879
+ return;
880
+ case "canvas.overlay.mount":
881
+ await this.withSession(message, clientId, (session) => this.handleCanvasOverlayMount(message, session));
882
+ return;
883
+ case "canvas.overlay.unmount":
884
+ await this.withSession(message, clientId, (session) => this.handleCanvasOverlayUnmount(message, session));
885
+ return;
886
+ case "canvas.overlay.select":
887
+ await this.withSession(message, clientId, (session) => this.handleCanvasOverlaySelect(message, session));
888
+ return;
889
+ case "canvas.overlay.sync":
890
+ await this.withSession(message, clientId, (session) => this.handleCanvasOverlaySync(message, session));
891
+ return;
892
+ case "canvas.applyRuntimePreviewBridge":
893
+ await this.withSession(message, clientId, (session) => this.handleCanvasRuntimePreviewBridge(message, session));
894
+ return;
327
895
  case "export.clonePage":
328
896
  await this.withSession(message, clientId, (session) => this.handleClonePage(message, session));
329
897
  return;
@@ -336,6 +904,9 @@ export class OpsRuntime {
336
904
  case "page.screenshot":
337
905
  await this.withSession(message, clientId, (session) => this.handleScreenshot(message, session));
338
906
  return;
907
+ case "page.dialog":
908
+ await this.withSession(message, clientId, (session) => this.handleDialog(message, session));
909
+ return;
339
910
  case "devtools.consolePoll":
340
911
  await this.withSession(message, clientId, (session) => this.handleConsolePoll(message, session));
341
912
  return;
@@ -350,6 +921,13 @@ export class OpsRuntime {
350
921
  const payload = isRecord(message.payload) ? message.payload : {};
351
922
  const parallelismPolicy = parseParallelismPolicy(payload.parallelismPolicy);
352
923
  const startUrl = typeof payload.startUrl === "string" ? payload.startUrl : undefined;
924
+ const isStartUrlConnect = message.command === "session.connect" && typeof startUrl === "string";
925
+ const requestedSessionId = typeof payload.sessionId === "string" && payload.sessionId.trim().length > 0
926
+ ? payload.sessionId.trim()
927
+ : undefined;
928
+ const requestedTabId = typeof payload.tabId === "number" && Number.isInteger(payload.tabId)
929
+ ? payload.tabId
930
+ : undefined;
353
931
  if (startUrl) {
354
932
  try {
355
933
  const restriction = getRestrictionMessage(new URL(startUrl));
@@ -363,51 +941,81 @@ export class OpsRuntime {
363
941
  return;
364
942
  }
365
943
  }
366
- const activeTab = startUrl
944
+ let activeTab = startUrl
367
945
  ? await this.tabs.createTab(startUrl, true)
368
- : await this.tabs.getActiveTab();
946
+ : typeof requestedTabId === "number"
947
+ ? await this.tabs.getTab(requestedTabId)
948
+ : await this.tabs.getActiveTab();
949
+ if (!startUrl && typeof requestedTabId !== "number") {
950
+ const currentRawUrl = activeTab?.url ?? activeTab?.pendingUrl ?? "";
951
+ const needsFallback = !activeTab
952
+ || typeof activeTab.id !== "number"
953
+ || currentRawUrl.length === 0
954
+ || isRestrictedUrl(currentRawUrl).restricted;
955
+ if (needsFallback) {
956
+ activeTab = await this.tabs.getFirstAttachableTab(typeof activeTab?.id === "number" ? activeTab.id : undefined) ?? activeTab;
957
+ }
958
+ }
369
959
  if (!activeTab || typeof activeTab.id !== "number") {
960
+ if (typeof requestedTabId === "number") {
961
+ this.sendError(message, buildError("invalid_request", `Unknown tabId: ${requestedTabId}`, false));
962
+ return;
963
+ }
370
964
  this.sendError(message, buildError("ops_unavailable", "No active tab to attach.", true));
371
965
  return;
372
966
  }
373
- if (activeTab.url) {
374
- const restriction = isRestrictedUrl(activeTab.url);
967
+ const activeTabId = activeTab.id;
968
+ let resolvedTab = startUrl
969
+ ? await this.tabs.waitForTabComplete(activeTabId)
970
+ .catch(() => undefined)
971
+ .then(async () => await this.tabs.getTab(activeTabId) ?? activeTab)
972
+ : activeTab;
973
+ if (resolvedTab.url) {
974
+ const restriction = isRestrictedUrl(resolvedTab.url);
375
975
  if (restriction.restricted) {
376
976
  this.sendError(message, buildError("restricted_url", restriction.message ?? "Restricted tab.", false));
377
977
  return;
378
978
  }
379
979
  }
380
980
  try {
381
- await this.cdp.attach(activeTab.id);
981
+ const refreshedTab = isStartUrlConnect
982
+ ? await this.attachStartUrlConnectTab(activeTabId)
983
+ : await this.attachLaunchTargetTab(activeTabId, false);
984
+ if (refreshedTab) {
985
+ resolvedTab = refreshedTab;
986
+ }
987
+ }
988
+ catch (error) {
989
+ const detail = error instanceof Error ? error.message : "Debugger attach failed";
990
+ this.sendError(message, buildError("cdp_attach_failed", detail, false, this.getDirectAttachErrorDetails(error)));
991
+ return;
992
+ }
993
+ if (!startUrl) {
994
+ await this.tabs.waitForTabComplete(activeTab.id).catch(() => undefined);
995
+ }
996
+ try {
997
+ await this.enableTargetDomains(activeTabId, true);
382
998
  }
383
999
  catch (error) {
384
1000
  const detail = error instanceof Error ? error.message : "Debugger attach failed";
385
- this.sendError(message, buildError("cdp_attach_failed", detail, false));
1001
+ this.sendError(message, buildError("cdp_attach_failed", detail, false, this.getDirectAttachErrorDetails(error)));
386
1002
  return;
387
1003
  }
388
- await this.tabs.waitForTabComplete(activeTab.id).catch(() => undefined);
389
1004
  const leaseId = typeof message.leaseId === "string" && message.leaseId.trim().length > 0
390
1005
  ? message.leaseId.trim()
391
1006
  : createId();
392
- const session = this.sessions.createSession(clientId, activeTab.id, leaseId, {
393
- url: activeTab.url ?? undefined,
394
- title: activeTab.title ?? undefined
1007
+ const session = this.sessions.createSession(clientId, activeTabId, leaseId, {
1008
+ url: resolvedTab.url ?? undefined,
1009
+ title: resolvedTab.title ?? undefined
395
1010
  }, {
396
1011
  parallelismPolicy
397
- });
398
- await this.enableSessionDomains(session);
399
- this.sendEvent({
400
- type: "ops_event",
401
- clientId,
402
- opsSessionId: session.id,
403
- event: "ops_session_created",
404
- payload: { tabId: session.tabId, targetId: session.targetId }
405
- });
1012
+ }, requestedSessionId);
1013
+ this.emitSessionEvent(session, "ops_session_created");
406
1014
  this.sendResponse(message, {
407
1015
  opsSessionId: session.id,
408
1016
  activeTargetId: session.activeTargetId,
409
- url: activeTab.url ?? undefined,
410
- title: activeTab.title ?? undefined,
1017
+ url: resolvedTab.url ?? undefined,
1018
+ title: resolvedTab.title ?? undefined,
411
1019
  leaseId: session.leaseId
412
1020
  });
413
1021
  }
@@ -422,12 +1030,18 @@ export class OpsRuntime {
422
1030
  const session = this.getSessionForMessage(message, clientId);
423
1031
  if (!session)
424
1032
  return;
425
- const tab = await this.tabs.getTab(session.tabId);
1033
+ const activeTargetId = session.activeTargetId ?? session.targetId;
1034
+ const reportedTarget = activeTargetId
1035
+ ? this.resolveRequestedTargetContext(session, activeTargetId, false)
1036
+ : null;
1037
+ const reportedTargetId = reportedTarget?.targetId ?? null;
1038
+ const tab = reportedTarget ? await this.tabs.getTab(reportedTarget.tabId) : null;
426
1039
  this.sendResponse(message, {
427
1040
  mode: "extension",
428
- activeTargetId: session.activeTargetId || null,
429
- url: tab?.url ?? undefined,
430
- title: tab?.title ?? undefined,
1041
+ activeTargetId: reportedTargetId,
1042
+ url: resolveReportedTargetUrl(reportedTarget, tab),
1043
+ title: resolveReportedTargetTitle(reportedTarget, tab),
1044
+ dialog: this.serializeDialogState(session, reportedTargetId),
431
1045
  leaseId: session.leaseId,
432
1046
  state: session.state
433
1047
  });
@@ -435,13 +1049,26 @@ export class OpsRuntime {
435
1049
  async handleTargetsList(message, session) {
436
1050
  const payload = isRecord(message.payload) ? message.payload : {};
437
1051
  const includeUrls = payload.includeUrls === true;
438
- const targets = await Promise.all(Array.from(session.targets.values()).map(async (target) => {
439
- const tab = await this.tabs.getTab(target.tabId);
440
- return {
1052
+ const targetContexts = [
1053
+ ...Array.from(session.targets.values()).map((target) => ({
1054
+ targetId: target.targetId,
1055
+ tabId: target.tabId
1056
+ })),
1057
+ ...this.sessions.listSyntheticTargets(session.id)
1058
+ .filter((target) => !session.targets.has(target.targetId))
1059
+ .map((target) => ({
441
1060
  targetId: target.targetId,
1061
+ tabId: target.tabId
1062
+ }))
1063
+ ];
1064
+ const targets = await Promise.all(targetContexts.map(async ({ targetId, tabId }) => {
1065
+ const target = this.resolveTargetContext(session, targetId);
1066
+ const tab = await this.tabs.getTab(tabId);
1067
+ return {
1068
+ targetId,
442
1069
  type: "page",
443
- title: tab?.title ?? target.title,
444
- url: includeUrls ? tab?.url ?? target.url : undefined
1070
+ title: resolveReportedTargetTitle(target, tab),
1071
+ url: includeUrls ? resolveReportedTargetUrl(target, tab) : undefined
445
1072
  };
446
1073
  }));
447
1074
  this.sendResponse(message, { activeTargetId: session.activeTargetId || null, targets });
@@ -449,65 +1076,270 @@ export class OpsRuntime {
449
1076
  async handleTargetsUse(message, session) {
450
1077
  const payload = isRecord(message.payload) ? message.payload : {};
451
1078
  const targetId = typeof payload.targetId === "string" ? payload.targetId : null;
452
- if (!targetId || !session.targets.has(targetId)) {
1079
+ if (!targetId || !this.hasOpsTarget(session, targetId)) {
453
1080
  this.sendError(message, buildError("invalid_request", "Unknown targetId", false));
454
1081
  return;
455
1082
  }
456
- session.activeTargetId = targetId;
457
- const target = session.targets.get(targetId) ?? null;
458
- if (target) {
459
- await this.tabs.activateTab(target.tabId).catch(() => undefined);
1083
+ let target = this.rehydrateSyntheticPopupBridge(session, targetId) ?? this.resolveTargetContext(session, targetId);
1084
+ if (target && !this.hasUsableDebuggee(target) && (target.synthetic || !!target.openerTargetId)) {
1085
+ try {
1086
+ target = await this.preparePopupTarget(session, targetId) ?? target;
1087
+ target = this.rehydrateSyntheticPopupBridge(session, targetId) ?? target;
1088
+ }
1089
+ catch (error) {
1090
+ const detail = error instanceof Error ? error.message : "Debugger attach failed";
1091
+ this.sendError(message, buildError("cdp_attach_failed", detail, false, this.getDirectAttachErrorDetails(error)));
1092
+ return;
1093
+ }
460
1094
  }
461
- const tab = target ? await this.tabs.getTab(target.tabId) : null;
462
- this.sendResponse(message, {
463
- activeTargetId: targetId,
464
- url: tab?.url ?? target?.url,
465
- title: tab?.title ?? target?.title
466
- });
467
- }
468
- async handleTargetsNew(message, session) {
469
- const payload = isRecord(message.payload) ? message.payload : {};
470
- const url = typeof payload.url === "string" ? payload.url : undefined;
471
- const tab = await this.tabs.createTab(url, true);
472
- if (!tab?.id) {
473
- this.sendError(message, buildError("execution_failed", "Target creation failed", false));
1095
+ if (target?.synthetic && !target.sessionId) {
1096
+ const syntheticPopupTarget = target.openerTargetId
1097
+ ? {
1098
+ targetId,
1099
+ tabId: target.tabId,
1100
+ ...(typeof target.url === "string" ? { url: target.url } : {}),
1101
+ ...(typeof target.title === "string" ? { title: target.title } : {}),
1102
+ openerTargetId: target.openerTargetId
1103
+ }
1104
+ : null;
1105
+ if (syntheticPopupTarget && await this.attachTargetViaOpenerSession(session, syntheticPopupTarget).catch(() => false)) {
1106
+ this.clearPopupAttachDiagnostic(session.id, targetId);
1107
+ await this.activateTargetAndRespond(message, session, targetId);
1108
+ return;
1109
+ }
1110
+ this.sendPopupAttachPendingError(message, session, targetId);
474
1111
  return;
475
1112
  }
476
- await this.tabs.waitForTabComplete(tab.id).catch(() => undefined);
477
- try {
478
- await this.cdp.attach(tab.id);
479
- }
480
- catch (error) {
481
- const detail = error instanceof Error ? error.message : "Debugger attach failed";
482
- this.sendError(message, buildError("cdp_attach_failed", detail, false));
483
- return;
1113
+ if (target && !target.synthetic) {
1114
+ const targetHasUsableDebuggee = this.hasUsableDebuggee(target);
1115
+ const hydratedPopupTarget = !targetHasUsableDebuggee
1116
+ ? await this.hydratePopupOpenerTarget(session, targetId)
1117
+ : null;
1118
+ const popupTarget = hydratedPopupTarget?.openerTargetId
1119
+ ? hydratedPopupTarget
1120
+ : target.openerTargetId
1121
+ ? {
1122
+ targetId,
1123
+ tabId: target.tabId,
1124
+ ...(typeof target.url === "string" ? { url: target.url } : {}),
1125
+ ...(typeof target.title === "string" ? { title: target.title } : {}),
1126
+ openerTargetId: target.openerTargetId
1127
+ }
1128
+ : null;
1129
+ if (popupTarget?.openerTargetId) {
1130
+ const resolvedPopupTarget = this.resolveTargetContext(session, targetId);
1131
+ if (resolvedPopupTarget && this.hasUsableDebuggee(resolvedPopupTarget)) {
1132
+ this.clearPopupAttachDiagnostic(session.id, targetId);
1133
+ await this.activateTargetAndRespond(message, session, targetId);
1134
+ return;
1135
+ }
1136
+ }
1137
+ const deferPopupActivation = Boolean(popupTarget?.openerTargetId && !targetHasUsableDebuggee);
1138
+ if (!deferPopupActivation) {
1139
+ await this.tabs.activateTab(target.tabId).catch(() => undefined);
1140
+ }
1141
+ if (!targetHasUsableDebuggee) {
1142
+ if (popupTarget?.openerTargetId && await this.attachTargetViaOpenerSession(session, popupTarget).catch(() => false)) {
1143
+ this.clearPopupAttachDiagnostic(session.id, targetId);
1144
+ await this.activateTargetAndRespond(message, session, targetId);
1145
+ return;
1146
+ }
1147
+ if (popupTarget?.openerTargetId) {
1148
+ const resolvedPopupTarget = this.resolveTargetContext(session, targetId);
1149
+ if (resolvedPopupTarget && this.hasUsableDebuggee(resolvedPopupTarget)) {
1150
+ this.clearPopupAttachDiagnostic(session.id, targetId);
1151
+ await this.activateTargetAndRespond(message, session, targetId);
1152
+ return;
1153
+ }
1154
+ if (this.shouldPreferDirectPopupTabAttach(popupTarget)) {
1155
+ try {
1156
+ await this.attachTargetTab(target.tabId);
1157
+ await this.enableTargetDomains(target.tabId, true);
1158
+ this.clearPopupAttachDiagnostic(session.id, targetId);
1159
+ await this.activateTargetAndRespond(message, session, targetId);
1160
+ return;
1161
+ }
1162
+ catch (error) {
1163
+ if (!isAttachBlockedError(error)) {
1164
+ const detail = error instanceof Error ? error.message : "Debugger attach failed";
1165
+ this.sendError(message, buildError("cdp_attach_failed", detail, false, this.getDirectAttachErrorDetails(error)));
1166
+ return;
1167
+ }
1168
+ }
1169
+ }
1170
+ this.sendPopupAttachPendingError(message, session, targetId);
1171
+ return;
1172
+ }
1173
+ try {
1174
+ await this.attachTargetTab(target.tabId);
1175
+ await this.enableTargetDomains(target.tabId, true);
1176
+ this.clearPopupAttachDiagnostic(session.id, targetId);
1177
+ }
1178
+ catch (error) {
1179
+ if (isAttachBlockedError(error) && popupTarget && await this.attachTargetViaOpenerSession(session, popupTarget).catch(() => false)) {
1180
+ session.activeTargetId = targetId;
1181
+ this.clearPopupAttachDiagnostic(session.id, targetId);
1182
+ await this.tabs.activateTab(target.tabId).catch(() => undefined);
1183
+ const tab = await this.tabs.getTab(target.tabId);
1184
+ this.sendResponse(message, {
1185
+ activeTargetId: targetId,
1186
+ url: resolveReportedTargetUrl(this.resolveTargetContext(session, targetId), tab),
1187
+ title: resolveReportedTargetTitle(this.resolveTargetContext(session, targetId), tab)
1188
+ });
1189
+ return;
1190
+ }
1191
+ const detail = error instanceof Error ? error.message : "Debugger attach failed";
1192
+ this.sendError(message, buildError("cdp_attach_failed", detail, false, this.getDirectAttachErrorDetails(error)));
1193
+ return;
1194
+ }
1195
+ }
484
1196
  }
485
- const target = this.sessions.addTarget(session.id, tab.id, { url: tab.url ?? undefined, title: tab.title ?? undefined });
486
- session.activeTargetId = target.targetId;
487
- this.sendResponse(message, { targetId: target.targetId });
1197
+ await this.activateTargetAndRespond(message, session, targetId);
488
1198
  }
489
- async handleTargetsClose(message, session) {
1199
+ async handleTargetsRegisterCanvas(message, session) {
490
1200
  const payload = isRecord(message.payload) ? message.payload : {};
491
- const targetId = typeof payload.targetId === "string" ? payload.targetId : null;
1201
+ const targetId = typeof payload.targetId === "string" ? payload.targetId.trim() : "";
492
1202
  if (!targetId) {
493
1203
  this.sendError(message, buildError("invalid_request", "Missing targetId", false));
494
1204
  return;
495
1205
  }
496
- const target = session.targets.get(targetId);
497
- if (!target) {
498
- this.sendError(message, buildError("invalid_request", "Unknown targetId", false));
499
- return;
1206
+ try {
1207
+ this.sendResponse(message, await this.registerCanvasTarget(session, targetId));
500
1208
  }
501
- this.sessions.removeTarget(session.id, targetId);
502
- await this.closeTabBestEffort(target.tabId);
503
- if (target.targetId === session.targetId || session.targets.size === 0) {
504
- this.sendResponse(message, { ok: true });
505
- this.scheduleSessionCleanup(session.id, "ops_session_closed");
1209
+ catch (error) {
1210
+ const detail = error instanceof Error ? error.message : "Canvas target registration failed";
1211
+ if (detail === "Canvas targetId must be tab-<id>.") {
1212
+ this.sendError(message, buildError("invalid_request", detail, false));
1213
+ return;
1214
+ }
1215
+ if (detail === "Unknown targetId") {
1216
+ this.sendError(message, buildError("invalid_request", detail, false));
1217
+ return;
1218
+ }
1219
+ if (detail === "Only the extension canvas tab can be registered.") {
1220
+ this.sendError(message, buildError("restricted_url", detail, false));
1221
+ return;
1222
+ }
1223
+ logError("ops.register_canvas_target", error, {
1224
+ code: "canvas_target_attach_failed",
1225
+ extra: { targetId }
1226
+ });
1227
+ this.sendError(message, buildError("execution_failed", detail, false));
506
1228
  return;
507
1229
  }
508
- this.sendResponse(message, { ok: true });
509
1230
  }
510
- async handlePageOpen(message, session) {
1231
+ async registerCanvasTarget(session, targetId) {
1232
+ const tabId = parseTabTargetId(targetId);
1233
+ if (tabId === null) {
1234
+ throw new Error("Canvas targetId must be tab-<id>.");
1235
+ }
1236
+ let tab = await this.tabs.getTab(tabId);
1237
+ if (!tab) {
1238
+ throw new Error("Unknown targetId");
1239
+ }
1240
+ await this.tabs.waitForTabComplete(tabId, 5000).catch(() => undefined);
1241
+ tab = await this.tabs.getTab(tabId) ?? tab;
1242
+ if (!this.isAllowedCanvasTargetUrl(tab.url)) {
1243
+ throw new Error("Only the extension canvas tab can be registered.");
1244
+ }
1245
+ const existing = session.targets.get(targetId);
1246
+ if (existing) {
1247
+ existing.url = tab.url ?? existing.url;
1248
+ existing.title = tab.title ?? existing.title;
1249
+ session.activeTargetId = targetId;
1250
+ return {
1251
+ targetId,
1252
+ url: existing.url,
1253
+ title: existing.title,
1254
+ adopted: false
1255
+ };
1256
+ }
1257
+ try {
1258
+ await this.attachTargetTab(tabId);
1259
+ await this.enableTargetDomains(tabId);
1260
+ }
1261
+ catch (error) {
1262
+ logError("ops.register_canvas_target", error, {
1263
+ code: "canvas_target_attach_failed",
1264
+ extra: { tabId, targetId }
1265
+ });
1266
+ }
1267
+ const target = this.sessions.addTarget(session.id, tabId, { url: tab.url ?? undefined, title: tab.title ?? undefined });
1268
+ session.activeTargetId = target.targetId;
1269
+ return {
1270
+ targetId: target.targetId,
1271
+ url: target.url,
1272
+ title: target.title,
1273
+ adopted: true
1274
+ };
1275
+ }
1276
+ async handleTargetsNew(message, session) {
1277
+ const payload = isRecord(message.payload) ? message.payload : {};
1278
+ const url = typeof payload.url === "string" ? payload.url : undefined;
1279
+ const tab = await this.tabs.createTab(url, false);
1280
+ if (!tab?.id) {
1281
+ this.sendError(message, buildError("execution_failed", "Target creation failed", false));
1282
+ return;
1283
+ }
1284
+ this.rememberCommandCreatedTab(session.id, tab.id, "targets.new");
1285
+ try {
1286
+ const existingTarget = await this.claimCommandCreatedTab(session, tab);
1287
+ try {
1288
+ if (!this.hasAttachedTabDebuggee(tab.id)) {
1289
+ await this.attachCreatedTargetTab(tab.id);
1290
+ }
1291
+ await this.enableTargetDomains(tab.id, true);
1292
+ }
1293
+ catch (error) {
1294
+ const detail = error instanceof Error ? error.message : "Debugger attach failed";
1295
+ this.sendError(message, buildError("cdp_attach_failed", detail, false, this.getDirectAttachErrorDetails(error)));
1296
+ return;
1297
+ }
1298
+ const target = existingTarget ?? this.sessions.addTarget(session.id, tab.id, await this.getCreatedTabSeed(tab));
1299
+ session.activeTargetId = target.targetId;
1300
+ await this.activateCreatedTab(tab.id);
1301
+ this.sendResponse(message, { targetId: target.targetId });
1302
+ }
1303
+ finally {
1304
+ this.forgetCommandCreatedTab(tab.id);
1305
+ }
1306
+ }
1307
+ async handleTargetsClose(message, session) {
1308
+ const payload = isRecord(message.payload) ? message.payload : {};
1309
+ const targetId = typeof payload.targetId === "string" ? payload.targetId : null;
1310
+ if (!targetId) {
1311
+ this.sendError(message, buildError("invalid_request", "Missing targetId", false));
1312
+ return;
1313
+ }
1314
+ const target = session.targets.get(targetId) ?? null;
1315
+ const synthetic = this.sessions.getSyntheticTarget(session.id, targetId);
1316
+ if (!target && !synthetic) {
1317
+ this.sendError(message, buildError("invalid_request", "Unknown targetId", false));
1318
+ return;
1319
+ }
1320
+ if (synthetic) {
1321
+ this.sessions.removeSyntheticTarget(session.id, targetId);
1322
+ await this.cdp.sendCommand(synthetic.sessionId ? { tabId: synthetic.tabId, sessionId: synthetic.sessionId } : { tabId: synthetic.tabId }, "Target.closeTarget", {
1323
+ targetId: synthetic.targetId
1324
+ }).catch(() => undefined);
1325
+ this.restoreSyntheticFallbackTarget(session, synthetic);
1326
+ this.sendResponse(message, { ok: true });
1327
+ return;
1328
+ }
1329
+ if (!target) {
1330
+ this.sendError(message, buildError("invalid_request", "Unknown targetId", false));
1331
+ return;
1332
+ }
1333
+ this.sessions.removeTarget(session.id, targetId);
1334
+ void this.closeTabBestEffort(target.tabId);
1335
+ if (target.targetId === session.targetId || session.targets.size === 0) {
1336
+ this.sendResponse(message, { ok: true });
1337
+ this.scheduleSessionCleanup(session.id, "ops_session_closed");
1338
+ return;
1339
+ }
1340
+ this.sendResponse(message, { ok: true });
1341
+ }
1342
+ async handlePageOpen(message, session) {
511
1343
  const payload = isRecord(message.payload) ? message.payload : {};
512
1344
  const name = typeof payload.name === "string" ? payload.name : null;
513
1345
  if (!name) {
@@ -521,34 +1353,44 @@ export class OpsRuntime {
521
1353
  return;
522
1354
  }
523
1355
  const url = typeof payload.url === "string" ? payload.url : undefined;
524
- const tab = await this.tabs.createTab(url, true);
1356
+ const tab = await this.tabs.createTab(url, false);
525
1357
  if (!tab?.id) {
526
1358
  this.sendError(message, buildError("execution_failed", "Target creation failed", false));
527
1359
  return;
528
1360
  }
529
- await this.tabs.waitForTabComplete(tab.id).catch(() => undefined);
1361
+ this.rememberCommandCreatedTab(session.id, tab.id, "page.open");
530
1362
  try {
531
- await this.cdp.attach(tab.id);
1363
+ const existingTarget = await this.claimCommandCreatedTab(session, tab);
1364
+ try {
1365
+ if (!this.hasAttachedTabDebuggee(tab.id)) {
1366
+ await this.attachCreatedTargetTab(tab.id);
1367
+ }
1368
+ await this.enableTargetDomains(tab.id, true);
1369
+ }
1370
+ catch (error) {
1371
+ const detail = error instanceof Error ? error.message : "Debugger attach failed";
1372
+ this.sendError(message, buildError("cdp_attach_failed", detail, false, this.getDirectAttachErrorDetails(error)));
1373
+ return;
1374
+ }
1375
+ const target = existingTarget ?? this.sessions.addTarget(session.id, tab.id, await this.getCreatedTabSeed(tab));
1376
+ this.sessions.setName(session.id, target.targetId, name);
1377
+ session.activeTargetId = target.targetId;
1378
+ await this.activateCreatedTab(tab.id);
1379
+ this.sendResponse(message, { targetId: target.targetId, created: true, url: target.url, title: target.title });
532
1380
  }
533
- catch (error) {
534
- const detail = error instanceof Error ? error.message : "Debugger attach failed";
535
- this.sendError(message, buildError("cdp_attach_failed", detail, false));
536
- return;
1381
+ finally {
1382
+ this.forgetCommandCreatedTab(tab.id);
537
1383
  }
538
- const target = this.sessions.addTarget(session.id, tab.id, { url: tab.url ?? undefined, title: tab.title ?? undefined });
539
- this.sessions.setName(session.id, target.targetId, name);
540
- session.activeTargetId = target.targetId;
541
- this.sendResponse(message, { targetId: target.targetId, created: true, url: target.url, title: target.title });
542
1384
  }
543
1385
  async handlePageList(message, session) {
544
1386
  const pages = await Promise.all(this.sessions.listNamedTargets(session.id).map(async ({ name, targetId }) => {
545
- const target = session.targets.get(targetId);
1387
+ const target = this.resolveTargetContext(session, targetId);
546
1388
  const tab = target ? await this.tabs.getTab(target.tabId) : null;
547
1389
  return {
548
1390
  name,
549
1391
  targetId,
550
- url: tab?.url ?? target?.url,
551
- title: tab?.title ?? target?.title
1392
+ url: resolveReportedTargetUrl(target, tab),
1393
+ title: resolveReportedTargetTitle(target, tab)
552
1394
  };
553
1395
  }));
554
1396
  this.sendResponse(message, { pages });
@@ -568,7 +1410,7 @@ export class OpsRuntime {
568
1410
  const target = session.targets.get(targetId);
569
1411
  if (target) {
570
1412
  this.sessions.removeTarget(session.id, targetId);
571
- await this.closeTabBestEffort(target.tabId);
1413
+ void this.closeTabBestEffort(target.tabId);
572
1414
  if (target.targetId === session.targetId || session.targets.size === 0) {
573
1415
  this.sendResponse(message, { ok: true });
574
1416
  this.scheduleSessionCleanup(session.id, "ops_session_closed");
@@ -584,11 +1426,14 @@ export class OpsRuntime {
584
1426
  this.sendError(message, buildError("invalid_request", "Missing url", false));
585
1427
  return;
586
1428
  }
1429
+ const syntheticHtml = decodeHtmlDataUrl(url);
587
1430
  try {
588
- const restriction = getRestrictionMessage(new URL(url));
589
- if (restriction) {
590
- this.sendError(message, buildError("restricted_url", restriction, false));
591
- return;
1431
+ if (syntheticHtml === null) {
1432
+ const restriction = getRestrictionMessage(new URL(url));
1433
+ if (restriction) {
1434
+ this.sendError(message, buildError("restricted_url", restriction, false));
1435
+ return;
1436
+ }
592
1437
  }
593
1438
  }
594
1439
  catch {
@@ -601,6 +1446,27 @@ export class OpsRuntime {
601
1446
  if (!target)
602
1447
  return;
603
1448
  await this.tabs.activateTab(target.tabId).catch(() => undefined);
1449
+ const targetRecord = session.targets.get(target.targetId);
1450
+ if (syntheticHtml !== null) {
1451
+ const result = await executeInTab(target.tabId, replaceDocumentWithHtmlScript, [{ html: syntheticHtml }]);
1452
+ session.refStore.clearTarget(target.targetId);
1453
+ this.sessions.upsertSyntheticTarget(session.id, {
1454
+ targetId: target.targetId,
1455
+ tabId: target.tabId,
1456
+ type: "page",
1457
+ url,
1458
+ title: typeof result?.title === "string" && result.title.trim().length > 0
1459
+ ? result.title
1460
+ : targetRecord?.title,
1461
+ attachedAt: Date.now()
1462
+ });
1463
+ this.sendResponse(message, {
1464
+ finalUrl: url,
1465
+ status: undefined,
1466
+ timingMs: Date.now() - start
1467
+ });
1468
+ return;
1469
+ }
604
1470
  const updated = await new Promise((resolve) => {
605
1471
  chrome.tabs.update(target.tabId, { url }, (tab) => {
606
1472
  resolve(tab ?? null);
@@ -608,10 +1474,13 @@ export class OpsRuntime {
608
1474
  });
609
1475
  await this.tabs.waitForTabComplete(target.tabId, timeoutMs).catch(() => undefined);
610
1476
  const refreshed = await this.tabs.getTab(target.tabId);
611
- const targetRecord = session.targets.get(target.targetId);
1477
+ this.sessions.removeSyntheticTarget(session.id, target.targetId);
612
1478
  if (targetRecord) {
613
- targetRecord.url = refreshed?.url ?? updated?.url ?? url;
614
- targetRecord.title = refreshed?.title ?? updated?.title ?? targetRecord.title;
1479
+ session.targets.set(target.targetId, {
1480
+ ...targetRecord,
1481
+ url: refreshed?.url ?? updated?.url ?? url,
1482
+ title: refreshed?.title ?? updated?.title ?? targetRecord.title
1483
+ });
615
1484
  }
616
1485
  this.sendResponse(message, {
617
1486
  finalUrl: refreshed?.url ?? updated?.url ?? url,
@@ -628,11 +1497,16 @@ export class OpsRuntime {
628
1497
  return;
629
1498
  if (typeof payload.ref === "string") {
630
1499
  const state = payload.state === "visible" || payload.state === "hidden" ? payload.state : "attached";
631
- const selector = this.resolveSelector(session, payload.ref, message);
632
- if (!selector)
1500
+ const resolved = this.resolveRefFromPayload(session, payload.ref, message);
1501
+ if (!resolved)
633
1502
  return;
634
1503
  try {
635
- await this.waitForSelector(target.tabId, selector, state, timeoutMs);
1504
+ if (this.isAllowedCanvasTargetUrl(target.url)) {
1505
+ await this.waitForSelector(target, resolved.selector, state, timeoutMs);
1506
+ }
1507
+ else {
1508
+ await this.waitForRefState(resolved, state, timeoutMs);
1509
+ }
636
1510
  this.sendResponse(message, { timingMs: Date.now() - start });
637
1511
  }
638
1512
  catch (error) {
@@ -650,62 +1524,90 @@ export class OpsRuntime {
650
1524
  }
651
1525
  async handleSnapshot(message, session) {
652
1526
  const payload = isRecord(message.payload) ? message.payload : {};
653
- const mode = payload.mode === "actionables" ? "actionables" : "outline";
654
- const maxChars = typeof payload.maxChars === "number" ? payload.maxChars : 16000;
655
- const cursor = typeof payload.cursor === "string" ? payload.cursor : undefined;
656
- const maxNodes = typeof payload.maxNodes === "number" ? payload.maxNodes : undefined;
657
- const target = this.requireActiveTarget(session, message);
658
- if (!target)
1527
+ const snapshot = await this.captureSnapshotPayload(message, session, {
1528
+ mode: payload.mode === "actionables" ? "actionables" : "outline",
1529
+ maxChars: typeof payload.maxChars === "number" ? payload.maxChars : 16000,
1530
+ cursor: typeof payload.cursor === "string" ? payload.cursor : undefined,
1531
+ maxNodes: typeof payload.maxNodes === "number" ? payload.maxNodes : undefined
1532
+ });
1533
+ if (!snapshot)
659
1534
  return;
660
- const start = Date.now();
661
- const entriesData = await buildSnapshot((method, params) => this.cdp.sendCommand({ tabId: target.tabId }, method, params), mode, true, maxNodes);
662
- const snapshot = session.refStore.setSnapshot(target.targetId, entriesData.entries);
663
- const startIndex = parseCursor(cursor);
664
- const { content, truncated, nextCursor } = paginate(entriesData.lines, startIndex, maxChars);
665
- const contentBytes = this.encoder.encode(content).length;
666
- if (contentBytes > MAX_SNAPSHOT_BYTES) {
667
- this.sendError(message, buildError("snapshot_too_large", "Snapshot exceeded max size.", false, {
668
- maxSnapshotBytes: MAX_SNAPSHOT_BYTES,
669
- actualBytes: contentBytes
670
- }));
1535
+ this.sendResponse(message, {
1536
+ snapshotId: snapshot.snapshotId,
1537
+ url: snapshot.url,
1538
+ title: snapshot.title,
1539
+ content: snapshot.content,
1540
+ truncated: snapshot.truncated,
1541
+ ...(snapshot.nextCursor ? { nextCursor: snapshot.nextCursor } : {}),
1542
+ refCount: snapshot.refCount,
1543
+ timingMs: snapshot.timingMs,
1544
+ warnings: snapshot.warnings
1545
+ });
1546
+ }
1547
+ async handleReview(message, session) {
1548
+ const payload = isRecord(message.payload) ? message.payload : {};
1549
+ const snapshot = await this.captureSnapshotPayload(message, session, {
1550
+ mode: "actionables",
1551
+ maxChars: typeof payload.maxChars === "number" ? payload.maxChars : 16000,
1552
+ cursor: typeof payload.cursor === "string" ? payload.cursor : undefined,
1553
+ maxNodes: typeof payload.maxNodes === "number" ? payload.maxNodes : undefined
1554
+ });
1555
+ if (!snapshot)
671
1556
  return;
672
- }
673
- const tab = await this.tabs.getTab(target.tabId);
674
1557
  this.sendResponse(message, {
1558
+ sessionId: session.id,
1559
+ targetId: snapshot.target.targetId,
1560
+ mode: "extension",
675
1561
  snapshotId: snapshot.snapshotId,
676
- url: tab?.url ?? undefined,
677
- title: tab?.title ?? undefined,
678
- content,
679
- truncated,
680
- nextCursor,
681
- refCount: snapshot.count,
682
- timingMs: Date.now() - start,
683
- warnings: entriesData.warnings
1562
+ url: snapshot.url,
1563
+ title: snapshot.title,
1564
+ content: snapshot.content,
1565
+ truncated: snapshot.truncated,
1566
+ ...(snapshot.nextCursor ? { nextCursor: snapshot.nextCursor } : {}),
1567
+ refCount: snapshot.refCount,
1568
+ timingMs: snapshot.timingMs,
1569
+ dialog: this.serializeDialogState(session, snapshot.target.targetId),
1570
+ ...(snapshot.warnings.length > 0 ? { warnings: snapshot.warnings } : {})
684
1571
  });
685
1572
  }
686
1573
  async handleClick(message, session) {
687
- const selector = this.resolveSelector(session, message.payload, message);
688
- if (!selector)
689
- return;
690
- const target = this.requireActiveTarget(session, message);
691
- if (!target)
1574
+ const resolved = this.resolveRefFromPayload(session, message.payload, message);
1575
+ if (!resolved)
692
1576
  return;
693
1577
  const start = Date.now();
694
- const before = await this.tabs.getTab(target.tabId);
695
- await this.dom.click(target.tabId, selector);
696
- const after = await this.tabs.getTab(target.tabId);
1578
+ const before = await this.tabs.getTab(resolved.target.tabId);
1579
+ await this.tabs.activateTab(resolved.target.tabId).catch(() => undefined);
1580
+ if (this.isAllowedCanvasTargetUrl(resolved.target.url)) {
1581
+ await this.runElementAction(resolved.target, resolved.selector, { type: "click" }, () => this.dom.click(resolved.target.tabId, resolved.selector));
1582
+ }
1583
+ else {
1584
+ await this.callFunctionOnRef(resolved, DOM_SCROLL_INTO_VIEW_DECLARATION);
1585
+ const point = await this.resolveRefPoint(resolved);
1586
+ await this.dispatchMouseEvent(resolved.target.debuggee, "mouseMoved", point.x, point.y);
1587
+ await this.dispatchMouseEvent(resolved.target.debuggee, "mousePressed", point.x, point.y, {
1588
+ button: "left",
1589
+ clickCount: 1
1590
+ });
1591
+ await this.dispatchMouseEvent(resolved.target.debuggee, "mouseReleased", point.x, point.y, {
1592
+ button: "left",
1593
+ clickCount: 1
1594
+ });
1595
+ }
1596
+ const after = await this.tabs.getTab(resolved.target.tabId);
697
1597
  const navigated = Boolean(before?.url && after?.url && before.url !== after.url);
698
1598
  this.sendResponse(message, { timingMs: Date.now() - start, navigated });
699
1599
  }
700
1600
  async handleHover(message, session) {
701
- const selector = this.resolveSelector(session, message.payload, message);
702
- if (!selector)
703
- return;
704
- const target = this.requireActiveTarget(session, message);
705
- if (!target)
1601
+ const resolved = this.resolveRefFromPayload(session, message.payload, message);
1602
+ if (!resolved)
706
1603
  return;
707
1604
  const start = Date.now();
708
- await this.dom.hover(target.tabId, selector);
1605
+ if (this.isAllowedCanvasTargetUrl(resolved.target.url)) {
1606
+ await this.runElementAction(resolved.target, resolved.selector, { type: "hover" }, () => this.dom.hover(resolved.target.tabId, resolved.selector));
1607
+ }
1608
+ else {
1609
+ await this.callFunctionOnRef(resolved, DOM_HOVER_DECLARATION);
1610
+ }
709
1611
  this.sendResponse(message, { timingMs: Date.now() - start });
710
1612
  }
711
1613
  async handlePress(message, session) {
@@ -718,22 +1620,33 @@ export class OpsRuntime {
718
1620
  const target = this.requireActiveTarget(session, message);
719
1621
  if (!target)
720
1622
  return;
721
- const selector = typeof payload.ref === "string" ? this.resolveSelector(session, payload.ref, message) : null;
722
- if (payload.ref && !selector)
1623
+ const resolved = typeof payload.ref === "string" ? this.resolveRefFromPayload(session, payload.ref, message) : null;
1624
+ if (payload.ref && !resolved)
723
1625
  return;
724
1626
  const start = Date.now();
725
- await this.dom.press(target.tabId, selector, key);
1627
+ if (resolved && this.isAllowedCanvasTargetUrl(target.url)) {
1628
+ await this.runCanvasPageAction(target, { type: "press", key }, resolved.selector, () => this.dom.press(target.tabId, resolved.selector, key));
1629
+ }
1630
+ else if (resolved) {
1631
+ await this.callFunctionOnRef(resolved, DOM_FOCUS_DECLARATION);
1632
+ await this.dispatchKeyPress(target.debuggee, key);
1633
+ }
1634
+ else {
1635
+ await this.dispatchKeyPress(target.debuggee, key);
1636
+ }
726
1637
  this.sendResponse(message, { timingMs: Date.now() - start });
727
1638
  }
728
1639
  async handleCheck(message, session, checked) {
729
- const selector = this.resolveSelector(session, message.payload, message);
730
- if (!selector)
731
- return;
732
- const target = this.requireActiveTarget(session, message);
733
- if (!target)
1640
+ const resolved = this.resolveRefFromPayload(session, message.payload, message);
1641
+ if (!resolved)
734
1642
  return;
735
1643
  const start = Date.now();
736
- await this.dom.setChecked(target.tabId, selector, checked);
1644
+ if (this.isAllowedCanvasTargetUrl(resolved.target.url)) {
1645
+ await this.runElementAction(resolved.target, resolved.selector, { type: "setChecked", checked }, () => this.dom.setChecked(resolved.target.tabId, resolved.selector, checked));
1646
+ }
1647
+ else {
1648
+ await this.callFunctionOnRef(resolved, DOM_SET_CHECKED_DECLARATION, [checked]);
1649
+ }
737
1650
  this.sendResponse(message, { timingMs: Date.now() - start });
738
1651
  }
739
1652
  async handleType(message, session) {
@@ -744,14 +1657,16 @@ export class OpsRuntime {
744
1657
  this.sendError(message, buildError("invalid_request", "Missing ref or text", false));
745
1658
  return;
746
1659
  }
747
- const selector = this.resolveSelector(session, ref, message);
748
- if (!selector)
749
- return;
750
- const target = this.requireActiveTarget(session, message);
751
- if (!target)
1660
+ const resolved = this.resolveRefFromPayload(session, ref, message);
1661
+ if (!resolved)
752
1662
  return;
753
1663
  const start = Date.now();
754
- await this.dom.type(target.tabId, selector, text, payload.clear === true, payload.submit === true);
1664
+ if (this.isAllowedCanvasTargetUrl(resolved.target.url)) {
1665
+ await this.runElementAction(resolved.target, resolved.selector, { type: "type", value: text, clear: payload.clear === true, submit: payload.submit === true }, () => this.dom.type(resolved.target.tabId, resolved.selector, text, payload.clear === true, payload.submit === true));
1666
+ }
1667
+ else {
1668
+ await this.callFunctionOnRef(resolved, DOM_TYPE_DECLARATION, [text, payload.clear === true, payload.submit === true]);
1669
+ }
755
1670
  this.sendResponse(message, { timingMs: Date.now() - start });
756
1671
  }
757
1672
  async handleSelect(message, session) {
@@ -762,37 +1677,126 @@ export class OpsRuntime {
762
1677
  this.sendError(message, buildError("invalid_request", "Missing ref or values", false));
763
1678
  return;
764
1679
  }
765
- const selector = this.resolveSelector(session, ref, message);
766
- if (!selector)
767
- return;
768
- const target = this.requireActiveTarget(session, message);
769
- if (!target)
1680
+ const resolved = this.resolveRefFromPayload(session, ref, message);
1681
+ if (!resolved)
770
1682
  return;
771
- await this.dom.select(target.tabId, selector, values);
1683
+ if (this.isAllowedCanvasTargetUrl(resolved.target.url)) {
1684
+ await this.runElementAction(resolved.target, resolved.selector, { type: "select", values: values }, () => this.dom.select(resolved.target.tabId, resolved.selector, values));
1685
+ }
1686
+ else {
1687
+ await this.callFunctionOnRef(resolved, DOM_SELECT_DECLARATION, [values]);
1688
+ }
772
1689
  this.sendResponse(message, {});
773
1690
  }
774
1691
  async handleScroll(message, session) {
775
1692
  const payload = isRecord(message.payload) ? message.payload : {};
776
1693
  const dy = typeof payload.dy === "number" ? payload.dy : 0;
777
1694
  const ref = typeof payload.ref === "string" ? payload.ref : undefined;
778
- const selector = ref ? this.resolveSelector(session, ref, message) ?? undefined : undefined;
779
- if (ref && !selector)
780
- return;
781
1695
  const target = this.requireActiveTarget(session, message);
782
1696
  if (!target)
783
1697
  return;
784
- await this.dom.scroll(target.tabId, dy, selector);
1698
+ const resolved = ref ? this.resolveRefFromPayload(session, ref, message) : null;
1699
+ if (ref && !resolved)
1700
+ return;
1701
+ if (resolved && !this.isAllowedCanvasTargetUrl(target.url)) {
1702
+ await this.callFunctionOnRef(resolved, DOM_SCROLL_BY_DECLARATION, [dy]);
1703
+ }
1704
+ else {
1705
+ const selector = resolved?.selector;
1706
+ await this.runCanvasPageAction(target, { type: "scroll", dy }, selector ?? null, () => this.dom.scroll(target.tabId, dy, selector));
1707
+ }
785
1708
  this.sendResponse(message, {});
786
1709
  }
787
1710
  async handleScrollIntoView(message, session) {
788
- const selector = this.resolveSelector(session, message.payload, message);
789
- if (!selector)
1711
+ const resolved = this.resolveRefFromPayload(session, message.payload, message);
1712
+ if (!resolved)
1713
+ return;
1714
+ const start = Date.now();
1715
+ if (this.isAllowedCanvasTargetUrl(resolved.target.url)) {
1716
+ await this.runElementAction(resolved.target, resolved.selector, { type: "scrollIntoView" }, () => this.dom.scrollIntoView(resolved.target.tabId, resolved.selector));
1717
+ }
1718
+ else {
1719
+ await this.callFunctionOnRef(resolved, DOM_SCROLL_INTO_VIEW_DECLARATION);
1720
+ }
1721
+ this.sendResponse(message, { timingMs: Date.now() - start });
1722
+ }
1723
+ async handlePointerMove(message, session) {
1724
+ const payload = isRecord(message.payload) ? message.payload : {};
1725
+ const coords = this.parsePointerCoords(payload);
1726
+ if (!coords) {
1727
+ this.sendError(message, buildError("invalid_request", "Pointer move requires numeric x and y.", false));
1728
+ return;
1729
+ }
1730
+ const target = this.requireActiveTarget(session, message);
1731
+ if (!target)
1732
+ return;
1733
+ const start = Date.now();
1734
+ await this.dispatchMouseEvent(target.debuggee, "mouseMoved", coords.x, coords.y, {
1735
+ steps: typeof payload.steps === "number" && Number.isFinite(payload.steps)
1736
+ ? Math.max(1, Math.floor(payload.steps))
1737
+ : undefined
1738
+ });
1739
+ this.sendResponse(message, { timingMs: Date.now() - start });
1740
+ }
1741
+ async handlePointerDown(message, session) {
1742
+ const payload = isRecord(message.payload) ? message.payload : {};
1743
+ const coords = this.parsePointerCoords(payload);
1744
+ if (!coords) {
1745
+ this.sendError(message, buildError("invalid_request", "Pointer down requires numeric x and y.", false));
1746
+ return;
1747
+ }
1748
+ const target = this.requireActiveTarget(session, message);
1749
+ if (!target)
1750
+ return;
1751
+ const start = Date.now();
1752
+ await this.dispatchMouseEvent(target.debuggee, "mouseMoved", coords.x, coords.y);
1753
+ await this.dispatchMouseEvent(target.debuggee, "mousePressed", coords.x, coords.y, {
1754
+ button: this.parsePointerButton(payload.button),
1755
+ clickCount: typeof payload.clickCount === "number" && Number.isFinite(payload.clickCount)
1756
+ ? Math.max(1, Math.floor(payload.clickCount))
1757
+ : 1
1758
+ });
1759
+ this.sendResponse(message, { timingMs: Date.now() - start });
1760
+ }
1761
+ async handlePointerUp(message, session) {
1762
+ const payload = isRecord(message.payload) ? message.payload : {};
1763
+ const coords = this.parsePointerCoords(payload);
1764
+ if (!coords) {
1765
+ this.sendError(message, buildError("invalid_request", "Pointer up requires numeric x and y.", false));
1766
+ return;
1767
+ }
1768
+ const target = this.requireActiveTarget(session, message);
1769
+ if (!target)
1770
+ return;
1771
+ const start = Date.now();
1772
+ await this.dispatchMouseEvent(target.debuggee, "mouseMoved", coords.x, coords.y);
1773
+ await this.dispatchMouseEvent(target.debuggee, "mouseReleased", coords.x, coords.y, {
1774
+ button: this.parsePointerButton(payload.button),
1775
+ clickCount: typeof payload.clickCount === "number" && Number.isFinite(payload.clickCount)
1776
+ ? Math.max(1, Math.floor(payload.clickCount))
1777
+ : 1
1778
+ });
1779
+ this.sendResponse(message, { timingMs: Date.now() - start });
1780
+ }
1781
+ async handlePointerDrag(message, session) {
1782
+ const payload = isRecord(message.payload) ? message.payload : {};
1783
+ const from = isRecord(payload.from) ? this.parsePointerCoords(payload.from) : null;
1784
+ const to = isRecord(payload.to) ? this.parsePointerCoords(payload.to) : null;
1785
+ if (!from || !to) {
1786
+ this.sendError(message, buildError("invalid_request", "Pointer drag requires numeric from/to coordinates.", false));
790
1787
  return;
1788
+ }
791
1789
  const target = this.requireActiveTarget(session, message);
792
1790
  if (!target)
793
1791
  return;
794
1792
  const start = Date.now();
795
- await this.dom.scrollIntoView(target.tabId, selector);
1793
+ const steps = typeof payload.steps === "number" && Number.isFinite(payload.steps)
1794
+ ? Math.max(1, Math.floor(payload.steps))
1795
+ : 1;
1796
+ await this.dispatchMouseEvent(target.debuggee, "mouseMoved", from.x, from.y);
1797
+ await this.dispatchMouseEvent(target.debuggee, "mousePressed", from.x, from.y);
1798
+ await this.dispatchMouseEvent(target.debuggee, "mouseMoved", to.x, to.y, { steps });
1799
+ await this.dispatchMouseEvent(target.debuggee, "mouseReleased", to.x, to.y);
796
1800
  this.sendResponse(message, { timingMs: Date.now() - start });
797
1801
  }
798
1802
  async handleDomGetHtml(message, session) {
@@ -803,13 +1807,12 @@ export class OpsRuntime {
803
1807
  this.sendError(message, buildError("invalid_request", "Missing ref", false));
804
1808
  return;
805
1809
  }
806
- const selector = this.resolveSelector(session, ref, message);
807
- if (!selector)
808
- return;
809
- const target = this.requireActiveTarget(session, message);
810
- if (!target)
1810
+ const resolved = this.resolveRefFromPayload(session, ref, message);
1811
+ if (!resolved)
811
1812
  return;
812
- const html = await this.dom.getOuterHtml(target.tabId, selector);
1813
+ const html = this.isAllowedCanvasTargetUrl(resolved.target.url)
1814
+ ? await this.runElementAction(resolved.target, resolved.selector, { type: "outerHTML" }, () => this.dom.getOuterHtml(resolved.target.tabId, resolved.selector))
1815
+ : await this.callFunctionOnRef(resolved, DOM_OUTER_HTML_DECLARATION);
813
1816
  const truncated = html.length > maxChars;
814
1817
  const outerHTML = truncated ? html.slice(0, maxChars) : html;
815
1818
  this.sendResponse(message, { outerHTML, truncated });
@@ -822,13 +1825,12 @@ export class OpsRuntime {
822
1825
  this.sendError(message, buildError("invalid_request", "Missing ref", false));
823
1826
  return;
824
1827
  }
825
- const selector = this.resolveSelector(session, ref, message);
826
- if (!selector)
1828
+ const resolved = this.resolveRefFromPayload(session, ref, message);
1829
+ if (!resolved)
827
1830
  return;
828
- const target = this.requireActiveTarget(session, message);
829
- if (!target)
830
- return;
831
- const text = await this.dom.getInnerText(target.tabId, selector);
1831
+ const text = this.isAllowedCanvasTargetUrl(resolved.target.url)
1832
+ ? await this.runElementAction(resolved.target, resolved.selector, { type: "innerText" }, () => this.dom.getInnerText(resolved.target.tabId, resolved.selector))
1833
+ : await this.callFunctionOnRef(resolved, DOM_INNER_TEXT_DECLARATION);
832
1834
  const truncated = text.length > maxChars;
833
1835
  this.sendResponse(message, { text: truncated ? text.slice(0, maxChars) : text, truncated });
834
1836
  }
@@ -840,13 +1842,12 @@ export class OpsRuntime {
840
1842
  this.sendError(message, buildError("invalid_request", "Missing ref or name", false));
841
1843
  return;
842
1844
  }
843
- const selector = this.resolveSelector(session, ref, message);
844
- if (!selector)
845
- return;
846
- const target = this.requireActiveTarget(session, message);
847
- if (!target)
1845
+ const resolved = this.resolveRefFromPayload(session, ref, message);
1846
+ if (!resolved)
848
1847
  return;
849
- const value = await this.dom.getAttr(target.tabId, selector, name);
1848
+ const value = this.isAllowedCanvasTargetUrl(resolved.target.url)
1849
+ ? await this.runElementAction(resolved.target, resolved.selector, { type: "getAttr", name }, () => this.dom.getAttr(resolved.target.tabId, resolved.selector, name))
1850
+ : await this.callFunctionOnRef(resolved, DOM_GET_ATTR_DECLARATION, [name]);
850
1851
  this.sendResponse(message, { value });
851
1852
  }
852
1853
  async handleDomGetValue(message, session) {
@@ -856,94 +1857,271 @@ export class OpsRuntime {
856
1857
  this.sendError(message, buildError("invalid_request", "Missing ref", false));
857
1858
  return;
858
1859
  }
859
- const selector = this.resolveSelector(session, ref, message);
860
- if (!selector)
861
- return;
862
- const target = this.requireActiveTarget(session, message);
863
- if (!target)
1860
+ const resolved = this.resolveRefFromPayload(session, ref, message);
1861
+ if (!resolved)
864
1862
  return;
865
- const value = await this.dom.getValue(target.tabId, selector);
1863
+ const value = this.isAllowedCanvasTargetUrl(resolved.target.url)
1864
+ ? await this.runElementAction(resolved.target, resolved.selector, { type: "getValue" }, () => this.dom.getValue(resolved.target.tabId, resolved.selector))
1865
+ : await this.callFunctionOnRef(resolved, DOM_GET_VALUE_DECLARATION);
866
1866
  this.sendResponse(message, { value });
867
1867
  }
868
1868
  async handleDomIsVisible(message, session) {
869
- const selector = this.resolveSelector(session, message.payload, message);
870
- if (!selector)
871
- return;
872
- const target = this.requireActiveTarget(session, message);
873
- if (!target)
1869
+ const resolved = this.resolveRefFromPayload(session, message.payload, message);
1870
+ if (!resolved)
874
1871
  return;
875
- const visible = await this.dom.isVisible(target.tabId, selector);
876
- this.sendResponse(message, { value: visible });
1872
+ const visible = this.isAllowedCanvasTargetUrl(resolved.target.url)
1873
+ ? await this.runElementAction(resolved.target, resolved.selector, { type: "getSelectorState" }, async () => await this.dom.getSelectorState(resolved.target.tabId, resolved.selector))
1874
+ : await this.callFunctionOnRef(resolved, DOM_IS_VISIBLE_DECLARATION);
1875
+ const isVisible = typeof visible === "object" && visible !== null && "visible" in visible
1876
+ ? Boolean(visible.visible)
1877
+ : Boolean(visible);
1878
+ this.sendResponse(message, { value: isVisible });
877
1879
  }
878
1880
  async handleDomIsEnabled(message, session) {
879
- const selector = this.resolveSelector(session, message.payload, message);
880
- if (!selector)
881
- return;
882
- const target = this.requireActiveTarget(session, message);
883
- if (!target)
1881
+ const resolved = this.resolveRefFromPayload(session, message.payload, message);
1882
+ if (!resolved)
884
1883
  return;
885
- const enabled = await this.dom.isEnabled(target.tabId, selector);
1884
+ const enabled = this.isAllowedCanvasTargetUrl(resolved.target.url)
1885
+ ? await this.runElementAction(resolved.target, resolved.selector, { type: "isEnabled" }, () => this.dom.isEnabled(resolved.target.tabId, resolved.selector))
1886
+ : await this.callFunctionOnRef(resolved, DOM_IS_ENABLED_DECLARATION);
886
1887
  this.sendResponse(message, { value: enabled });
887
1888
  }
888
1889
  async handleDomIsChecked(message, session) {
889
- const selector = this.resolveSelector(session, message.payload, message);
890
- if (!selector)
891
- return;
892
- const target = this.requireActiveTarget(session, message);
893
- if (!target)
1890
+ const resolved = this.resolveRefFromPayload(session, message.payload, message);
1891
+ if (!resolved)
894
1892
  return;
895
- const checked = await this.dom.isChecked(target.tabId, selector);
1893
+ const checked = this.isAllowedCanvasTargetUrl(resolved.target.url)
1894
+ ? await this.runElementAction(resolved.target, resolved.selector, { type: "isChecked" }, () => this.dom.isChecked(resolved.target.tabId, resolved.selector))
1895
+ : await this.callFunctionOnRef(resolved, DOM_IS_CHECKED_DECLARATION);
896
1896
  this.sendResponse(message, { value: checked });
897
1897
  }
898
- async handleClonePage(message, session) {
1898
+ async handleDomRefPoint(message, session) {
1899
+ const resolved = this.resolveRefFromPayload(session, message.payload, message);
1900
+ if (!resolved)
1901
+ return;
1902
+ const point = await this.resolveRefPoint(resolved);
1903
+ this.sendResponse(message, point);
1904
+ }
1905
+ async handleCanvasOverlayMount(message, session) {
899
1906
  const payload = isRecord(message.payload) ? message.payload : {};
1907
+ const mountId = typeof payload.mountId === "string" && payload.mountId.trim().length > 0
1908
+ ? payload.mountId.trim()
1909
+ : `mount_${createId()}`;
1910
+ const title = typeof payload.title === "string" && payload.title.trim().length > 0
1911
+ ? payload.title.trim()
1912
+ : "OpenDevBrowser Canvas";
1913
+ const prototypeId = typeof payload.prototypeId === "string" && payload.prototypeId.trim().length > 0
1914
+ ? payload.prototypeId.trim()
1915
+ : "prototype";
900
1916
  const target = this.requireActiveTarget(session, message);
901
1917
  if (!target)
902
1918
  return;
903
- const capture = await this.dom.captureDom(target.tabId, "body", {
904
- sanitize: payload.sanitize !== false,
905
- maxNodes: typeof payload.maxNodes === "number" ? payload.maxNodes : undefined,
906
- inlineStyles: payload.inlineStyles !== false,
907
- styleAllowlist: Array.isArray(payload.styleAllowlist) ? payload.styleAllowlist.filter((item) => typeof item === "string") : [],
908
- skipStyleValues: Array.isArray(payload.skipStyleValues) ? payload.skipStyleValues.filter((item) => typeof item === "string") : []
1919
+ const selection = parseCanvasOverlaySelection(payload.selection, target.targetId);
1920
+ const result = await this.dom.mountCanvasOverlay(target.tabId, {
1921
+ mountId,
1922
+ title,
1923
+ prototypeId,
1924
+ selection
1925
+ });
1926
+ this.sendResponse(message, {
1927
+ mountId,
1928
+ targetId: target.targetId,
1929
+ previewState: "background",
1930
+ overlayState: result.overlayState ?? "mounted",
1931
+ capabilities: { selection: true, guides: true }
909
1932
  });
910
- this.sendResponse(message, { capture });
911
1933
  }
912
- async handleCloneComponent(message, session) {
1934
+ async handleCanvasOverlayUnmount(message, session) {
913
1935
  const payload = isRecord(message.payload) ? message.payload : {};
914
- const ref = typeof payload.ref === "string" ? payload.ref : null;
915
- if (!ref) {
916
- this.sendError(message, buildError("invalid_request", "Missing ref", false));
1936
+ const mountId = typeof payload.mountId === "string" ? payload.mountId.trim() : "";
1937
+ if (!mountId) {
1938
+ this.sendError(message, buildError("invalid_request", "Missing mountId", false));
917
1939
  return;
918
1940
  }
919
- const selector = this.resolveSelector(session, ref, message);
920
- if (!selector)
921
- return;
922
1941
  const target = this.requireActiveTarget(session, message);
923
1942
  if (!target)
924
1943
  return;
925
- const capture = await this.dom.captureDom(target.tabId, selector, {
926
- sanitize: payload.sanitize !== false,
927
- maxNodes: typeof payload.maxNodes === "number" ? payload.maxNodes : undefined,
928
- inlineStyles: payload.inlineStyles !== false,
929
- styleAllowlist: Array.isArray(payload.styleAllowlist) ? payload.styleAllowlist.filter((item) => typeof item === "string") : [],
930
- skipStyleValues: Array.isArray(payload.skipStyleValues) ? payload.skipStyleValues.filter((item) => typeof item === "string") : []
1944
+ await this.dom.unmountCanvasOverlay(target.tabId, mountId);
1945
+ this.sendResponse(message, {
1946
+ ok: true,
1947
+ mountId,
1948
+ targetId: target.targetId,
1949
+ overlayState: "idle"
931
1950
  });
932
- this.sendResponse(message, { capture });
933
1951
  }
934
- async handlePerf(message, session) {
1952
+ async handleCanvasOverlaySelect(message, session) {
1953
+ const payload = isRecord(message.payload) ? message.payload : {};
1954
+ const mountId = typeof payload.mountId === "string" ? payload.mountId.trim() : "";
1955
+ const nodeId = typeof payload.nodeId === "string" && payload.nodeId.trim().length > 0
1956
+ ? payload.nodeId.trim()
1957
+ : null;
1958
+ const selectionHint = isRecord(payload.selectionHint) ? payload.selectionHint : {};
1959
+ if (!mountId || (!nodeId && Object.keys(selectionHint).length === 0)) {
1960
+ this.sendError(message, buildError("invalid_request", "Missing mountId or selection target", false));
1961
+ return;
1962
+ }
935
1963
  const target = this.requireActiveTarget(session, message);
936
1964
  if (!target)
937
1965
  return;
938
- const result = await this.cdp.sendCommand({ tabId: target.tabId }, "Performance.getMetrics", {});
1966
+ const selection = await this.dom.selectCanvasOverlay(target.tabId, { nodeId, selectionHint });
1967
+ this.sendResponse(message, {
1968
+ mountId,
1969
+ targetId: target.targetId,
1970
+ selection
1971
+ });
1972
+ }
1973
+ async handleCanvasOverlaySync(message, session) {
1974
+ const payload = isRecord(message.payload) ? message.payload : {};
1975
+ const mountId = typeof payload.mountId === "string" ? payload.mountId.trim() : "";
1976
+ if (!mountId) {
1977
+ this.sendError(message, buildError("invalid_request", "Missing mountId", false));
1978
+ return;
1979
+ }
1980
+ const title = typeof payload.title === "string" && payload.title.trim().length > 0
1981
+ ? payload.title.trim()
1982
+ : "OpenDevBrowser Canvas";
1983
+ const target = this.requireActiveTarget(session, message);
1984
+ if (!target)
1985
+ return;
1986
+ const selection = parseCanvasOverlaySelection(payload.selection, target.targetId);
1987
+ const result = await this.dom.syncCanvasOverlay(target.tabId, {
1988
+ mountId,
1989
+ title,
1990
+ selection
1991
+ });
1992
+ this.sendResponse(message, {
1993
+ ok: true,
1994
+ mountId,
1995
+ targetId: target.targetId,
1996
+ overlayState: result.overlayState ?? "mounted"
1997
+ });
1998
+ }
1999
+ async handleCanvasRuntimePreviewBridge(message, session) {
2000
+ const payload = isRecord(message.payload) ? message.payload : {};
2001
+ const bindingId = typeof payload.bindingId === "string" ? payload.bindingId.trim() : "";
2002
+ const rootSelector = typeof payload.rootSelector === "string" ? payload.rootSelector.trim() : "";
2003
+ const html = typeof payload.html === "string" ? payload.html : "";
2004
+ if (!bindingId || !rootSelector) {
2005
+ this.sendError(message, buildError("invalid_request", "Missing bindingId or rootSelector", false));
2006
+ return;
2007
+ }
2008
+ const target = this.requireActiveTarget(session, message);
2009
+ if (!target)
2010
+ return;
2011
+ const result = await this.dom.applyRuntimePreviewBridge(target.tabId, bindingId, rootSelector, html);
2012
+ this.sendResponse(message, result);
2013
+ }
2014
+ async handleClonePage(message, session) {
2015
+ const payload = isRecord(message.payload) ? message.payload : {};
2016
+ const target = this.requireActiveTarget(session, message);
2017
+ if (!target)
2018
+ return;
2019
+ const canvasCapture = await this.captureCanvasPage(target.tabId, target.targetId);
2020
+ if (canvasCapture) {
2021
+ this.sendResponse(message, { capture: canvasCapture });
2022
+ return;
2023
+ }
2024
+ const capture = await this.dom.captureDom(target.tabId, "body", {
2025
+ sanitize: payload.sanitize !== false,
2026
+ maxNodes: typeof payload.maxNodes === "number" ? payload.maxNodes : undefined,
2027
+ inlineStyles: payload.inlineStyles !== false,
2028
+ styleAllowlist: Array.isArray(payload.styleAllowlist) ? payload.styleAllowlist.filter((item) => typeof item === "string") : [],
2029
+ skipStyleValues: Array.isArray(payload.skipStyleValues) ? payload.skipStyleValues.filter((item) => typeof item === "string") : []
2030
+ });
2031
+ this.sendResponse(message, { capture });
2032
+ }
2033
+ async handleCloneComponent(message, session) {
2034
+ const payload = isRecord(message.payload) ? message.payload : {};
2035
+ const ref = typeof payload.ref === "string" ? payload.ref : null;
2036
+ if (!ref) {
2037
+ this.sendError(message, buildError("invalid_request", "Missing ref", false));
2038
+ return;
2039
+ }
2040
+ const selector = this.resolveSelector(session, ref, message);
2041
+ if (!selector)
2042
+ return;
2043
+ const target = this.requireActiveTarget(session, message);
2044
+ if (!target)
2045
+ return;
2046
+ const capture = await this.dom.captureDom(target.tabId, selector, {
2047
+ sanitize: payload.sanitize !== false,
2048
+ maxNodes: typeof payload.maxNodes === "number" ? payload.maxNodes : undefined,
2049
+ inlineStyles: payload.inlineStyles !== false,
2050
+ styleAllowlist: Array.isArray(payload.styleAllowlist) ? payload.styleAllowlist.filter((item) => typeof item === "string") : [],
2051
+ skipStyleValues: Array.isArray(payload.skipStyleValues) ? payload.skipStyleValues.filter((item) => typeof item === "string") : []
2052
+ });
2053
+ this.sendResponse(message, { capture });
2054
+ }
2055
+ async handlePerf(message, session) {
2056
+ const target = this.requireActiveTarget(session, message);
2057
+ if (!target)
2058
+ return;
2059
+ const result = await this.cdp.sendCommand(target.debuggee, "Performance.getMetrics", {});
939
2060
  this.sendResponse(message, { metrics: Array.isArray(result.metrics) ? result.metrics : [] });
940
2061
  }
941
2062
  async handleScreenshot(message, session) {
2063
+ const payload = isRecord(message.payload) ? message.payload : {};
2064
+ if (payload.fullPage === true && typeof payload.ref === "string") {
2065
+ this.sendError(message, buildError("invalid_request", "ref and fullPage cannot be combined.", false));
2066
+ return;
2067
+ }
2068
+ if (typeof payload.ref === "string") {
2069
+ const resolved = this.resolveRefFromPayload(session, payload, message);
2070
+ if (!resolved)
2071
+ return;
2072
+ try {
2073
+ await this.callFunctionOnRef(resolved, DOM_SCROLL_INTO_VIEW_DECLARATION);
2074
+ const clip = await this.callFunctionOnRef(resolved, DOM_SCREENSHOT_CLIP_DECLARATION);
2075
+ const result = await withTimeout(this.cdp.sendCommand(resolved.target.debuggee, "Page.captureScreenshot", {
2076
+ format: "png",
2077
+ captureBeyondViewport: true,
2078
+ clip: this.normalizeScreenshotClip(clip, resolved.ref)
2079
+ }), SCREENSHOT_TIMEOUT_MS, "Ops screenshot timed out");
2080
+ if (result?.data) {
2081
+ this.sendResponse(message, { base64: result.data });
2082
+ return;
2083
+ }
2084
+ }
2085
+ catch (error) {
2086
+ logError("ops.screenshot.ref", error, { code: "screenshot_failed" });
2087
+ }
2088
+ this.sendError(message, buildError("execution_failed", "Screenshot failed", false));
2089
+ return;
2090
+ }
942
2091
  const target = this.requireActiveTarget(session, message);
943
2092
  if (!target)
944
2093
  return;
2094
+ if (payload.fullPage === true) {
2095
+ try {
2096
+ const metrics = await this.cdp.sendCommand(target.debuggee, "Page.getLayoutMetrics", {});
2097
+ const contentSize = isRecord(metrics.cssContentSize) ? metrics.cssContentSize : metrics.contentSize;
2098
+ const width = typeof contentSize?.width === "number" && Number.isFinite(contentSize.width)
2099
+ ? Math.max(1, Math.ceil(contentSize.width))
2100
+ : null;
2101
+ const height = typeof contentSize?.height === "number" && Number.isFinite(contentSize.height)
2102
+ ? Math.max(1, Math.ceil(contentSize.height))
2103
+ : null;
2104
+ if (width === null || height === null) {
2105
+ throw new Error("Full-page screenshot metrics unavailable");
2106
+ }
2107
+ const result = await withTimeout(this.cdp.sendCommand(target.debuggee, "Page.captureScreenshot", {
2108
+ format: "png",
2109
+ captureBeyondViewport: true,
2110
+ clip: { x: 0, y: 0, width, height, scale: 1 }
2111
+ }), SCREENSHOT_TIMEOUT_MS, "Ops screenshot timed out");
2112
+ if (result?.data) {
2113
+ this.sendResponse(message, { base64: result.data });
2114
+ return;
2115
+ }
2116
+ }
2117
+ catch (error) {
2118
+ logError("ops.screenshot.full_page", error, { code: "screenshot_failed" });
2119
+ }
2120
+ this.sendError(message, buildError("execution_failed", "Screenshot failed", false));
2121
+ return;
2122
+ }
945
2123
  try {
946
- const result = await withTimeout(this.cdp.sendCommand({ tabId: target.tabId }, "Page.captureScreenshot", { format: "png" }), SCREENSHOT_TIMEOUT_MS, "Ops screenshot timed out");
2124
+ const result = await withTimeout(this.cdp.sendCommand(target.debuggee, "Page.captureScreenshot", { format: "png" }), SCREENSHOT_TIMEOUT_MS, "Ops screenshot timed out");
947
2125
  if (result?.data) {
948
2126
  this.sendResponse(message, { base64: result.data });
949
2127
  return;
@@ -959,6 +2137,102 @@ export class OpsRuntime {
959
2137
  }
960
2138
  this.sendError(message, buildError("execution_failed", "Screenshot failed", false));
961
2139
  }
2140
+ async handleUpload(message, session) {
2141
+ const payload = isRecord(message.payload) ? message.payload : {};
2142
+ const files = Array.isArray(payload.files)
2143
+ ? payload.files.filter((value) => typeof value === "string" && value.trim().length > 0)
2144
+ : [];
2145
+ if (files.length === 0) {
2146
+ this.sendError(message, buildError("invalid_request", "Missing files", false));
2147
+ return;
2148
+ }
2149
+ const resolved = this.resolveRefFromPayload(session, payload, message);
2150
+ if (!resolved)
2151
+ return;
2152
+ try {
2153
+ const info = await this.callFunctionOnRef(resolved, DOM_FILE_INPUT_INFO_DECLARATION);
2154
+ if (info?.disabled === true) {
2155
+ this.sendError(message, buildError("execution_failed", `Cannot upload files to disabled ref: ${resolved.ref}`, false));
2156
+ return;
2157
+ }
2158
+ if (info?.isFileInput === true) {
2159
+ await this.cdp.sendCommand(resolved.target.debuggee, "DOM.setFileInputFiles", {
2160
+ backendNodeId: resolved.backendNodeId,
2161
+ files
2162
+ });
2163
+ this.sendResponse(message, {
2164
+ targetId: resolved.target.targetId,
2165
+ fileCount: files.length,
2166
+ mode: "direct_input"
2167
+ });
2168
+ return;
2169
+ }
2170
+ this.sessions.clearFileChooser(session.id, resolved.target.targetId);
2171
+ await this.cdp.sendCommand(resolved.target.debuggee, "Page.setInterceptFileChooserDialog", { enabled: true });
2172
+ await this.callFunctionOnRef(resolved, DOM_SCROLL_INTO_VIEW_DECLARATION);
2173
+ const point = await this.resolveRefPoint(resolved);
2174
+ await this.dispatchMouseEvent(resolved.target.debuggee, "mouseMoved", point.x, point.y);
2175
+ await this.dispatchMouseEvent(resolved.target.debuggee, "mousePressed", point.x, point.y, {
2176
+ button: "left",
2177
+ clickCount: 1
2178
+ });
2179
+ await this.dispatchMouseEvent(resolved.target.debuggee, "mouseReleased", point.x, point.y, {
2180
+ button: "left",
2181
+ clickCount: 1
2182
+ });
2183
+ const chooser = await this.waitForFileChooser(session.id, resolved.target.targetId);
2184
+ if (typeof chooser.backendNodeId !== "number") {
2185
+ throw new Error("File chooser opened without backend node id");
2186
+ }
2187
+ await this.cdp.sendCommand(resolved.target.debuggee, "DOM.setFileInputFiles", {
2188
+ backendNodeId: chooser.backendNodeId,
2189
+ files
2190
+ });
2191
+ this.sessions.clearFileChooser(session.id, resolved.target.targetId);
2192
+ this.sendResponse(message, {
2193
+ targetId: resolved.target.targetId,
2194
+ fileCount: files.length,
2195
+ mode: "file_chooser"
2196
+ });
2197
+ }
2198
+ catch (error) {
2199
+ this.sessions.clearFileChooser(session.id, resolved.target.targetId);
2200
+ logError("ops.upload", error, { code: "upload_failed" });
2201
+ this.sendError(message, buildError("execution_failed", error instanceof Error ? error.message : "Upload failed", false));
2202
+ }
2203
+ }
2204
+ async handleDialog(message, session) {
2205
+ const payload = isRecord(message.payload) ? message.payload : {};
2206
+ const action = payload.action === "accept" || payload.action === "dismiss" || payload.action === "status"
2207
+ ? payload.action
2208
+ : "status";
2209
+ const target = this.requireActiveTarget(session, message);
2210
+ if (!target)
2211
+ return;
2212
+ const dialog = this.serializeDialogState(session, target.targetId);
2213
+ if (!dialog.open || action === "status") {
2214
+ this.sendResponse(message, {
2215
+ dialog,
2216
+ ...(action === "status" ? {} : { handled: false })
2217
+ });
2218
+ return;
2219
+ }
2220
+ try {
2221
+ await this.cdp.sendCommand(target.debuggee, "Page.handleJavaScriptDialog", {
2222
+ accept: action === "accept",
2223
+ ...(action === "accept" && typeof payload.promptText === "string" ? { promptText: payload.promptText } : {})
2224
+ });
2225
+ this.sessions.clearDialog(session.id, target.targetId);
2226
+ this.sendResponse(message, {
2227
+ dialog: { open: false, targetId: target.targetId },
2228
+ handled: true
2229
+ });
2230
+ }
2231
+ catch (error) {
2232
+ logError("ops.dialog", error, { code: "dialog_failed" });
2233
+ this.sendError(message, buildError("execution_failed", error instanceof Error ? error.message : "Dialog handling failed", false));
2234
+ }
2235
+ }
962
2236
  async handleConsolePoll(message, session) {
963
2237
  const payload = isRecord(message.payload) ? message.payload : {};
964
2238
  const sinceSeq = typeof payload.sinceSeq === "number" ? payload.sinceSeq : 0;
@@ -1011,7 +2285,7 @@ export class OpsRuntime {
1011
2285
  if (!target)
1012
2286
  return;
1013
2287
  try {
1014
- await this.cdp.sendCommand({ tabId: target.tabId }, "Network.setCookies", { cookies: normalized });
2288
+ await this.cdp.sendCommand(target.debuggee, "Network.setCookies", { cookies: normalized });
1015
2289
  }
1016
2290
  catch (error) {
1017
2291
  const detail = error instanceof Error ? error.message : "Cookie import failed";
@@ -1044,7 +2318,7 @@ export class OpsRuntime {
1044
2318
  return;
1045
2319
  let rawCookies = [];
1046
2320
  try {
1047
- const response = await this.cdp.sendCommand({ tabId: target.tabId }, "Network.getCookies", urls ? { urls } : {});
2321
+ const response = await this.cdp.sendCommand(target.debuggee, "Network.getCookies", urls ? { urls } : {});
1048
2322
  rawCookies = Array.isArray(response.cookies) ? response.cookies : [];
1049
2323
  }
1050
2324
  catch (error) {
@@ -1061,20 +2335,323 @@ export class OpsRuntime {
1061
2335
  count: cookies.length
1062
2336
  });
1063
2337
  }
1064
- async enableSessionDomains(session) {
2338
+ async attachTargetTab(tabId) {
2339
+ try {
2340
+ await this.cdp.attach(tabId);
2341
+ }
2342
+ catch (error) {
2343
+ if (isAttachBlockedError(error)) {
2344
+ await delay(50);
2345
+ try {
2346
+ await this.cdp.attach(tabId);
2347
+ return;
2348
+ }
2349
+ catch (retryError) {
2350
+ error = retryError;
2351
+ }
2352
+ }
2353
+ const diagnostic = this.cdp.getLastRootAttachDiagnostic?.(tabId) ?? null;
2354
+ const detail = error instanceof Error ? error.message : "Debugger attach failed";
2355
+ logError("ops.direct_attach_stage", error instanceof Error ? error : new Error(detail), {
2356
+ code: "direct_attach_stage",
2357
+ extra: {
2358
+ tabId,
2359
+ ...(this.toDirectAttachDiagnosticDetails(diagnostic) ?? {}),
2360
+ ...(!diagnostic ? { reason: detail } : {})
2361
+ }
2362
+ });
2363
+ throw this.decorateDirectAttachError(error, diagnostic);
2364
+ }
2365
+ }
2366
+ async attachCreatedTargetTab(tabId) {
2367
+ try {
2368
+ await this.attachTargetTab(tabId);
2369
+ return;
2370
+ }
2371
+ catch (error) {
2372
+ if (!isAttachBlockedError(error)) {
2373
+ throw error;
2374
+ }
2375
+ }
2376
+ await this.tabs.waitForTabComplete(tabId).catch(() => undefined);
2377
+ if (this.hasAttachedTabDebuggee(tabId)) {
2378
+ return;
2379
+ }
2380
+ this.cdp.markClientClosed();
2381
+ await this.attachTargetTab(tabId);
2382
+ }
2383
+ async attachStartUrlConnectTab(tabId) {
2384
+ return this.attachLaunchTargetTab(tabId, true);
2385
+ }
2386
+ async attachLaunchTargetTab(tabId, waitForTabCompleteBeforeRetry) {
2387
+ try {
2388
+ await this.attachTargetTab(tabId);
2389
+ return null;
2390
+ }
2391
+ catch (error) {
2392
+ if (!isAttachBlockedError(error)) {
2393
+ throw error;
2394
+ }
2395
+ }
2396
+ this.cdp.markClientClosed();
2397
+ if (waitForTabCompleteBeforeRetry) {
2398
+ await this.tabs.waitForTabComplete(tabId).catch(() => undefined);
2399
+ }
2400
+ const refreshedTab = waitForTabCompleteBeforeRetry
2401
+ ? await this.tabs.getTab(tabId)
2402
+ : null;
2403
+ try {
2404
+ await this.attachTargetTab(tabId);
2405
+ return refreshedTab ?? null;
2406
+ }
2407
+ catch (error) {
2408
+ if (!isAttachBlockedError(error) || typeof this.cdp.refreshTabAttachment !== "function") {
2409
+ throw error;
2410
+ }
2411
+ await this.cdp.refreshTabAttachment(tabId);
2412
+ await this.resolveReadyTabDebuggee(tabId, { strict: true, allowRefresh: false });
2413
+ return await this.tabs.getTab(tabId) ?? refreshedTab ?? null;
2414
+ }
2415
+ }
2416
+ async getCreatedTabSeed(tab) {
2417
+ const refreshedTab = typeof tab.id === "number"
2418
+ ? await this.tabs.getTab(tab.id)
2419
+ : null;
2420
+ return getReportedTabSeed(refreshedTab ?? tab);
2421
+ }
2422
+ async claimCommandCreatedTab(session, tab) {
2423
+ const tabId = typeof tab.id === "number" ? tab.id : null;
2424
+ if (tabId === null) {
2425
+ return null;
2426
+ }
2427
+ const currentOwner = this.sessions.getByTabId(tabId);
2428
+ if (currentOwner && currentOwner.id !== session.id) {
2429
+ this.sessions.removeTargetByTabId(currentOwner.id, tabId);
2430
+ }
2431
+ const existingTargetId = this.sessions.getTargetIdByTabId(session.id, tabId);
2432
+ if (!existingTargetId) {
2433
+ return null;
2434
+ }
2435
+ const target = session.targets.get(existingTargetId) ?? null;
2436
+ if (!target) {
2437
+ return null;
2438
+ }
2439
+ const seed = await this.getCreatedTabSeed(tab);
2440
+ if (typeof seed.url === "string" && seed.url.length > 0) {
2441
+ target.url = seed.url;
2442
+ }
2443
+ if (typeof seed.title === "string" && seed.title.length > 0) {
2444
+ target.title = seed.title;
2445
+ }
2446
+ target.openerTargetId = undefined;
2447
+ return target;
2448
+ }
2449
+ async activateCreatedTab(tabId) {
2450
+ await this.tabs.activateTab(tabId).catch(() => undefined);
2451
+ }
2452
+ rememberCommandCreatedTab(sessionId, tabId, kind) {
2453
+ this.commandCreatedTabs.set(tabId, { sessionId, kind });
2454
+ }
2455
+ isCommandCreatedTab(tabId, sessionId) {
2456
+ const entry = this.commandCreatedTabs.get(tabId);
2457
+ return sessionId ? entry?.sessionId === sessionId : Boolean(entry);
2458
+ }
2459
+ forgetCommandCreatedTab(tabId) {
2460
+ this.commandCreatedTabs.delete(tabId);
2461
+ }
2462
+ isConcreteDebuggee(debuggee) {
2463
+ return Boolean(debuggee
2464
+ && ((typeof debuggee.sessionId === "string" && debuggee.sessionId.length > 0)
2465
+ || (typeof debuggee.targetId === "string" && debuggee.targetId.length > 0)));
2466
+ }
2467
+ hasAttachedSessionDebuggee(debuggee) {
2468
+ return Boolean(debuggee && typeof debuggee.sessionId === "string" && debuggee.sessionId.length > 0);
2469
+ }
2470
+ hasAttachedTabDebuggee(tabId) {
2471
+ if (typeof this.cdp.isTabAttached === "function") {
2472
+ return this.cdp.isTabAttached(tabId);
2473
+ }
2474
+ if (typeof this.cdp.getAttachedTabIds === "function") {
2475
+ return this.cdp.getAttachedTabIds().includes(tabId);
2476
+ }
2477
+ return false;
2478
+ }
2479
+ async resolveReadyTabDebuggee(tabId, options) {
2480
+ const readDebuggee = () => this.cdp.getTabDebuggee?.(tabId);
2481
+ let pageDebuggee = readDebuggee();
2482
+ if (this.hasAttachedSessionDebuggee(pageDebuggee)) {
2483
+ return pageDebuggee;
2484
+ }
2485
+ if (!options.strict && this.isConcreteDebuggee(pageDebuggee)) {
2486
+ return pageDebuggee;
2487
+ }
2488
+ await this.cdp.primeAttachedRootSession?.(tabId);
2489
+ pageDebuggee = readDebuggee();
2490
+ if (this.hasAttachedSessionDebuggee(pageDebuggee)) {
2491
+ return pageDebuggee;
2492
+ }
2493
+ if (!options.strict && this.isConcreteDebuggee(pageDebuggee)) {
2494
+ return pageDebuggee;
2495
+ }
2496
+ if (options.allowRefresh && typeof this.cdp.refreshTabAttachment === "function") {
2497
+ await this.cdp.refreshTabAttachment(tabId);
2498
+ await this.cdp.primeAttachedRootSession?.(tabId);
2499
+ pageDebuggee = readDebuggee();
2500
+ if (this.hasAttachedSessionDebuggee(pageDebuggee)) {
2501
+ return pageDebuggee;
2502
+ }
2503
+ if (!options.strict && this.isConcreteDebuggee(pageDebuggee)) {
2504
+ return pageDebuggee;
2505
+ }
2506
+ }
2507
+ if (this.isConcreteDebuggee(pageDebuggee)) {
2508
+ return pageDebuggee;
2509
+ }
2510
+ if (options.strict) {
2511
+ throw new Error(`Concrete debugger session unavailable for tab ${tabId}.`);
2512
+ }
2513
+ return pageDebuggee ?? { tabId };
2514
+ }
2515
+ async enableTargetDomains(tabId, strict = false) {
2516
+ const buildEnablementFailureDetails = (allowRefresh, refreshedAfterBlock, enablementStage) => ({
2517
+ phase: "strict_enablement",
2518
+ enablementStage,
2519
+ tabId,
2520
+ strict,
2521
+ allowRefresh,
2522
+ refreshedAfterBlock
2523
+ });
2524
+ const enableOnce = async (allowRefresh, refreshedAfterBlock) => {
2525
+ let pageDebuggee;
2526
+ try {
2527
+ pageDebuggee = await this.resolveReadyTabDebuggee(tabId, {
2528
+ strict,
2529
+ allowRefresh
2530
+ });
2531
+ }
2532
+ catch (error) {
2533
+ throw this.decorateCdpFailure(error, buildEnablementFailureDetails(allowRefresh, refreshedAfterBlock, "resolve_ready_debuggee"));
2534
+ }
2535
+ await this.enableRootTracking(buildEnablementFailureDetails(allowRefresh, refreshedAfterBlock, "configure_auto_attach"));
2536
+ await this.enableTargetDomainsOnDebuggee(pageDebuggee, buildEnablementFailureDetails(allowRefresh, refreshedAfterBlock, "page_enable"));
2537
+ await this.enableTargetDiscovery(buildEnablementFailureDetails(allowRefresh, refreshedAfterBlock, "set_discover_targets"));
2538
+ };
2539
+ try {
2540
+ try {
2541
+ await enableOnce(strict, false);
2542
+ }
2543
+ catch (error) {
2544
+ if (!strict || !isAttachBlockedError(error) || typeof this.cdp.refreshTabAttachment !== "function") {
2545
+ throw error;
2546
+ }
2547
+ this.cdp.markClientClosed?.();
2548
+ await this.cdp.refreshTabAttachment(tabId);
2549
+ await enableOnce(false, true);
2550
+ }
2551
+ }
2552
+ catch (error) {
2553
+ logError("ops.enable_domains", error, { code: "enable_domains_failed", extra: { tabId, strict } });
2554
+ if (strict) {
2555
+ throw error;
2556
+ }
2557
+ }
2558
+ }
2559
+ async enableRootTracking(baseDetails) {
2560
+ try {
2561
+ await this.cdp.configureAutoAttach?.({
2562
+ autoAttach: true,
2563
+ waitForDebuggerOnStart: false,
2564
+ flatten: true
2565
+ });
2566
+ }
2567
+ catch (error) {
2568
+ throw this.decorateCdpFailure(error, {
2569
+ ...baseDetails,
2570
+ enablementStage: "configure_auto_attach"
2571
+ });
2572
+ }
2573
+ }
2574
+ async enableTargetDiscovery(baseDetails) {
1065
2575
  try {
1066
- await this.cdp.sendCommand({ tabId: session.tabId }, "Runtime.enable", {});
1067
- await this.cdp.sendCommand({ tabId: session.tabId }, "Network.enable", {});
1068
- await this.cdp.sendCommand({ tabId: session.tabId }, "Performance.enable", {});
2576
+ await this.cdp.setDiscoverTargetsEnabled?.(true);
1069
2577
  }
1070
2578
  catch (error) {
1071
- logError("ops.enable_domains", error, { code: "enable_domains_failed" });
2579
+ logError("ops.discover_targets", error, {
2580
+ code: "discover_targets_enable_failed",
2581
+ extra: baseDetails
2582
+ });
1072
2583
  }
1073
2584
  }
2585
+ async enableTargetDomainsOnDebuggee(debuggee, baseDetails) {
2586
+ const enableCommands = [
2587
+ { method: "Page.enable", params: {}, stage: "page_enable" },
2588
+ { method: "Page.setInterceptFileChooserDialog", params: { enabled: true }, stage: "page_file_chooser" },
2589
+ { method: "Runtime.enable", params: {}, stage: "runtime_enable" },
2590
+ { method: "Network.enable", params: {}, stage: "network_enable" },
2591
+ { method: "Performance.enable", params: {}, stage: "performance_enable" }
2592
+ ];
2593
+ for (const command of enableCommands) {
2594
+ try {
2595
+ await this.cdp.sendCommand(debuggee, command.method, command.params);
2596
+ }
2597
+ catch (error) {
2598
+ throw this.decorateCdpFailure(error, {
2599
+ ...baseDetails,
2600
+ enablementStage: command.stage
2601
+ });
2602
+ }
2603
+ }
2604
+ }
2605
+ parsePointerCoords(payload) {
2606
+ const x = typeof payload.x === "number" && Number.isFinite(payload.x) ? payload.x : null;
2607
+ const y = typeof payload.y === "number" && Number.isFinite(payload.y) ? payload.y : null;
2608
+ return x === null || y === null ? null : { x, y };
2609
+ }
2610
+ parsePointerButton(value) {
2611
+ return value === "middle" || value === "right" ? value : "left";
2612
+ }
2613
+ async dispatchMouseEvent(debuggee, type, x, y, options = {}) {
2614
+ if (type === "mouseMoved" && options.steps && options.steps > 1) {
2615
+ const stepCount = Math.max(1, options.steps);
2616
+ for (let index = 1; index <= stepCount; index += 1) {
2617
+ await this.cdp.sendCommand(debuggee, "Input.dispatchMouseEvent", {
2618
+ type,
2619
+ x,
2620
+ y,
2621
+ button: options.button ?? "none",
2622
+ clickCount: options.clickCount ?? 0
2623
+ });
2624
+ }
2625
+ return;
2626
+ }
2627
+ await this.cdp.sendCommand(debuggee, "Input.dispatchMouseEvent", {
2628
+ type,
2629
+ x,
2630
+ y,
2631
+ button: options.button ?? (type === "mouseMoved" ? "none" : "left"),
2632
+ clickCount: options.clickCount ?? (type === "mouseMoved" ? 0 : 1)
2633
+ });
2634
+ }
2635
+ async dispatchKeyPress(debuggee, key) {
2636
+ const text = key.length === 1 ? key : undefined;
2637
+ await this.cdp.sendCommand(debuggee, "Input.dispatchKeyEvent", {
2638
+ type: "keyDown",
2639
+ key,
2640
+ ...(text ? { text } : {})
2641
+ });
2642
+ await this.cdp.sendCommand(debuggee, "Input.dispatchKeyEvent", {
2643
+ type: "keyUp",
2644
+ key
2645
+ });
2646
+ }
1074
2647
  async withSession(message, clientId, handler) {
1075
2648
  const session = this.getSessionForMessage(message, clientId);
1076
2649
  if (!session)
1077
2650
  return;
2651
+ if (DIALOG_SCOPED_COMMANDS.has(message.command)) {
2652
+ await this.withDialogQueue(message, session, handler);
2653
+ return;
2654
+ }
1078
2655
  if (!TARGET_SCOPED_COMMANDS.has(message.command)) {
1079
2656
  session.queue = session.queue.then(() => handler(session), () => handler(session));
1080
2657
  await session.queue;
@@ -1096,6 +2673,9 @@ export class OpsRuntime {
1096
2673
  const requested = typeof payload.targetId === "string" ? payload.targetId.trim() : "";
1097
2674
  return requested || session.activeTargetId || session.targetId;
1098
2675
  }
2676
+ dialogQueueKey(sessionId, targetId) {
2677
+ return `${sessionId}:${targetId}`;
2678
+ }
1099
2679
  sessionQueueAgeMs(session) {
1100
2680
  let oldest = null;
1101
2681
  for (const value of session.targetQueueOldestAt.values()) {
@@ -1245,66 +2825,950 @@ export class OpsRuntime {
1245
2825
  }
1246
2826
  }
1247
2827
  }
1248
- getSessionForMessage(message, clientId) {
1249
- const opsSessionId = message.opsSessionId;
1250
- if (!opsSessionId) {
1251
- this.sendError(message, buildError("invalid_request", "Missing opsSessionId", false));
2828
+ async withDialogQueue(message, session, handler) {
2829
+ const targetId = this.resolveTargetIdForQueue(session, message);
2830
+ const queueKey = this.dialogQueueKey(session.id, targetId);
2831
+ const previous = this.dialogQueues.get(queueKey) ?? Promise.resolve();
2832
+ let releaseQueue = () => { };
2833
+ const gate = new Promise((resolve) => {
2834
+ releaseQueue = resolve;
2835
+ });
2836
+ const tail = previous.then(() => gate, () => gate);
2837
+ this.dialogQueues.set(queueKey, tail);
2838
+ await previous;
2839
+ try {
2840
+ await handler(session);
2841
+ }
2842
+ finally {
2843
+ releaseQueue();
2844
+ if (this.dialogQueues.get(queueKey) === tail) {
2845
+ this.dialogQueues.delete(queueKey);
2846
+ }
2847
+ }
2848
+ }
2849
+ getSessionForMessage(message, clientId) {
2850
+ const opsSessionId = message.opsSessionId;
2851
+ if (!opsSessionId) {
2852
+ this.sendError(message, buildError("invalid_request", "Missing opsSessionId", false));
2853
+ return null;
2854
+ }
2855
+ const session = this.sessions.get(opsSessionId);
2856
+ if (!session) {
2857
+ this.sendError(message, buildError("invalid_session", "Unknown ops session", false));
2858
+ return null;
2859
+ }
2860
+ const leaseId = typeof message.leaseId === "string" ? message.leaseId : "";
2861
+ if (session.ownerClientId !== clientId) {
2862
+ if (leaseId && leaseId === session.leaseId) {
2863
+ this.reclaimSession(session, clientId);
2864
+ }
2865
+ else {
2866
+ this.sendError(message, buildError("not_owner", "Client does not own session", false));
2867
+ return null;
2868
+ }
2869
+ }
2870
+ else if (session.state === "closing") {
2871
+ if (leaseId && leaseId === session.leaseId) {
2872
+ this.reclaimSession(session, clientId);
2873
+ }
2874
+ else {
2875
+ this.sendError(message, buildError("not_owner", "Client does not own session", false));
2876
+ return null;
2877
+ }
2878
+ }
2879
+ if (leaseId !== session.leaseId) {
2880
+ this.sendError(message, buildError("not_owner", "Lease does not match session owner", false));
2881
+ return null;
2882
+ }
2883
+ session.lastUsedAt = Date.now();
2884
+ return session;
2885
+ }
2886
+ requestedTargetId(session, message) {
2887
+ return this.extractPayloadTargetId(message.payload) ?? session.activeTargetId ?? session.targetId;
2888
+ }
2889
+ hasOpsTarget(session, targetId) {
2890
+ return session.targets.has(targetId) || this.sessions.getSyntheticTarget(session.id, targetId) !== null;
2891
+ }
2892
+ resolveTargetContext(session, targetId) {
2893
+ const target = session.targets.get(targetId) ?? null;
2894
+ const explicitSynthetic = this.sessions.getSyntheticTarget(session.id, targetId);
2895
+ const bridgeSynthetic = explicitSynthetic ? null : this.findSyntheticSessionBridge(session, target);
2896
+ const synthetic = explicitSynthetic ?? bridgeSynthetic;
2897
+ if (!target && !synthetic) {
2898
+ return null;
2899
+ }
2900
+ const targetTabId = target?.tabId ?? synthetic?.tabId ?? session.tabId;
2901
+ const baseType = synthetic?.type ?? "page";
2902
+ return {
2903
+ targetId,
2904
+ tabId: targetTabId,
2905
+ type: baseType,
2906
+ synthetic: explicitSynthetic !== null && !session.targets.has(targetId),
2907
+ ...(explicitSynthetic?.url ? { url: explicitSynthetic.url } : target?.url ? { url: target.url } : bridgeSynthetic?.url ? { url: bridgeSynthetic.url } : {}),
2908
+ ...(explicitSynthetic?.title ? { title: explicitSynthetic.title } : target?.title ? { title: target.title } : bridgeSynthetic?.title ? { title: bridgeSynthetic.title } : {}),
2909
+ ...(synthetic?.sessionId ? { sessionId: synthetic.sessionId } : {}),
2910
+ ...(explicitSynthetic?.openerTargetId
2911
+ ? { openerTargetId: explicitSynthetic.openerTargetId }
2912
+ : target?.openerTargetId
2913
+ ? { openerTargetId: target.openerTargetId }
2914
+ : bridgeSynthetic?.openerTargetId
2915
+ ? { openerTargetId: bridgeSynthetic.openerTargetId }
2916
+ : {}),
2917
+ debuggee: synthetic?.sessionId
2918
+ ? { tabId: synthetic.tabId, sessionId: synthetic.sessionId }
2919
+ : this.cdp.getTabDebuggee?.(targetTabId) ?? { tabId: targetTabId }
2920
+ };
2921
+ }
2922
+ resolveDebuggerEventTargetId(session, source, resolvedTabId) {
2923
+ const sourceSessionId = typeof source.sessionId === "string"
2924
+ ? source.sessionId
2925
+ : null;
2926
+ if (sourceSessionId) {
2927
+ const synthetic = this.sessions.findSyntheticTargetBySessionId(session.id, sourceSessionId);
2928
+ if (synthetic) {
2929
+ return synthetic.targetId;
2930
+ }
2931
+ }
2932
+ const sourceTargetId = typeof source.targetId === "string" ? source.targetId : null;
2933
+ if (sourceTargetId) {
2934
+ const synthetic = this.sessions.getSyntheticTarget(session.id, sourceTargetId);
2935
+ if (synthetic) {
2936
+ return synthetic.targetId;
2937
+ }
2938
+ if (session.targets.has(sourceTargetId)) {
2939
+ return sourceTargetId;
2940
+ }
2941
+ }
2942
+ const tabId = resolvedTabId ?? this.cdp.resolveSourceTabId(source);
2943
+ if (tabId !== null) {
2944
+ return this.sessions.getTargetIdByTabId(session.id, tabId) ?? session.activeTargetId ?? session.targetId;
2945
+ }
2946
+ return session.activeTargetId ?? session.targetId;
2947
+ }
2948
+ resolveRouterEventTargetId(session, event) {
2949
+ if (typeof event.sessionId === "string") {
2950
+ const synthetic = this.sessions.findSyntheticTargetBySessionId(session.id, event.sessionId);
2951
+ if (synthetic) {
2952
+ return synthetic.targetId;
2953
+ }
2954
+ }
2955
+ return this.sessions.getTargetIdByTabId(session.id, event.tabId) ?? session.activeTargetId ?? session.targetId;
2956
+ }
2957
+ applyDialogOpening(session, targetId, params) {
2958
+ const payload = params;
2959
+ this.sessions.setDialog(session.id, targetId, {
2960
+ open: true,
2961
+ targetId,
2962
+ ...(typeof payload?.type === "string" ? { type: payload.type } : {}),
2963
+ ...(typeof payload?.message === "string" ? { message: payload.message } : {}),
2964
+ ...(typeof payload?.defaultPrompt === "string" ? { defaultPrompt: payload.defaultPrompt } : {}),
2965
+ ...(typeof payload?.url === "string" ? { url: payload.url } : {}),
2966
+ openedAt: new Date().toISOString()
2967
+ });
2968
+ }
2969
+ applyDialogClosed(session, targetId) {
2970
+ this.sessions.clearDialog(session.id, targetId);
2971
+ }
2972
+ applyFileChooserOpened(session, targetId, params) {
2973
+ const payload = params;
2974
+ this.sessions.setFileChooser(session.id, targetId, {
2975
+ open: true,
2976
+ targetId,
2977
+ ...(typeof payload?.backendNodeId === "number" ? { backendNodeId: payload.backendNodeId } : {}),
2978
+ openedAt: new Date().toISOString()
2979
+ });
2980
+ }
2981
+ serializeDialogState(session, targetId) {
2982
+ if (!targetId) {
2983
+ return { open: false, targetId: session.activeTargetId ?? session.targetId };
2984
+ }
2985
+ return this.sessions.getDialog(session.id, targetId) ?? { open: false, targetId };
2986
+ }
2987
+ hasUsableDebuggee(target) {
2988
+ if (typeof target.sessionId === "string" && target.sessionId.length > 0) {
2989
+ if (typeof this.cdp.hasDebuggerSession === "function") {
2990
+ return this.cdp.hasDebuggerSession(target.sessionId);
2991
+ }
2992
+ return true;
2993
+ }
2994
+ return this.isConcreteDebuggee(this.cdp.getTabDebuggee?.(target.tabId));
2995
+ }
2996
+ extractPayloadTargetId(payload) {
2997
+ if (!isRecord(payload)) {
2998
+ return null;
2999
+ }
3000
+ return typeof payload.targetId === "string" && payload.targetId.trim().length > 0
3001
+ ? payload.targetId.trim()
3002
+ : null;
3003
+ }
3004
+ resolveRequestedTargetContext(session, targetId, explicitTarget) {
3005
+ const target = this.resolveTargetContext(session, targetId);
3006
+ if (!target) {
3007
+ return null;
3008
+ }
3009
+ if (!explicitTarget
3010
+ && target.targetId !== session.targetId
3011
+ && !this.hasUsableDebuggee(target)
3012
+ && (target.synthetic || typeof target.openerTargetId === "string" || typeof target.sessionId === "string")) {
3013
+ const fallbackTarget = this.resolveTargetContext(session, session.targetId);
3014
+ if (fallbackTarget) {
3015
+ session.activeTargetId = fallbackTarget.targetId;
3016
+ return fallbackTarget;
3017
+ }
3018
+ }
3019
+ return target;
3020
+ }
3021
+ async preparePopupTarget(session, targetId) {
3022
+ let target = this.resolveTargetContext(session, targetId);
3023
+ if (!target || this.hasUsableDebuggee(target)) {
3024
+ return target;
3025
+ }
3026
+ const hydratedPopupTarget = typeof target.sessionId !== "string"
3027
+ ? await this.hydratePopupOpenerTarget(session, targetId)
3028
+ : null;
3029
+ const popupTarget = hydratedPopupTarget?.openerTargetId
3030
+ ? hydratedPopupTarget
3031
+ : target.openerTargetId
3032
+ ? {
3033
+ targetId,
3034
+ tabId: target.tabId,
3035
+ ...(typeof target.url === "string" ? { url: target.url } : {}),
3036
+ ...(typeof target.title === "string" ? { title: target.title } : {}),
3037
+ openerTargetId: target.openerTargetId
3038
+ }
3039
+ : null;
3040
+ if (!popupTarget?.openerTargetId) {
3041
+ return target;
3042
+ }
3043
+ if (this.shouldPreferDirectPopupTabAttach(popupTarget)) {
3044
+ await this.tabs.activateTab(popupTarget.tabId).catch(() => undefined);
3045
+ try {
3046
+ await this.attachTargetTab(popupTarget.tabId);
3047
+ await this.enableTargetDomains(popupTarget.tabId, true);
3048
+ this.clearPopupAttachDiagnostic(session.id, targetId);
3049
+ target = this.resolveTargetContext(session, targetId) ?? target;
3050
+ if (this.hasUsableDebuggee(target)) {
3051
+ return target;
3052
+ }
3053
+ }
3054
+ catch (error) {
3055
+ if (!isAttachBlockedError(error)) {
3056
+ throw error;
3057
+ }
3058
+ this.cdp.markClientClosed();
3059
+ try {
3060
+ await this.attachTargetTab(popupTarget.tabId);
3061
+ await this.enableTargetDomains(popupTarget.tabId, true);
3062
+ this.clearPopupAttachDiagnostic(session.id, targetId);
3063
+ target = this.resolveTargetContext(session, targetId) ?? target;
3064
+ if (this.hasUsableDebuggee(target)) {
3065
+ return target;
3066
+ }
3067
+ }
3068
+ catch (resetError) {
3069
+ if (!isAttachBlockedError(resetError)) {
3070
+ throw resetError;
3071
+ }
3072
+ }
3073
+ }
3074
+ }
3075
+ if (await this.attachTargetViaOpenerSession(session, popupTarget).catch(() => false)) {
3076
+ this.clearPopupAttachDiagnostic(session.id, targetId);
3077
+ }
3078
+ return this.resolveTargetContext(session, targetId) ?? target;
3079
+ }
3080
+ shouldPreferDirectPopupTabAttach(target) {
3081
+ const openerTabId = parseTargetAliasTabId(target.openerTargetId);
3082
+ return openerTabId !== null && openerTabId !== target.tabId;
3083
+ }
3084
+ async activateTargetAndRespond(message, session, targetId) {
3085
+ session.activeTargetId = targetId;
3086
+ const target = this.resolveTargetContext(session, targetId);
3087
+ if (target) {
3088
+ await this.tabs.activateTab(target.tabId).catch(() => undefined);
3089
+ }
3090
+ const tab = target ? await this.tabs.getTab(target.tabId) : null;
3091
+ this.sendResponse(message, {
3092
+ activeTargetId: targetId,
3093
+ url: target ? resolveReportedTargetUrl(target, tab) : undefined,
3094
+ title: target ? resolveReportedTargetTitle(target, tab) : undefined
3095
+ });
3096
+ }
3097
+ shouldPromotePopupTarget(session, openerTargetId, target) {
3098
+ return ((!!target.openerTargetId || target.targetId !== session.targetId)
3099
+ && this.hasUsableDebuggee(target)
3100
+ && (!session.activeTargetId
3101
+ || session.activeTargetId === session.targetId
3102
+ || session.activeTargetId === openerTargetId));
3103
+ }
3104
+ promotePopupTarget(session, targetId) {
3105
+ const target = this.resolveTargetContext(session, targetId);
3106
+ if (!target || !target.openerTargetId) {
3107
+ return;
3108
+ }
3109
+ if (this.shouldPromotePopupTarget(session, target.openerTargetId, target)) {
3110
+ session.activeTargetId = targetId;
3111
+ }
3112
+ }
3113
+ rehydrateSyntheticPopupBridge(session, targetId) {
3114
+ const target = session.targets.get(targetId) ?? null;
3115
+ const bridge = this.findSyntheticSessionBridge(session, target);
3116
+ if (!bridge
3117
+ || typeof bridge.sessionId !== "string"
3118
+ || bridge.sessionId.length === 0
3119
+ || typeof this.cdp.registerChildSession !== "function"
3120
+ || this.cdp.hasDebuggerSession?.(bridge.sessionId) === true) {
3121
+ return this.resolveTargetContext(session, targetId);
3122
+ }
3123
+ this.cdp.registerChildSession(bridge.tabId, {
3124
+ targetId: bridge.targetId,
3125
+ type: bridge.type,
3126
+ ...(typeof bridge.url === "string" ? { url: bridge.url } : {}),
3127
+ ...(typeof bridge.title === "string" ? { title: bridge.title } : {}),
3128
+ ...(typeof bridge.openerTargetId === "string" && !bridge.openerTargetId.startsWith("tab-")
3129
+ ? { openerId: bridge.openerTargetId }
3130
+ : {})
3131
+ }, bridge.sessionId);
3132
+ return this.resolveTargetContext(session, targetId);
3133
+ }
3134
+ findSyntheticSessionBridge(session, target) {
3135
+ if (!target) {
3136
+ return null;
3137
+ }
3138
+ const candidates = this.sessions
3139
+ .listSyntheticTargets(session.id)
3140
+ .filter((candidate) => typeof candidate.sessionId === "string" && candidate.sessionId.length > 0);
3141
+ if (candidates.length === 0) {
3142
+ return null;
3143
+ }
3144
+ const targetUrl = typeof target.url === "string" && target.url.length > 0 ? target.url : null;
3145
+ const targetTitle = typeof target.title === "string" && target.title.length > 0 ? target.title : null;
3146
+ let matches = targetUrl
3147
+ ? candidates.filter((candidate) => candidate.url === targetUrl)
3148
+ : [];
3149
+ if (matches.length === 0 && targetTitle) {
3150
+ matches = candidates.filter((candidate) => candidate.title === targetTitle);
3151
+ }
3152
+ else if (matches.length > 1 && targetTitle) {
3153
+ const titledMatches = matches.filter((candidate) => candidate.title === targetTitle);
3154
+ if (titledMatches.length > 0) {
3155
+ matches = titledMatches;
3156
+ }
3157
+ }
3158
+ if (matches.length === 0 && typeof target.openerTargetId === "string" && target.openerTargetId.length > 0) {
3159
+ const targetOpenerTabId = parseTargetAliasTabId(target.openerTargetId);
3160
+ matches = candidates.filter((candidate) => {
3161
+ if (candidate.openerTargetId === target.openerTargetId) {
3162
+ return true;
3163
+ }
3164
+ const candidateOpenerTabId = parseTargetAliasTabId(candidate.openerTargetId);
3165
+ return targetOpenerTabId !== null && candidateOpenerTabId === targetOpenerTabId;
3166
+ });
3167
+ }
3168
+ if (matches.length === 0) {
3169
+ return null;
3170
+ }
3171
+ if (matches.length === 1) {
3172
+ return matches[0] ?? null;
3173
+ }
3174
+ return matches.sort((left, right) => right.attachedAt - left.attachedAt)[0] ?? null;
3175
+ }
3176
+ async attachTargetViaOpenerSession(session, target) {
3177
+ if (typeof target.openerTargetId !== "string" || target.openerTargetId.length === 0) {
3178
+ return false;
3179
+ }
3180
+ const opener = this.resolveTargetContext(session, target.openerTargetId)
3181
+ ?? this.resolveTargetContext(session, session.targetId);
3182
+ if (!opener) {
3183
+ return false;
3184
+ }
3185
+ const openerBridgeDebuggee = { tabId: opener.tabId };
3186
+ let targetsLookupFailedReason = null;
3187
+ let targetInfos = [];
3188
+ try {
3189
+ const rawTargets = await this.cdp.sendCommand(openerBridgeDebuggee, "Target.getTargets", {}, { preserveTab: true });
3190
+ targetInfos = isRecord(rawTargets) && Array.isArray(rawTargets.targetInfos)
3191
+ ? rawTargets.targetInfos.map((entry) => extractTargetInfo(entry)).filter((entry) => entry !== null)
3192
+ : [];
3193
+ }
3194
+ catch (error) {
3195
+ targetsLookupFailedReason = error instanceof Error ? error.message : String(error);
3196
+ targetInfos = [];
3197
+ }
3198
+ const pageTargets = targetInfos.filter((info) => info.type === "page");
3199
+ const targetUrl = typeof target.url === "string" && target.url.length > 0 ? target.url : null;
3200
+ const targetTitle = typeof target.title === "string" && target.title.length > 0 ? target.title : null;
3201
+ let matcher = targetUrl ? "url" : undefined;
3202
+ let matches = targetUrl
3203
+ ? pageTargets.filter((info) => info.url === targetUrl)
3204
+ : pageTargets;
3205
+ if (matches.length === 0 && targetTitle) {
3206
+ matches = pageTargets.filter((info) => info.title === targetTitle);
3207
+ if (matches.length > 0) {
3208
+ matcher = "title";
3209
+ }
3210
+ }
3211
+ else if (matches.length > 1 && targetTitle) {
3212
+ const titledMatches = matches.filter((info) => info.title === targetTitle);
3213
+ if (titledMatches.length > 0) {
3214
+ matches = titledMatches;
3215
+ matcher = "title";
3216
+ }
3217
+ }
3218
+ if (matches.length === 0 && typeof target.openerTargetId === "string" && target.openerTargetId.length > 0) {
3219
+ const openerUrl = typeof opener.url === "string" && opener.url.length > 0 ? opener.url : null;
3220
+ const openerTitle = typeof opener.title === "string" && opener.title.length > 0 ? opener.title : null;
3221
+ const nonOpenerMatches = pageTargets.filter((info) => {
3222
+ if (openerUrl && info.url === openerUrl) {
3223
+ return false;
3224
+ }
3225
+ if (openerTitle && info.title === openerTitle) {
3226
+ return false;
3227
+ }
3228
+ return true;
3229
+ });
3230
+ if (nonOpenerMatches.length === 1) {
3231
+ matches = nonOpenerMatches;
3232
+ matcher = "non_opener";
3233
+ }
3234
+ }
3235
+ const popupTargetInfo = matches[0] ?? null;
3236
+ const resolvedTabTargetId = popupTargetInfo?.targetId
3237
+ ? null
3238
+ : (typeof this.cdp.resolveTabTargetId === "function"
3239
+ ? await this.cdp.resolveTabTargetId(target.tabId)
3240
+ : null);
3241
+ const popupTargetId = popupTargetInfo?.targetId ?? resolvedTabTargetId;
3242
+ if (!popupTargetId) {
3243
+ this.recordPopupAttachDiagnostic(session, target, {
3244
+ stage: targetsLookupFailedReason ? "targets_lookup_failed" : "resolve_tab_target_failed",
3245
+ ...(matcher ? { matcher } : {}),
3246
+ ...(targetsLookupFailedReason ? { reason: targetsLookupFailedReason, targetsLookupFailed: true } : {})
3247
+ });
3248
+ return false;
3249
+ }
3250
+ if (resolvedTabTargetId) {
3251
+ matcher = "resolve_tab_target_id";
3252
+ }
3253
+ const shouldRefreshAfterResolvedFallback = Boolean(resolvedTabTargetId
3254
+ && typeof this.cdp.refreshTabAttachment === "function"
3255
+ && ((targetsLookupFailedReason
3256
+ && this.shouldRefreshPopupOpenerAfterLookupFailure(targetsLookupFailedReason))
3257
+ || (popupTargetInfo === null && pageTargets.length === 0)));
3258
+ let refreshDiagnostic = null;
3259
+ let refreshReasonOverride = null;
3260
+ if (shouldRefreshAfterResolvedFallback) {
3261
+ try {
3262
+ await this.cdp.refreshTabAttachment(opener.tabId);
3263
+ }
3264
+ catch (error) {
3265
+ refreshDiagnostic = this.cdp.getLastRootRefreshDiagnostic?.(opener.tabId) ?? null;
3266
+ const refreshReason = error instanceof Error ? error.message : String(error);
3267
+ const canProceedWithRetainedRoot = Boolean(refreshDiagnostic?.rootSessionPresentAfterRefresh
3268
+ && refreshDiagnostic?.rootTargetIdAfterRefresh
3269
+ && refreshReason.includes("Not allowed"));
3270
+ if (canProceedWithRetainedRoot) {
3271
+ refreshReasonOverride = refreshReason;
3272
+ }
3273
+ else {
3274
+ this.recordPopupAttachDiagnostic(session, target, {
3275
+ stage: "raw_attach_failed",
3276
+ popupTargetId,
3277
+ ...(matcher ? { matcher } : {}),
3278
+ ...this.toPopupRefreshDiagnostic(refreshDiagnostic),
3279
+ ...(targetsLookupFailedReason ? { targetsLookupFailed: true } : {}),
3280
+ reason: refreshReason
3281
+ });
3282
+ return false;
3283
+ }
3284
+ }
3285
+ refreshDiagnostic = this.cdp.getLastRootRefreshDiagnostic?.(opener.tabId) ?? null;
3286
+ }
3287
+ let sessionId = null;
3288
+ for (let attempt = 0; attempt < 2; attempt += 1) {
3289
+ try {
3290
+ sessionId = typeof this.cdp.attachChildTarget === "function"
3291
+ ? await this.cdp.attachChildTarget(opener.tabId, popupTargetId)
3292
+ : await this.cdp.sendCommand(openerBridgeDebuggee, "Target.attachToTarget", {
3293
+ targetId: popupTargetId,
3294
+ flatten: true
3295
+ }).then((attached) => isRecord(attached) && typeof attached.sessionId === "string" ? attached.sessionId : null);
3296
+ }
3297
+ catch (error) {
3298
+ const routerDiagnostic = this.cdp.getLastChildAttachDiagnostic(opener.tabId, popupTargetId);
3299
+ const stage = routerDiagnostic?.stage ?? "attached_root_attach_failed";
3300
+ if (attempt === 0 && this.shouldRetryPopupAttachStage(stage)) {
3301
+ await this.waitForPopupAttachRetry();
3302
+ continue;
3303
+ }
3304
+ this.recordPopupAttachDiagnostic(session, target, {
3305
+ stage,
3306
+ popupTargetId,
3307
+ ...(matcher ? { matcher } : {}),
3308
+ ...this.toPopupChildAttachDiagnostic(routerDiagnostic),
3309
+ ...this.toPopupRefreshDiagnostic(refreshDiagnostic),
3310
+ ...(refreshReasonOverride ? { refreshReason: refreshReasonOverride } : {}),
3311
+ ...(targetsLookupFailedReason ? { targetsLookupFailed: true } : {}),
3312
+ reason: routerDiagnostic?.reason ?? (error instanceof Error ? error.message : String(error))
3313
+ });
3314
+ return false;
3315
+ }
3316
+ if (sessionId) {
3317
+ break;
3318
+ }
3319
+ const routerDiagnostic = this.cdp.getLastChildAttachDiagnostic(opener.tabId, popupTargetId);
3320
+ const stage = routerDiagnostic?.stage ?? "attached_root_attach_null";
3321
+ if (attempt === 0 && this.shouldRetryPopupAttachStage(stage)) {
3322
+ await this.waitForPopupAttachRetry();
3323
+ continue;
3324
+ }
3325
+ this.recordPopupAttachDiagnostic(session, target, {
3326
+ stage,
3327
+ popupTargetId,
3328
+ ...(matcher ? { matcher } : {}),
3329
+ ...this.toPopupChildAttachDiagnostic(routerDiagnostic),
3330
+ ...this.toPopupRefreshDiagnostic(refreshDiagnostic),
3331
+ ...(refreshReasonOverride ? { refreshReason: refreshReasonOverride } : {}),
3332
+ ...(targetsLookupFailedReason ? { targetsLookupFailed: true } : {}),
3333
+ ...(routerDiagnostic?.reason ? { reason: routerDiagnostic.reason } : {})
3334
+ });
3335
+ return false;
3336
+ }
3337
+ this.sessions.upsertSyntheticTarget(session.id, {
3338
+ targetId: popupTargetId,
3339
+ tabId: opener.tabId,
3340
+ type: popupTargetInfo?.type ?? "page",
3341
+ ...(typeof popupTargetInfo?.url === "string" ? { url: popupTargetInfo.url } : targetUrl ? { url: targetUrl } : {}),
3342
+ ...(typeof popupTargetInfo?.title === "string" ? { title: popupTargetInfo.title } : targetTitle ? { title: targetTitle } : {}),
3343
+ sessionId: sessionId ?? undefined,
3344
+ openerTargetId: target.openerTargetId,
3345
+ attachedAt: Date.now()
3346
+ });
3347
+ if (sessionId
3348
+ && popupTargetInfo
3349
+ && typeof this.cdp.registerChildSession === "function") {
3350
+ this.cdp.registerChildSession(opener.tabId, popupTargetInfo, sessionId);
3351
+ }
3352
+ this.clearPopupAttachDiagnostic(session.id, target.targetId);
3353
+ return true;
3354
+ }
3355
+ popupAttachDiagnosticKey(sessionId, targetId) {
3356
+ return `${sessionId}:${targetId}`;
3357
+ }
3358
+ shouldRefreshPopupOpenerAfterLookupFailure(reason) {
3359
+ return reason.includes("Debugger is not attached")
3360
+ || reason.includes("No tab attached")
3361
+ || reason.includes("Detached while handling command");
3362
+ }
3363
+ shouldRetryPopupAttachStage(stage) {
3364
+ return stage === "raw_attach_failed"
3365
+ || stage === "attached_root_unavailable"
3366
+ || stage === "attached_root_attach_null"
3367
+ || stage === "attached_root_attach_failed";
3368
+ }
3369
+ async waitForPopupAttachRetry() {
3370
+ await new Promise((resolve) => setTimeout(resolve, POPUP_ATTACH_RETRY_DELAY_MS));
3371
+ }
3372
+ toPopupChildAttachDiagnostic(diagnostic) {
3373
+ if (!diagnostic) {
3374
+ return {};
3375
+ }
3376
+ return {
3377
+ ...(diagnostic.initialStage ? { initialStage: diagnostic.initialStage } : {}),
3378
+ ...(diagnostic.rootTargetRetryStage ? { rootTargetRetryStage: diagnostic.rootTargetRetryStage } : {}),
3379
+ ...(diagnostic.attachedRootRecoveryStage
3380
+ ? { attachedRootRecoveryStage: diagnostic.attachedRootRecoveryStage }
3381
+ : {}),
3382
+ ...(diagnostic.attachedRootRecoverySource
3383
+ ? { attachedRootRecoverySource: diagnostic.attachedRootRecoverySource }
3384
+ : {}),
3385
+ ...(diagnostic.attachedRootRecoveryAttachTargetId
3386
+ ? { attachedRootRecoveryAttachTargetId: diagnostic.attachedRootRecoveryAttachTargetId }
3387
+ : {}),
3388
+ ...(typeof diagnostic.attachedRootRecoveryRetriedAfterRegisterRoot === "boolean"
3389
+ ? { attachedRootRecoveryRetriedAfterRegisterRoot: diagnostic.attachedRootRecoveryRetriedAfterRegisterRoot }
3390
+ : {}),
3391
+ ...(typeof diagnostic.attachedRootRecoveryRegisterRootChanged === "boolean"
3392
+ ? { attachedRootRecoveryRegisterRootChanged: diagnostic.attachedRootRecoveryRegisterRootChanged }
3393
+ : {}),
3394
+ ...(typeof diagnostic.attachedRootRecoveryRegisterRootAttachTargetChanged === "boolean"
3395
+ ? { attachedRootRecoveryRegisterRootAttachTargetChanged: diagnostic.attachedRootRecoveryRegisterRootAttachTargetChanged }
3396
+ : {}),
3397
+ ...(typeof diagnostic.attachedRootRecoveryRegisterAttachedRootSessionCalled === "boolean"
3398
+ ? {
3399
+ attachedRootRecoveryRegisterAttachedRootSessionCalled: diagnostic.attachedRootRecoveryRegisterAttachedRootSessionCalled
3400
+ }
3401
+ : {}),
3402
+ ...(diagnostic.attachedRootUnavailableTerminalBranch
3403
+ ? { attachedRootUnavailableTerminalBranch: diagnostic.attachedRootUnavailableTerminalBranch }
3404
+ : {}),
3405
+ ...(diagnostic.reattachRecoveryStage
3406
+ ? { reattachRecoveryStage: diagnostic.reattachRecoveryStage }
3407
+ : {}),
3408
+ ...(diagnostic.reattachRecoveryReason
3409
+ ? { reattachRecoveryReason: diagnostic.reattachRecoveryReason }
3410
+ : {}),
3411
+ ...(diagnostic.attachedRootRecoveryReason
3412
+ ? { attachedRootRecoveryReason: diagnostic.attachedRootRecoveryReason }
3413
+ : {})
3414
+ };
3415
+ }
3416
+ formatCdpFailureDiagnosticSuffix(details) {
3417
+ if (details?.phase === "strict_enablement" && details.enablementStage) {
3418
+ return ` (phase: ${details.phase}; stage: ${details.enablementStage})`;
3419
+ }
3420
+ if (!details?.stage) {
3421
+ return "";
3422
+ }
3423
+ return ` (origin: ${details.origin}; stage: ${details.stage})`;
3424
+ }
3425
+ toDirectAttachDiagnosticDetails(diagnostic) {
3426
+ if (!diagnostic) {
3427
+ return undefined;
3428
+ }
3429
+ return {
3430
+ origin: diagnostic.origin,
3431
+ stage: diagnostic.stage,
3432
+ attachBy: diagnostic.attachBy,
3433
+ ...(diagnostic.probeMethod ? { probeMethod: diagnostic.probeMethod } : {}),
3434
+ ...(diagnostic.reason ? { reason: diagnostic.reason } : {})
3435
+ };
3436
+ }
3437
+ decorateDirectAttachError(error, diagnostic) {
3438
+ const detail = this.getCdpFailureMessage(error);
3439
+ if (!diagnostic) {
3440
+ return error instanceof Error ? error : new Error(detail);
3441
+ }
3442
+ return this.decorateCdpFailure(error, this.toDirectAttachDiagnosticDetails(diagnostic) ?? {});
3443
+ }
3444
+ getCdpFailureMessage(error) {
3445
+ if (error instanceof Error) {
3446
+ return error.message;
3447
+ }
3448
+ if (error && typeof error === "object" && "message" in error) {
3449
+ const message = error.message;
3450
+ if (typeof message === "string") {
3451
+ if ("code" in error) {
3452
+ const code = error.code;
3453
+ return typeof code === "number"
3454
+ ? JSON.stringify({ code, message })
3455
+ : message;
3456
+ }
3457
+ return message;
3458
+ }
3459
+ }
3460
+ return "Debugger attach failed";
3461
+ }
3462
+ decorateCdpFailure(error, details) {
3463
+ const detail = this.getCdpFailureMessage(error);
3464
+ const decorated = error instanceof Error ? error : new Error(detail);
3465
+ const mergedDetails = {
3466
+ ...(decorated.directAttachDetails ?? {}),
3467
+ ...details,
3468
+ ...(details.reason ? {} : { reason: detail })
3469
+ };
3470
+ const suffix = this.formatCdpFailureDiagnosticSuffix(mergedDetails);
3471
+ decorated.message = suffix.length > 0 && detail.endsWith(suffix)
3472
+ ? detail
3473
+ : `${detail}${suffix}`;
3474
+ decorated.directAttachDetails = mergedDetails;
3475
+ return decorated;
3476
+ }
3477
+ getDirectAttachErrorDetails(error) {
3478
+ if (!(error instanceof Error)) {
3479
+ return undefined;
3480
+ }
3481
+ const decorated = error;
3482
+ return decorated.directAttachDetails;
3483
+ }
3484
+ getPopupAttachDiagnostic(sessionId, targetId) {
3485
+ return this.popupAttachDiagnostics.get(this.popupAttachDiagnosticKey(sessionId, targetId)) ?? null;
3486
+ }
3487
+ clearPopupAttachDiagnostic(sessionId, targetId) {
3488
+ this.popupAttachDiagnostics.delete(this.popupAttachDiagnosticKey(sessionId, targetId));
3489
+ }
3490
+ recordPopupAttachDiagnostic(session, target, diagnostic) {
3491
+ const entry = {
3492
+ targetId: target.targetId,
3493
+ tabId: target.tabId,
3494
+ ...(target.openerTargetId ? { openerTargetId: target.openerTargetId } : {}),
3495
+ at: Date.now(),
3496
+ ...diagnostic
3497
+ };
3498
+ this.popupAttachDiagnostics.set(this.popupAttachDiagnosticKey(session.id, target.targetId), entry);
3499
+ logError("ops.popup_attach_stage", new Error(entry.stage), {
3500
+ code: "popup_attach_stage",
3501
+ extra: {
3502
+ targetId: entry.targetId,
3503
+ tabId: entry.tabId,
3504
+ ...(entry.openerTargetId ? { openerTargetId: entry.openerTargetId } : {}),
3505
+ ...(entry.popupTargetId ? { popupTargetId: entry.popupTargetId } : {}),
3506
+ ...(entry.matcher ? { matcher: entry.matcher } : {}),
3507
+ ...(entry.initialStage ? { initialStage: entry.initialStage } : {}),
3508
+ ...(entry.rootTargetRetryStage ? { rootTargetRetryStage: entry.rootTargetRetryStage } : {}),
3509
+ ...(entry.attachedRootRecoveryStage ? { attachedRootRecoveryStage: entry.attachedRootRecoveryStage } : {}),
3510
+ ...(entry.attachedRootRecoverySource ? { attachedRootRecoverySource: entry.attachedRootRecoverySource } : {}),
3511
+ ...(entry.attachedRootRecoveryAttachTargetId
3512
+ ? { attachedRootRecoveryAttachTargetId: entry.attachedRootRecoveryAttachTargetId }
3513
+ : {}),
3514
+ ...(typeof entry.attachedRootRecoveryRetriedAfterRegisterRoot === "boolean"
3515
+ ? { attachedRootRecoveryRetriedAfterRegisterRoot: entry.attachedRootRecoveryRetriedAfterRegisterRoot }
3516
+ : {}),
3517
+ ...(typeof entry.attachedRootRecoveryRegisterRootChanged === "boolean"
3518
+ ? { attachedRootRecoveryRegisterRootChanged: entry.attachedRootRecoveryRegisterRootChanged }
3519
+ : {}),
3520
+ ...(typeof entry.attachedRootRecoveryRegisterRootAttachTargetChanged === "boolean"
3521
+ ? { attachedRootRecoveryRegisterRootAttachTargetChanged: entry.attachedRootRecoveryRegisterRootAttachTargetChanged }
3522
+ : {}),
3523
+ ...(typeof entry.attachedRootRecoveryRegisterAttachedRootSessionCalled === "boolean"
3524
+ ? {
3525
+ attachedRootRecoveryRegisterAttachedRootSessionCalled: entry.attachedRootRecoveryRegisterAttachedRootSessionCalled
3526
+ }
3527
+ : {}),
3528
+ ...(entry.attachedRootUnavailableTerminalBranch
3529
+ ? { attachedRootUnavailableTerminalBranch: entry.attachedRootUnavailableTerminalBranch }
3530
+ : {}),
3531
+ ...(entry.reattachRecoveryStage ? { reattachRecoveryStage: entry.reattachRecoveryStage } : {}),
3532
+ ...(entry.reattachRecoveryReason ? { reattachRecoveryReason: entry.reattachRecoveryReason } : {}),
3533
+ ...(entry.attachedRootRecoveryReason ? { attachedRootRecoveryReason: entry.attachedRootRecoveryReason } : {}),
3534
+ ...(entry.refreshPath ? { refreshPath: entry.refreshPath } : {}),
3535
+ ...(typeof entry.refreshCompleted === "boolean" ? { refreshCompleted: entry.refreshCompleted } : {}),
3536
+ ...(typeof entry.refreshDebuggeePresent === "boolean" ? { refreshDebuggeePresent: entry.refreshDebuggeePresent } : {}),
3537
+ ...(typeof entry.refreshRootSessionPresent === "boolean"
3538
+ ? { refreshRootSessionPresent: entry.refreshRootSessionPresent }
3539
+ : {}),
3540
+ ...(entry.refreshRootTargetId ? { refreshRootTargetId: entry.refreshRootTargetId } : {}),
3541
+ ...(entry.refreshProbeMethod ? { refreshProbeMethod: entry.refreshProbeMethod } : {}),
3542
+ ...(entry.refreshProbeStage ? { refreshProbeStage: entry.refreshProbeStage } : {}),
3543
+ ...(entry.refreshProbeReason ? { refreshProbeReason: entry.refreshProbeReason } : {}),
3544
+ ...(entry.refreshReason ? { refreshReason: entry.refreshReason } : {}),
3545
+ ...(entry.targetsLookupFailed ? { targetsLookupFailed: true } : {}),
3546
+ ...(entry.reason ? { reason: entry.reason } : {})
3547
+ }
3548
+ });
3549
+ }
3550
+ toPopupRefreshDiagnostic(diagnostic) {
3551
+ if (!diagnostic) {
3552
+ return {};
3553
+ }
3554
+ return {
3555
+ refreshPath: diagnostic.path,
3556
+ refreshCompleted: diagnostic.refreshCompleted,
3557
+ refreshDebuggeePresent: diagnostic.debuggeePresentAfterRefresh,
3558
+ refreshRootSessionPresent: diagnostic.rootSessionPresentAfterRefresh,
3559
+ ...(diagnostic.rootTargetIdAfterRefresh ? { refreshRootTargetId: diagnostic.rootTargetIdAfterRefresh } : {}),
3560
+ refreshProbeMethod: diagnostic.probeMethod,
3561
+ refreshProbeStage: diagnostic.probeStage,
3562
+ ...(diagnostic.probeReason ? { refreshProbeReason: diagnostic.probeReason } : {}),
3563
+ ...(diagnostic.reason ? { refreshReason: diagnostic.reason } : {})
3564
+ };
3565
+ }
3566
+ requireActiveTarget(session, message) {
3567
+ const explicitTargetId = this.extractPayloadTargetId(message.payload);
3568
+ const targetId = explicitTargetId ?? session.activeTargetId ?? session.targetId;
3569
+ if (!targetId) {
3570
+ this.sendError(message, buildError("invalid_request", "No active target", false));
3571
+ return null;
3572
+ }
3573
+ const target = this.resolveRequestedTargetContext(session, targetId, explicitTargetId !== null);
3574
+ if (!target) {
3575
+ this.sendError(message, buildError("invalid_request", "Active target missing", false));
3576
+ return null;
3577
+ }
3578
+ if (target.url) {
3579
+ const restriction = isRestrictedUrl(target.url);
3580
+ if (restriction.restricted && !this.isAllowedCanvasRestrictionTarget(session, targetId, target)) {
3581
+ this.sendError(message, buildError("restricted_url", restriction.message ?? "Restricted tab.", false));
3582
+ return null;
3583
+ }
3584
+ }
3585
+ if (target.synthetic && !target.sessionId && !this.isSyntheticRootPreviewTarget(session, targetId, target)) {
3586
+ this.sendPopupAttachPendingError(message, session, targetId);
3587
+ return null;
3588
+ }
3589
+ if (target.openerTargetId && !this.hasUsableDebuggee(target)) {
3590
+ this.sendPopupAttachPendingError(message, session, targetId);
3591
+ return null;
3592
+ }
3593
+ return target;
3594
+ }
3595
+ isAllowedCanvasRestrictionTarget(session, targetId, target) {
3596
+ if (this.isAllowedCanvasTargetUrl(target.url)) {
3597
+ return true;
3598
+ }
3599
+ if (isHtmlDataUrl(target.url ?? "") && this.isRegisteredCanvasTarget(session, targetId)) {
3600
+ return true;
3601
+ }
3602
+ return this.isSyntheticRootPreviewTarget(session, targetId, target)
3603
+ && isHtmlDataUrl(target.url ?? "");
3604
+ }
3605
+ isRegisteredCanvasTarget(session, targetId) {
3606
+ return this.isAllowedCanvasTargetUrl(session.targets.get(targetId)?.url);
3607
+ }
3608
+ isSyntheticRootPreviewTarget(session, _targetId, target) {
3609
+ const rootSynthetic = this.sessions.getSyntheticTarget(session.id, session.targetId);
3610
+ const effectiveUrl = target.url ?? rootSynthetic?.url;
3611
+ return isHtmlDataUrl(rootSynthetic?.url ?? "")
3612
+ && isHtmlDataUrl(effectiveUrl ?? "")
3613
+ && !rootSynthetic?.openerTargetId
3614
+ && rootSynthetic?.tabId === session.tabId
3615
+ && !target.openerTargetId
3616
+ && target.tabId === session.tabId;
3617
+ }
3618
+ isAllowedCanvasTargetUrl(rawUrl) {
3619
+ if (typeof rawUrl !== "string" || rawUrl.length === 0) {
3620
+ return false;
3621
+ }
3622
+ try {
3623
+ const allowedUrl = chrome.runtime.getURL("canvas.html");
3624
+ return rawUrl === allowedUrl || rawUrl.startsWith(`${allowedUrl}#`) || rawUrl.startsWith(`${allowedUrl}?`);
3625
+ }
3626
+ catch {
3627
+ return false;
3628
+ }
3629
+ }
3630
+ async captureCanvasPage(tabId, targetId) {
3631
+ if (!this.getCanvasPageState) {
1252
3632
  return null;
1253
3633
  }
1254
- const session = this.sessions.get(opsSessionId);
1255
- if (!session) {
1256
- this.sendError(message, buildError("invalid_session", "Unknown ops session", false));
3634
+ const state = this.getCanvasPageState(targetId);
3635
+ if (!state) {
1257
3636
  return null;
1258
3637
  }
1259
- if (session.state === "closing") {
1260
- const leaseId = typeof message.leaseId === "string" ? message.leaseId : "";
1261
- if (leaseId && leaseId === session.leaseId) {
1262
- this.reclaimSession(session, clientId);
3638
+ const previewHtml = typeof state.html === "string" && state.html.length > 0
3639
+ ? extractBodyHtml(state.html)
3640
+ : null;
3641
+ const shouldProbeLiveStage = Boolean(state.pendingMutation)
3642
+ || (canvasStateContainsRichMedia(state) && !htmlContainsRichMedia(previewHtml));
3643
+ if (shouldProbeLiveStage) {
3644
+ const liveStageCapture = await this.captureLiveCanvasStage(tabId);
3645
+ if (liveStageCapture) {
3646
+ return liveStageCapture;
1263
3647
  }
1264
- else {
1265
- this.sendError(message, buildError("not_owner", "Client does not own session", false));
1266
- return null;
3648
+ const documentCapture = buildCanvasDocumentCapture(state);
3649
+ if (documentCapture) {
3650
+ return documentCapture;
1267
3651
  }
1268
3652
  }
1269
- if (session.ownerClientId !== clientId) {
1270
- this.sendError(message, buildError("not_owner", "Client does not own session", false));
1271
- return null;
1272
- }
1273
- if (typeof message.leaseId !== "string" || message.leaseId !== session.leaseId) {
1274
- this.sendError(message, buildError("not_owner", "Lease does not match session owner", false));
1275
- return null;
3653
+ if (!previewHtml) {
3654
+ return buildCanvasDocumentCapture(state);
1276
3655
  }
1277
- session.lastUsedAt = Date.now();
1278
- return session;
3656
+ return {
3657
+ html: previewHtml,
3658
+ styles: {},
3659
+ warnings: ["canvas_state_capture"],
3660
+ inlineStyles: false
3661
+ };
1279
3662
  }
1280
- requestedTargetId(session, message) {
1281
- const payload = isRecord(message.payload) ? message.payload : {};
1282
- if (typeof payload.targetId === "string" && payload.targetId.trim().length > 0) {
1283
- return payload.targetId.trim();
3663
+ async runElementAction(target, selector, action, fallback) {
3664
+ return await this.runCanvasPageAction(target, action, selector, fallback);
3665
+ }
3666
+ async runCanvasPageAction(target, action, selector, fallback) {
3667
+ if (!this.isAllowedCanvasTargetUrl(target.url) || !this.performCanvasPageAction) {
3668
+ return await fallback();
1284
3669
  }
1285
- return session.activeTargetId || null;
3670
+ return await this.performCanvasPageAction(target.targetId, action, selector ?? null);
1286
3671
  }
1287
- requireActiveTarget(session, message) {
1288
- const targetId = this.requestedTargetId(session, message);
1289
- if (!targetId) {
1290
- this.sendError(message, buildError("invalid_request", "No active target", false));
3672
+ async captureLiveCanvasStage(tabId) {
3673
+ try {
3674
+ const results = await chrome.scripting.executeScript({
3675
+ target: { tabId },
3676
+ func: () => {
3677
+ const stage = document.getElementById("canvas-stage-inner");
3678
+ if (!(stage instanceof HTMLElement)) {
3679
+ return null;
3680
+ }
3681
+ const html = stage.innerHTML.trim();
3682
+ if (!html) {
3683
+ return null;
3684
+ }
3685
+ const width = stage.style.width || `${Math.max(stage.scrollWidth, 320)}px`;
3686
+ const height = stage.style.height || `${Math.max(stage.scrollHeight, 240)}px`;
3687
+ return `<body><main data-surface="canvas" style="position:relative;width:${width};min-height:${height};">${html}</main></body>`;
3688
+ }
3689
+ });
3690
+ const html = typeof results[0]?.result === "string" ? results[0].result : null;
3691
+ if (!html) {
3692
+ return null;
3693
+ }
3694
+ return {
3695
+ html,
3696
+ styles: {},
3697
+ warnings: ["canvas_state_capture"],
3698
+ inlineStyles: true
3699
+ };
3700
+ }
3701
+ catch {
1291
3702
  return null;
1292
3703
  }
1293
- const target = session.targets.get(targetId);
3704
+ }
3705
+ resolveRefContext(session, ref, targetId) {
3706
+ const target = this.resolveTargetContext(session, targetId);
1294
3707
  if (!target) {
1295
- this.sendError(message, buildError("invalid_request", "Active target missing", false));
1296
3708
  return null;
1297
3709
  }
1298
- if (target.url) {
1299
- const restriction = isRestrictedUrl(target.url);
1300
- if (restriction.restricted) {
1301
- this.sendError(message, buildError("restricted_url", restriction.message ?? "Restricted tab.", false));
3710
+ const entry = session.refStore.resolve(targetId, ref);
3711
+ if (!entry) {
3712
+ return null;
3713
+ }
3714
+ const snapshotId = session.refStore.getSnapshotId(targetId);
3715
+ if (!snapshotId || entry.snapshotId !== snapshotId) {
3716
+ return null;
3717
+ }
3718
+ return {
3719
+ target,
3720
+ ref,
3721
+ selector: entry.selector,
3722
+ backendNodeId: entry.backendNodeId,
3723
+ snapshotId: entry.snapshotId,
3724
+ ...(entry.frameId ? { frameId: entry.frameId } : {}),
3725
+ ...(entry.role ? { role: entry.role } : {}),
3726
+ ...(entry.name ? { name: entry.name } : {})
3727
+ };
3728
+ }
3729
+ async captureSnapshotPayload(message, session, options) {
3730
+ const explicitTargetId = this.extractPayloadTargetId(message.payload);
3731
+ if (explicitTargetId) {
3732
+ try {
3733
+ await this.preparePopupTarget(session, explicitTargetId);
3734
+ }
3735
+ catch (error) {
3736
+ const detail = error instanceof Error ? error.message : "Debugger attach failed";
3737
+ this.sendError(message, buildError("cdp_attach_failed", detail, false, this.getDirectAttachErrorDetails(error)));
1302
3738
  return null;
1303
3739
  }
1304
3740
  }
1305
- return { tabId: target.tabId, targetId: target.targetId };
3741
+ const target = this.requireActiveTarget(session, message);
3742
+ if (!target)
3743
+ return null;
3744
+ const start = Date.now();
3745
+ const entriesData = await buildSnapshot((method, params) => this.cdp.sendCommand(target.debuggee, method, params), options.mode, () => session.refStore.nextRef(target.targetId), options.mode !== "actionables", options.maxNodes);
3746
+ const snapshot = session.refStore.setSnapshot(target.targetId, entriesData.entries);
3747
+ const startIndex = parseCursor(options.cursor);
3748
+ const { content, truncated, nextCursor } = paginate(entriesData.lines, startIndex, options.maxChars);
3749
+ const contentBytes = this.encoder.encode(content).length;
3750
+ if (contentBytes > MAX_SNAPSHOT_BYTES) {
3751
+ this.sendError(message, buildError("snapshot_too_large", "Snapshot exceeded max size.", false, {
3752
+ maxSnapshotBytes: MAX_SNAPSHOT_BYTES,
3753
+ actualBytes: contentBytes
3754
+ }));
3755
+ return null;
3756
+ }
3757
+ const tab = await this.tabs.getTab(target.tabId);
3758
+ return {
3759
+ target,
3760
+ snapshotId: snapshot.snapshotId,
3761
+ url: resolveReportedTargetUrl(target, tab),
3762
+ title: resolveReportedTargetTitle(target, tab),
3763
+ content,
3764
+ truncated,
3765
+ ...(nextCursor ? { nextCursor } : {}),
3766
+ refCount: snapshot.count,
3767
+ timingMs: Date.now() - start,
3768
+ warnings: entriesData.warnings
3769
+ };
1306
3770
  }
1307
- resolveSelector(session, refOrPayload, message) {
3771
+ resolveRefFromPayload(session, refOrPayload, message) {
1308
3772
  const ref = typeof refOrPayload === "string"
1309
3773
  ? refOrPayload
1310
3774
  : (isRecord(refOrPayload) && typeof refOrPayload.ref === "string" ? refOrPayload.ref : null);
@@ -1317,17 +3781,188 @@ export class OpsRuntime {
1317
3781
  this.sendError(message, buildError("invalid_request", "No active target", false));
1318
3782
  return null;
1319
3783
  }
1320
- const entry = session.refStore.resolve(targetId, ref);
1321
- if (!entry) {
1322
- this.sendError(message, buildError("invalid_request", `Unknown ref: ${ref}`, false));
3784
+ const resolved = this.resolveRefContext(session, ref, targetId);
3785
+ if (!resolved) {
3786
+ this.sendError(message, buildError("invalid_request", `Unknown ref: ${ref}. Take a new snapshot first.`, false));
1323
3787
  return null;
1324
3788
  }
1325
- return entry.selector;
3789
+ if (resolved.target.synthetic && !resolved.target.sessionId) {
3790
+ this.sendPopupAttachPendingError(message, session, resolved.target.targetId);
3791
+ return null;
3792
+ }
3793
+ return resolved;
3794
+ }
3795
+ formatPopupAttachDiagnosticSuffix(diagnostic) {
3796
+ if (!diagnostic?.stage) {
3797
+ return "";
3798
+ }
3799
+ const parts = [`stage: ${diagnostic.stage}`];
3800
+ if (diagnostic.rootTargetRetryStage) {
3801
+ parts.push(`root-target-retry: ${diagnostic.rootTargetRetryStage}`);
3802
+ }
3803
+ if (diagnostic.attachedRootRecoveryStage) {
3804
+ const attachedRootPart = diagnostic.attachedRootRecoverySource
3805
+ ? `${diagnostic.attachedRootRecoveryStage} via ${diagnostic.attachedRootRecoverySource}`
3806
+ : diagnostic.attachedRootRecoveryStage;
3807
+ parts.push(`attached-root: ${attachedRootPart}`);
3808
+ }
3809
+ if (diagnostic.attachedRootUnavailableTerminalBranch) {
3810
+ parts.push(`terminal: ${diagnostic.attachedRootUnavailableTerminalBranch}`);
3811
+ }
3812
+ return ` (${parts.join("; ")})`;
3813
+ }
3814
+ sendPopupAttachPendingError(message, session, targetId) {
3815
+ const diagnostic = session && typeof targetId === "string"
3816
+ ? this.getPopupAttachDiagnostic(session.id, targetId)
3817
+ : null;
3818
+ const stageSuffix = this.formatPopupAttachDiagnosticSuffix(diagnostic);
3819
+ this.sendError(message, buildError("execution_failed", `Popup target has not finished attaching yet${stageSuffix}. Take a new review or snapshot and retry.`, true, diagnostic
3820
+ ? {
3821
+ stage: diagnostic.stage,
3822
+ ...(diagnostic.popupTargetId ? { popupTargetId: diagnostic.popupTargetId } : {}),
3823
+ ...(diagnostic.matcher ? { matcher: diagnostic.matcher } : {}),
3824
+ ...(diagnostic.initialStage ? { initialStage: diagnostic.initialStage } : {}),
3825
+ ...(diagnostic.rootTargetRetryStage ? { rootTargetRetryStage: diagnostic.rootTargetRetryStage } : {}),
3826
+ ...(diagnostic.attachedRootRecoveryStage
3827
+ ? { attachedRootRecoveryStage: diagnostic.attachedRootRecoveryStage }
3828
+ : {}),
3829
+ ...(diagnostic.attachedRootRecoverySource
3830
+ ? { attachedRootRecoverySource: diagnostic.attachedRootRecoverySource }
3831
+ : {}),
3832
+ ...(diagnostic.attachedRootRecoveryAttachTargetId
3833
+ ? { attachedRootRecoveryAttachTargetId: diagnostic.attachedRootRecoveryAttachTargetId }
3834
+ : {}),
3835
+ ...(typeof diagnostic.attachedRootRecoveryRetriedAfterRegisterRoot === "boolean"
3836
+ ? { attachedRootRecoveryRetriedAfterRegisterRoot: diagnostic.attachedRootRecoveryRetriedAfterRegisterRoot }
3837
+ : {}),
3838
+ ...(typeof diagnostic.attachedRootRecoveryRegisterRootChanged === "boolean"
3839
+ ? { attachedRootRecoveryRegisterRootChanged: diagnostic.attachedRootRecoveryRegisterRootChanged }
3840
+ : {}),
3841
+ ...(typeof diagnostic.attachedRootRecoveryRegisterRootAttachTargetChanged === "boolean"
3842
+ ? {
3843
+ attachedRootRecoveryRegisterRootAttachTargetChanged: diagnostic.attachedRootRecoveryRegisterRootAttachTargetChanged
3844
+ }
3845
+ : {}),
3846
+ ...(typeof diagnostic.attachedRootRecoveryRegisterAttachedRootSessionCalled === "boolean"
3847
+ ? {
3848
+ attachedRootRecoveryRegisterAttachedRootSessionCalled: diagnostic.attachedRootRecoveryRegisterAttachedRootSessionCalled
3849
+ }
3850
+ : {}),
3851
+ ...(diagnostic.attachedRootUnavailableTerminalBranch
3852
+ ? { attachedRootUnavailableTerminalBranch: diagnostic.attachedRootUnavailableTerminalBranch }
3853
+ : {}),
3854
+ ...(diagnostic.reattachRecoveryStage
3855
+ ? { reattachRecoveryStage: diagnostic.reattachRecoveryStage }
3856
+ : {}),
3857
+ ...(diagnostic.reattachRecoveryReason
3858
+ ? { reattachRecoveryReason: diagnostic.reattachRecoveryReason }
3859
+ : {}),
3860
+ ...(diagnostic.attachedRootRecoveryReason
3861
+ ? { attachedRootRecoveryReason: diagnostic.attachedRootRecoveryReason }
3862
+ : {}),
3863
+ ...(diagnostic.refreshPath ? { refreshPath: diagnostic.refreshPath } : {}),
3864
+ ...(typeof diagnostic.refreshCompleted === "boolean" ? { refreshCompleted: diagnostic.refreshCompleted } : {}),
3865
+ ...(typeof diagnostic.refreshDebuggeePresent === "boolean"
3866
+ ? { refreshDebuggeePresent: diagnostic.refreshDebuggeePresent }
3867
+ : {}),
3868
+ ...(typeof diagnostic.refreshRootSessionPresent === "boolean"
3869
+ ? { refreshRootSessionPresent: diagnostic.refreshRootSessionPresent }
3870
+ : {}),
3871
+ ...(diagnostic.refreshRootTargetId ? { refreshRootTargetId: diagnostic.refreshRootTargetId } : {}),
3872
+ ...(diagnostic.refreshProbeMethod ? { refreshProbeMethod: diagnostic.refreshProbeMethod } : {}),
3873
+ ...(diagnostic.refreshProbeStage ? { refreshProbeStage: diagnostic.refreshProbeStage } : {}),
3874
+ ...(diagnostic.refreshProbeReason ? { refreshProbeReason: diagnostic.refreshProbeReason } : {}),
3875
+ ...(diagnostic.refreshReason ? { refreshReason: diagnostic.refreshReason } : {}),
3876
+ ...(diagnostic.targetsLookupFailed ? { targetsLookupFailed: true } : {}),
3877
+ ...(diagnostic.reason ? { reason: diagnostic.reason } : {})
3878
+ }
3879
+ : undefined));
3880
+ }
3881
+ resolveSelector(session, refOrPayload, message) {
3882
+ return this.resolveRefFromPayload(session, refOrPayload, message)?.selector ?? null;
3883
+ }
3884
+ async callFunctionOnRef(resolved, functionDeclaration, args = [], ref = resolved.ref) {
3885
+ try {
3886
+ const resolvedNode = await this.cdp.sendCommand(resolved.target.debuggee, "DOM.resolveNode", {
3887
+ backendNodeId: resolved.backendNodeId
3888
+ });
3889
+ const objectId = resolvedNode.object?.objectId;
3890
+ if (!objectId) {
3891
+ throw buildStaleSnapshotError(ref);
3892
+ }
3893
+ const result = await this.cdp.sendCommand(resolved.target.debuggee, "Runtime.callFunctionOn", {
3894
+ objectId,
3895
+ functionDeclaration,
3896
+ arguments: args.map((value) => ({ value })),
3897
+ returnByValue: true
3898
+ });
3899
+ if (result.exceptionDetails) {
3900
+ throw new Error(result.exceptionDetails.text ?? "Runtime.callFunctionOn failed");
3901
+ }
3902
+ return result.result?.value;
3903
+ }
3904
+ catch (error) {
3905
+ if (isSnapshotStaleMessage(error)) {
3906
+ throw buildStaleSnapshotError(ref);
3907
+ }
3908
+ throw error;
3909
+ }
3910
+ }
3911
+ async resolveRefPoint(resolved) {
3912
+ try {
3913
+ const box = await this.cdp.sendCommand(resolved.target.debuggee, "DOM.getBoxModel", {
3914
+ backendNodeId: resolved.backendNodeId
3915
+ });
3916
+ const quad = Array.isArray(box.model?.content) ? box.model?.content : [];
3917
+ if (quad.length >= 8) {
3918
+ const xs = [quad[0], quad[2], quad[4], quad[6]].filter((value) => typeof value === "number");
3919
+ const ys = [quad[1], quad[3], quad[5], quad[7]].filter((value) => typeof value === "number");
3920
+ if (xs.length === 4 && ys.length === 4) {
3921
+ return {
3922
+ x: Math.round((Math.min(...xs) + Math.max(...xs)) / 2),
3923
+ y: Math.round((Math.min(...ys) + Math.max(...ys)) / 2)
3924
+ };
3925
+ }
3926
+ }
3927
+ }
3928
+ catch (error) {
3929
+ if (isSnapshotStaleMessage(error)) {
3930
+ throw buildStaleSnapshotError(resolved.ref);
3931
+ }
3932
+ }
3933
+ const point = await this.callFunctionOnRef(resolved, DOM_REF_POINT_DECLARATION);
3934
+ const x = typeof point?.x === "number" && Number.isFinite(point.x) ? Math.round(point.x) : null;
3935
+ const y = typeof point?.y === "number" && Number.isFinite(point.y) ? Math.round(point.y) : null;
3936
+ if (x === null || y === null) {
3937
+ throw new Error(`Could not resolve a clickable point for ref: ${resolved.ref}`);
3938
+ }
3939
+ return { x, y };
1326
3940
  }
1327
- async waitForSelector(tabId, selector, state, timeoutMs) {
3941
+ normalizeScreenshotClip(value, ref) {
3942
+ const x = typeof value?.x === "number" && Number.isFinite(value.x) ? value.x : null;
3943
+ const y = typeof value?.y === "number" && Number.isFinite(value.y) ? value.y : null;
3944
+ const width = typeof value?.width === "number" && Number.isFinite(value.width) ? value.width : null;
3945
+ const height = typeof value?.height === "number" && Number.isFinite(value.height) ? value.height : null;
3946
+ if (x === null || y === null || width === null || height === null || width <= 0 || height <= 0) {
3947
+ throw new Error(`Could not resolve screenshot bounds for ref: ${ref}`);
3948
+ }
3949
+ return { x, y, width, height, scale: 1 };
3950
+ }
3951
+ async waitForFileChooser(sessionId, targetId, timeoutMs = SCREENSHOT_TIMEOUT_MS) {
3952
+ const startedAt = Date.now();
3953
+ while (Date.now() - startedAt < timeoutMs) {
3954
+ const chooser = this.sessions.getFileChooser(sessionId, targetId);
3955
+ if (chooser?.open) {
3956
+ return chooser;
3957
+ }
3958
+ await delay(50);
3959
+ }
3960
+ throw new Error("File chooser did not open");
3961
+ }
3962
+ async waitForSelector(target, selector, state, timeoutMs) {
1328
3963
  const start = Date.now();
1329
3964
  while (Date.now() - start < timeoutMs) {
1330
- const snapshot = await this.dom.getSelectorState(tabId, selector);
3965
+ const snapshot = await this.runElementAction(target, selector, { type: "getSelectorState" }, () => this.dom.getSelectorState(target.tabId, selector));
1331
3966
  if (state === "attached" && snapshot.attached)
1332
3967
  return;
1333
3968
  if (state === "visible" && snapshot.visible)
@@ -1338,6 +3973,22 @@ export class OpsRuntime {
1338
3973
  }
1339
3974
  throw new Error("Wait for selector timed out");
1340
3975
  }
3976
+ async waitForRefState(resolved, state, timeoutMs) {
3977
+ const start = Date.now();
3978
+ while (Date.now() - start < timeoutMs) {
3979
+ const snapshot = await this.callFunctionOnRef(resolved, DOM_SELECTOR_STATE_DECLARATION);
3980
+ const attached = snapshot?.attached === true;
3981
+ const visible = snapshot?.visible === true;
3982
+ if (state === "attached" && attached)
3983
+ return;
3984
+ if (state === "visible" && visible)
3985
+ return;
3986
+ if (state === "hidden" && (!attached || !visible))
3987
+ return;
3988
+ await delay(200);
3989
+ }
3990
+ throw new Error("Wait for selector timed out");
3991
+ }
1341
3992
  cleanupSession(session, event) {
1342
3993
  this.clearClosingTimer(session.id);
1343
3994
  const waiters = this.parallelWaiters.get(session.id);
@@ -1355,13 +4006,7 @@ export class OpsRuntime {
1355
4006
  for (const target of session.targets.values()) {
1356
4007
  void this.cdp.detachTab(target.tabId).catch(() => undefined);
1357
4008
  }
1358
- this.sendEvent({
1359
- type: "ops_event",
1360
- clientId: session.ownerClientId,
1361
- opsSessionId: session.id,
1362
- event,
1363
- payload: { tabId: session.tabId, targetId: session.targetId }
1364
- });
4009
+ this.emitSessionEvent(session, event);
1365
4010
  }
1366
4011
  handleClosedTarget(tabId, event) {
1367
4012
  const session = this.sessions.getByTabId(tabId);
@@ -1377,7 +4022,7 @@ export class OpsRuntime {
1377
4022
  this.cleanupSession(session, event);
1378
4023
  }
1379
4024
  }
1380
- handleDebuggerDetachForTab(tabId) {
4025
+ async handleDebuggerDetachForTab(tabId) {
1381
4026
  const session = this.sessions.getByTabId(tabId);
1382
4027
  if (!session)
1383
4028
  return;
@@ -1385,6 +4030,21 @@ export class OpsRuntime {
1385
4030
  // Root tab detach can be transient during child-target shutdown; tab removal handler owns root teardown.
1386
4031
  return;
1387
4032
  }
4033
+ const targetId = this.sessions.getTargetIdByTabId(session.id, tabId);
4034
+ const target = targetId ? session.targets.get(targetId) ?? null : null;
4035
+ const liveTab = await this.tabs.getTab(tabId);
4036
+ if (target && this.isAllowedCanvasTargetUrl(target.url ?? liveTab?.url)) {
4037
+ if (liveTab && targetId) {
4038
+ session.targets.set(targetId, {
4039
+ ...target,
4040
+ url: liveTab.url ?? target.url,
4041
+ title: liveTab.title ?? target.title
4042
+ });
4043
+ }
4044
+ // Design tabs can detach transiently while the extension page stays open; retain the target so `/ops`
4045
+ // can reattach it later via `targets.use`.
4046
+ return;
4047
+ }
1388
4048
  this.handleClosedTarget(tabId, "ops_session_closed");
1389
4049
  }
1390
4050
  async closeTabBestEffort(tabId) {
@@ -1462,9 +4122,19 @@ export class OpsRuntime {
1462
4122
  sendEvent(event) {
1463
4123
  this.sendEnvelope(event);
1464
4124
  }
4125
+ emitSessionEvent(session, event) {
4126
+ this.sendEvent({
4127
+ type: "ops_event",
4128
+ clientId: session.ownerClientId,
4129
+ opsSessionId: session.id,
4130
+ event,
4131
+ payload: { tabId: session.tabId, targetId: session.targetId }
4132
+ });
4133
+ }
1465
4134
  markSessionClosing(session, reason) {
1466
- if (session.state === "closing")
1467
- return;
4135
+ if (session.state === "closing") {
4136
+ return false;
4137
+ }
1468
4138
  session.state = "closing";
1469
4139
  session.closingReason = reason;
1470
4140
  session.expiresAt = Date.now() + SESSION_TTL_MS;
@@ -1476,13 +4146,18 @@ export class OpsRuntime {
1476
4146
  }
1477
4147
  }, SESSION_TTL_MS);
1478
4148
  this.closingTimers.set(session.id, timeoutId);
4149
+ return true;
1479
4150
  }
1480
4151
  reclaimSession(session, clientId) {
4152
+ const wasClosing = session.state === "closing";
1481
4153
  session.ownerClientId = clientId;
1482
4154
  session.state = "active";
1483
4155
  session.expiresAt = undefined;
1484
4156
  session.closingReason = undefined;
1485
4157
  this.clearClosingTimer(session.id);
4158
+ if (wasClosing) {
4159
+ this.emitSessionEvent(session, "ops_session_reclaimed");
4160
+ }
1486
4161
  }
1487
4162
  clearClosingTimer(sessionId) {
1488
4163
  const timer = this.closingTimers.get(sessionId);
@@ -1696,6 +4371,18 @@ const toCookieListRecord = (entry) => {
1696
4371
  const isRecord = (value) => {
1697
4372
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
1698
4373
  };
4374
+ const parseCanvasOverlaySelection = (value, targetId) => {
4375
+ const record = isRecord(value) ? value : {};
4376
+ const updatedAt = typeof record.updatedAt === "string" && record.updatedAt.trim().length > 0
4377
+ ? record.updatedAt
4378
+ : undefined;
4379
+ return {
4380
+ pageId: typeof record.pageId === "string" && record.pageId.trim().length > 0 ? record.pageId : null,
4381
+ nodeId: typeof record.nodeId === "string" && record.nodeId.trim().length > 0 ? record.nodeId : null,
4382
+ targetId: typeof record.targetId === "string" && record.targetId.trim().length > 0 ? record.targetId : targetId,
4383
+ ...(updatedAt ? { updatedAt } : {})
4384
+ };
4385
+ };
1699
4386
  const parseCursor = (cursor) => {
1700
4387
  if (!cursor)
1701
4388
  return 0;
@@ -1729,6 +4416,342 @@ const paginate = (lines, startIndex, maxChars) => {
1729
4416
  };
1730
4417
  };
1731
4418
  const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
4419
+ const parseTabTargetId = (targetId) => {
4420
+ const match = /^tab-(\d+)$/.exec(targetId);
4421
+ if (!match) {
4422
+ return null;
4423
+ }
4424
+ const parsed = Number.parseInt(match[1], 10);
4425
+ return parsed;
4426
+ };
4427
+ const parseTargetAliasTabId = (targetId) => {
4428
+ if (typeof targetId !== "string" || targetId.length === 0) {
4429
+ return null;
4430
+ }
4431
+ if (targetId.startsWith("target-")) {
4432
+ const parsed = Number.parseInt(targetId.slice(7), 10);
4433
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
4434
+ }
4435
+ return parseTabTargetId(targetId);
4436
+ };
4437
+ const extractBodyHtml = (html) => {
4438
+ const bodyMatch = html.match(/<body\b[^>]*>[\s\S]*<\/body>/i);
4439
+ if (bodyMatch) {
4440
+ return bodyMatch[0];
4441
+ }
4442
+ return html;
4443
+ };
4444
+ const htmlContainsRichMedia = (html) => {
4445
+ return typeof html === "string" && /<(img|video|audio)\b/i.test(html);
4446
+ };
4447
+ const canvasStateContainsRichMedia = (state) => {
4448
+ const document = isRecord(state.document) ? state.document : null;
4449
+ const pages = Array.isArray(document?.pages) ? document.pages : [];
4450
+ const assets = Array.isArray(document?.assets) ? document.assets : [];
4451
+ const assetsById = new Map(assets.flatMap((asset) => typeof asset?.id === "string" ? [[asset.id, asset]] : []));
4452
+ return pages.some((page) => Array.isArray(page?.nodes) && page.nodes.some((node) => nodeContainsRichMedia(node, assetsById)));
4453
+ };
4454
+ const nodeContainsRichMedia = (node, assetsById) => {
4455
+ const tagName = readCanvasMediaTagName(node);
4456
+ if (tagName === "img" || tagName === "video" || tagName === "audio") {
4457
+ return true;
4458
+ }
4459
+ const assetIds = Array.isArray(node.metadata.assetIds)
4460
+ ? node.metadata.assetIds.filter((entry) => typeof entry === "string" && entry.trim().length > 0)
4461
+ : [];
4462
+ return assetIds.some((assetId) => {
4463
+ const asset = assetsById.get(assetId);
4464
+ const kind = typeof asset?.kind === "string" ? asset.kind.toLowerCase() : "";
4465
+ const mime = typeof asset?.mime === "string" ? asset.mime.toLowerCase() : "";
4466
+ return kind === "image" || kind === "video" || kind === "audio" || mime.startsWith("image/") || mime.startsWith("video/") || mime.startsWith("audio/");
4467
+ });
4468
+ };
4469
+ const readCanvasMediaTagName = (node) => {
4470
+ if (typeof node.props.tagName === "string" && node.props.tagName.trim().length > 0) {
4471
+ return node.props.tagName.trim().toLowerCase();
4472
+ }
4473
+ const codeSync = isRecord(node.metadata.codeSync) ? node.metadata.codeSync : null;
4474
+ if (codeSync && typeof codeSync.tagName === "string" && codeSync.tagName.trim().length > 0) {
4475
+ return codeSync.tagName.trim().toLowerCase();
4476
+ }
4477
+ return null;
4478
+ };
4479
+ const buildCanvasDocumentCapture = (state) => {
4480
+ const page = Array.isArray(state.document.pages) ? state.document.pages[0] : null;
4481
+ if (!page || !Array.isArray(page.nodes) || page.nodes.length === 0) {
4482
+ return null;
4483
+ }
4484
+ const { width, height } = computeCanvasDocumentBounds(page.nodes);
4485
+ const nodes = [...page.nodes]
4486
+ .sort(compareCanvasCaptureNodes)
4487
+ .map((node) => renderCanvasDocumentNode(state.document, node))
4488
+ .join("");
4489
+ return {
4490
+ html: `<body><main data-surface="canvas" style="position:relative;width:${width}px;min-height:${height}px;">${nodes}</main></body>`,
4491
+ styles: {},
4492
+ warnings: ["canvas_state_capture"],
4493
+ inlineStyles: true
4494
+ };
4495
+ };
4496
+ const computeCanvasDocumentBounds = (nodes) => {
4497
+ if (nodes.length === 0) {
4498
+ return { width: 1600, height: 1200 };
4499
+ }
4500
+ const maxX = Math.max(...nodes.map((node) => node.rect.x + node.rect.width));
4501
+ const maxY = Math.max(...nodes.map((node) => node.rect.y + node.rect.height));
4502
+ return {
4503
+ width: Math.max(maxX + 240, 1600),
4504
+ height: Math.max(maxY + 240, 1200)
4505
+ };
4506
+ };
4507
+ const compareCanvasCaptureNodes = (left, right) => {
4508
+ const rootOrder = Number(left.parentId !== null) - Number(right.parentId !== null);
4509
+ if (rootOrder !== 0) {
4510
+ return rootOrder;
4511
+ }
4512
+ const areaOrder = (right.rect.width * right.rect.height) - (left.rect.width * left.rect.height);
4513
+ if (areaOrder !== 0) {
4514
+ return areaOrder;
4515
+ }
4516
+ const verticalOrder = left.rect.y - right.rect.y;
4517
+ return verticalOrder !== 0 ? verticalOrder : left.rect.x - right.rect.x;
4518
+ };
4519
+ const renderCanvasDocumentNode = (document, node) => {
4520
+ const media = resolveCanvasDocumentMedia(document, node);
4521
+ const text = escapeCanvasHtml(nodeTextForCapture(node) || node.name);
4522
+ const style = serializeCanvasCaptureStyle({
4523
+ position: "absolute",
4524
+ left: `${node.rect.x}px`,
4525
+ top: `${node.rect.y}px`,
4526
+ width: `${Math.max(node.rect.width, 40)}px`,
4527
+ minHeight: `${Math.max(node.rect.height, readCanvasMediaTagName(node) === "audio" ? 64 : 40)}px`,
4528
+ overflow: "hidden",
4529
+ ...node.style
4530
+ });
4531
+ const title = escapeCanvasAttribute(`${node.kind} • ${node.name}`);
4532
+ if (media?.kind === "image" && media.src) {
4533
+ return `<div data-node-id="${escapeCanvasAttribute(node.id)}" title="${title}" style="${style}"><img src="${escapeCanvasAttribute(media.src)}" alt="${escapeCanvasAttribute(media.alt ?? node.name)}" loading="lazy" draggable="false" style="width:100%;height:100%;object-fit:cover;display:block;" /></div>`;
4534
+ }
4535
+ if (media?.kind === "video" && media.src) {
4536
+ const poster = media.poster ? ` poster="${escapeCanvasAttribute(media.poster)}"` : "";
4537
+ return `<div data-node-id="${escapeCanvasAttribute(node.id)}" title="${title}" style="${style}"><video src="${escapeCanvasAttribute(media.src)}"${poster} muted loop autoplay playsinline preload="metadata" style="width:100%;height:100%;object-fit:cover;display:block;"></video></div>`;
4538
+ }
4539
+ if (media?.kind === "audio" && media.src) {
4540
+ return `<div data-node-id="${escapeCanvasAttribute(node.id)}" title="${title}" style="${style}"><audio src="${escapeCanvasAttribute(media.src)}" controls preload="metadata" style="width:100%;display:block;"></audio>${text ? `<div style="margin-top:8px;font:500 12px/1.4 sans-serif;">${text}</div>` : ""}</div>`;
4541
+ }
4542
+ return `<div data-node-id="${escapeCanvasAttribute(node.id)}" title="${title}" style="${style}">${text}</div>`;
4543
+ };
4544
+ const nodeTextForCapture = (node) => {
4545
+ const raw = node.props.text ?? node.metadata.text;
4546
+ if (raw !== undefined && raw !== null) {
4547
+ return typeof raw === "string" ? raw : String(raw);
4548
+ }
4549
+ return node.kind === "text" || node.kind === "note" || node.kind === "component-instance"
4550
+ ? node.name
4551
+ : "";
4552
+ };
4553
+ const resolveCanvasDocumentMedia = (document, node) => {
4554
+ const tagName = readCanvasMediaTagName(node);
4555
+ const attributes = isRecord(node.props.attributes) ? node.props.attributes : {};
4556
+ const assetIds = Array.isArray(node.metadata.assetIds)
4557
+ ? node.metadata.assetIds.filter((entry) => typeof entry === "string" && entry.trim().length > 0)
4558
+ : [];
4559
+ const asset = assetIds.length > 0
4560
+ ? document.assets.find((entry) => entry.id === assetIds[0])
4561
+ : null;
4562
+ const assetKind = typeof asset?.kind === "string" ? asset.kind.toLowerCase() : null;
4563
+ const assetMime = typeof asset?.mime === "string" ? asset.mime.toLowerCase() : null;
4564
+ const src = typeof node.props.src === "string"
4565
+ ? node.props.src
4566
+ : typeof attributes.src === "string"
4567
+ ? attributes.src
4568
+ : typeof asset?.url === "string"
4569
+ ? asset.url
4570
+ : typeof asset?.repoPath === "string"
4571
+ ? asset.repoPath
4572
+ : null;
4573
+ const poster = typeof node.props.poster === "string"
4574
+ ? node.props.poster
4575
+ : typeof attributes.poster === "string"
4576
+ ? attributes.poster
4577
+ : null;
4578
+ const alt = typeof node.props.alt === "string"
4579
+ ? node.props.alt
4580
+ : typeof attributes.alt === "string"
4581
+ ? attributes.alt
4582
+ : node.name;
4583
+ if (tagName === "img" || assetKind === "image" || assetMime?.startsWith("image/")) {
4584
+ return { kind: "image", src, poster: null, alt };
4585
+ }
4586
+ if (tagName === "video" || assetKind === "video" || assetMime?.startsWith("video/")) {
4587
+ return { kind: "video", src, poster, alt };
4588
+ }
4589
+ if (tagName === "audio" || assetKind === "audio" || assetMime?.startsWith("audio/")) {
4590
+ return { kind: "audio", src, poster: null, alt };
4591
+ }
4592
+ return null;
4593
+ };
4594
+ const serializeCanvasCaptureStyle = (style) => {
4595
+ return Object.entries(style)
4596
+ .flatMap(([key, value]) => {
4597
+ if (typeof value !== "string" && typeof value !== "number") {
4598
+ return [];
4599
+ }
4600
+ const cssKey = key.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`);
4601
+ const cssValue = typeof value === "number" && !CANVAS_CAPTURE_UNITLESS_STYLES.has(key) ? `${value}px` : String(value);
4602
+ return `${cssKey}:${escapeCanvasAttribute(cssValue)};`;
4603
+ })
4604
+ .join("");
4605
+ };
4606
+ const escapeCanvasHtml = (value) => {
4607
+ return value
4608
+ .replaceAll("&", "&amp;")
4609
+ .replaceAll("<", "&lt;")
4610
+ .replaceAll(">", "&gt;");
4611
+ };
4612
+ const escapeCanvasAttribute = (value) => {
4613
+ return escapeCanvasHtml(value)
4614
+ .replaceAll("\"", "&quot;")
4615
+ .replaceAll("'", "&#39;");
4616
+ };
4617
+ const CANVAS_CAPTURE_UNITLESS_STYLES = new Set(["fontWeight", "lineHeight", "opacity", "zIndex"]);
4618
+ const buildStaleSnapshotError = (ref) => (new Error(`Unknown ref: ${ref}. ${STALE_REF_ERROR_SUFFIX}`));
4619
+ const isSnapshotStaleMessage = (error) => {
4620
+ const message = error instanceof Error ? error.message : String(error ?? "");
4621
+ if (message.includes(STALE_REF_ERROR_SUFFIX)) {
4622
+ return true;
4623
+ }
4624
+ const normalized = message.toLowerCase();
4625
+ return normalized.includes("no node with given id")
4626
+ || normalized.includes("could not find node with given id")
4627
+ || normalized.includes("cannot find object with id")
4628
+ || normalized.includes("cannot find context with specified id")
4629
+ || normalized.includes("execution context was destroyed")
4630
+ || normalized.includes("inspected target navigated or closed");
4631
+ };
4632
+ const extractTargetInfo = (params) => {
4633
+ const payload = isRecord(params) && isRecord(params.targetInfo) ? params.targetInfo : params;
4634
+ if (!isRecord(payload) || typeof payload.targetId !== "string" || typeof payload.type !== "string") {
4635
+ return null;
4636
+ }
4637
+ return {
4638
+ targetId: payload.targetId,
4639
+ type: payload.type,
4640
+ ...(typeof payload.url === "string" ? { url: payload.url } : {}),
4641
+ ...(typeof payload.title === "string" ? { title: payload.title } : {}),
4642
+ ...(typeof payload.openerId === "string" ? { openerId: payload.openerId } : {})
4643
+ };
4644
+ };
4645
+ const isSyntheticPageTarget = (session, targetId, type) => {
4646
+ if (type !== "page" || targetId === session.targetId) {
4647
+ return false;
4648
+ }
4649
+ const parsedTabId = parseTabTargetId(targetId);
4650
+ return session.targets.has(targetId) || parsedTabId === null || parsedTabId !== session.tabId;
4651
+ };
4652
+ const resolveReportedTargetUrl = (target, tab) => {
4653
+ if (target?.synthetic === true && typeof target.url === "string" && target.url.length > 0) {
4654
+ return target.url;
4655
+ }
4656
+ if (typeof target?.url === "string" && isHtmlDataUrl(target.url)) {
4657
+ return target.url;
4658
+ }
4659
+ if (typeof target?.url === "string" && isCanvasExtensionUrl(target.url)) {
4660
+ return target.url;
4661
+ }
4662
+ return getReportedTabUrl(tab) ?? target?.url;
4663
+ };
4664
+ const resolveReportedTargetTitle = (target, tab) => {
4665
+ if (target?.synthetic === true && typeof target.title === "string" && target.title.length > 0) {
4666
+ return target.title;
4667
+ }
4668
+ if (typeof target?.url === "string" && isHtmlDataUrl(target.url) && typeof target.title === "string" && target.title.length > 0) {
4669
+ return target.title;
4670
+ }
4671
+ if (typeof target?.url === "string" && isCanvasExtensionUrl(target.url) && typeof target.title === "string" && target.title.length > 0) {
4672
+ return target.title;
4673
+ }
4674
+ if (isTabNavigationPending(tab)) {
4675
+ return undefined;
4676
+ }
4677
+ return getReportedTabTitle(tab) ?? target?.title;
4678
+ };
4679
+ const getReportedTabSeed = (tab) => {
4680
+ return {
4681
+ url: getReportedTabUrl(tab),
4682
+ title: getReportedTabTitle(tab)
4683
+ };
4684
+ };
4685
+ const getReportedTabUrl = (tab) => {
4686
+ if (!tab) {
4687
+ return undefined;
4688
+ }
4689
+ const pendingUrl = typeof tab.pendingUrl === "string" && tab.pendingUrl.length > 0 ? tab.pendingUrl : undefined;
4690
+ const liveUrl = typeof tab.url === "string" && tab.url.length > 0 ? tab.url : undefined;
4691
+ return pendingUrl ?? liveUrl;
4692
+ };
4693
+ const getReportedTabTitle = (tab) => {
4694
+ if (!tab || isTabNavigationPending(tab)) {
4695
+ return undefined;
4696
+ }
4697
+ return typeof tab.title === "string" && tab.title.length > 0 ? tab.title : undefined;
4698
+ };
4699
+ const isTabNavigationPending = (tab) => {
4700
+ return tab?.status === "loading";
4701
+ };
4702
+ const isHtmlDataUrl = (url) => {
4703
+ return url.startsWith("data:text/html");
4704
+ };
4705
+ const isCanvasExtensionUrl = (url) => {
4706
+ try {
4707
+ const canvasUrl = chrome.runtime.getURL("canvas.html");
4708
+ return url === canvasUrl || url.startsWith(`${canvasUrl}#`) || url.startsWith(`${canvasUrl}?`);
4709
+ }
4710
+ catch {
4711
+ return false;
4712
+ }
4713
+ };
4714
+ const decodeHtmlDataUrl = (url) => {
4715
+ if (!isHtmlDataUrl(url)) {
4716
+ return null;
4717
+ }
4718
+ const commaIndex = url.indexOf(",");
4719
+ if (commaIndex === -1) {
4720
+ return null;
4721
+ }
4722
+ const metadata = url.slice(0, commaIndex).toLowerCase();
4723
+ const payload = url.slice(commaIndex + 1);
4724
+ if (metadata.includes(";base64")) {
4725
+ const decoded = atob(payload);
4726
+ const bytes = Uint8Array.from(decoded, (char) => char.charCodeAt(0));
4727
+ return new TextDecoder().decode(bytes);
4728
+ }
4729
+ try {
4730
+ return decodeURIComponent(payload);
4731
+ }
4732
+ catch {
4733
+ return payload;
4734
+ }
4735
+ };
4736
+ const executeInTab = async (tabId, func, args) => {
4737
+ return await new Promise((resolve, reject) => {
4738
+ chrome.scripting.executeScript({ target: { tabId }, func: func, args }, (results) => {
4739
+ const lastError = chrome.runtime.lastError;
4740
+ if (lastError) {
4741
+ reject(new Error(lastError.message));
4742
+ return;
4743
+ }
4744
+ const [first] = results ?? [];
4745
+ resolve((first?.result ?? null));
4746
+ });
4747
+ });
4748
+ };
4749
+ function replaceDocumentWithHtmlScript(input) {
4750
+ document.open();
4751
+ document.write(input.html);
4752
+ document.close();
4753
+ return { title: document.title };
4754
+ }
1732
4755
  const withTimeout = async (promise, timeoutMs, message) => {
1733
4756
  return await new Promise((resolve, reject) => {
1734
4757
  const timeoutId = setTimeout(() => {