opendevbrowser 0.0.17 → 0.0.19

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 (529) hide show
  1. package/README.md +172 -73
  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 +4 -1
  12. package/dist/browser/annotation-manager.d.ts.map +1 -1
  13. package/dist/browser/browser-manager.d.ts +147 -47
  14. package/dist/browser/browser-manager.d.ts.map +1 -1
  15. package/dist/browser/canvas-client.d.ts +1 -0
  16. package/dist/browser/canvas-client.d.ts.map +1 -1
  17. package/dist/browser/canvas-code-sync-manager.d.ts +9 -1
  18. package/dist/browser/canvas-code-sync-manager.d.ts.map +1 -1
  19. package/dist/browser/canvas-manager.d.ts +29 -1
  20. package/dist/browser/canvas-manager.d.ts.map +1 -1
  21. package/dist/browser/global-challenge-coordinator.d.ts +27 -0
  22. package/dist/browser/global-challenge-coordinator.d.ts.map +1 -0
  23. package/dist/browser/manager-types.d.ts +167 -1
  24. package/dist/browser/manager-types.d.ts.map +1 -1
  25. package/dist/browser/ops-browser-manager.d.ts +103 -3
  26. package/dist/browser/ops-browser-manager.d.ts.map +1 -1
  27. package/dist/browser/ops-client.d.ts +17 -1
  28. package/dist/browser/ops-client.d.ts.map +1 -1
  29. package/dist/browser/playwright-runtime.d.ts +4 -0
  30. package/dist/browser/playwright-runtime.d.ts.map +1 -0
  31. package/dist/browser/review-surface.d.ts +9 -0
  32. package/dist/browser/review-surface.d.ts.map +1 -0
  33. package/dist/browser/screencast-recorder.d.ts +57 -0
  34. package/dist/browser/screencast-recorder.d.ts.map +1 -0
  35. package/dist/browser/session-inspector.d.ts +71 -0
  36. package/dist/browser/session-inspector.d.ts.map +1 -0
  37. package/dist/browser/session-store.d.ts +5 -1
  38. package/dist/browser/session-store.d.ts.map +1 -1
  39. package/dist/browser/system-chrome-cookies.d.ts +46 -0
  40. package/dist/browser/system-chrome-cookies.d.ts.map +1 -0
  41. package/dist/browser/target-manager.d.ts +1 -0
  42. package/dist/browser/target-manager.d.ts.map +1 -1
  43. package/dist/cache/chrome-locator.d.ts.map +1 -1
  44. package/dist/cache/chrome-user-data.d.ts +17 -0
  45. package/dist/cache/chrome-user-data.d.ts.map +1 -0
  46. package/dist/canvas/adapter-plugins/loader.d.ts +13 -0
  47. package/dist/canvas/adapter-plugins/loader.d.ts.map +1 -0
  48. package/dist/canvas/adapter-plugins/manifest.d.ts +146 -0
  49. package/dist/canvas/adapter-plugins/manifest.d.ts.map +1 -0
  50. package/dist/canvas/adapter-plugins/types.d.ts +83 -0
  51. package/dist/canvas/adapter-plugins/types.d.ts.map +1 -0
  52. package/dist/canvas/adapter-plugins/validator.d.ts +10 -0
  53. package/dist/canvas/adapter-plugins/validator.d.ts.map +1 -0
  54. package/dist/canvas/code-sync/apply-tsx.d.ts +3 -1
  55. package/dist/canvas/code-sync/apply-tsx.d.ts.map +1 -1
  56. package/dist/canvas/code-sync/import.d.ts +1 -0
  57. package/dist/canvas/code-sync/import.d.ts.map +1 -1
  58. package/dist/canvas/code-sync/manifest.d.ts +2 -1
  59. package/dist/canvas/code-sync/manifest.d.ts.map +1 -1
  60. package/dist/canvas/code-sync/tsx-adapter.d.ts.map +1 -1
  61. package/dist/canvas/code-sync/types.d.ts +102 -10
  62. package/dist/canvas/code-sync/types.d.ts.map +1 -1
  63. package/dist/canvas/document-store.d.ts +11 -1
  64. package/dist/canvas/document-store.d.ts.map +1 -1
  65. package/dist/canvas/export.d.ts.map +1 -1
  66. package/dist/canvas/framework-adapters/custom-elements-v1.d.ts +3 -0
  67. package/dist/canvas/framework-adapters/custom-elements-v1.d.ts.map +1 -0
  68. package/dist/canvas/framework-adapters/html-static-v1.d.ts +3 -0
  69. package/dist/canvas/framework-adapters/html-static-v1.d.ts.map +1 -0
  70. package/dist/canvas/framework-adapters/markup.d.ts +9 -0
  71. package/dist/canvas/framework-adapters/markup.d.ts.map +1 -0
  72. package/dist/canvas/framework-adapters/react-tsx-v2.d.ts +3 -0
  73. package/dist/canvas/framework-adapters/react-tsx-v2.d.ts.map +1 -0
  74. package/dist/canvas/framework-adapters/registry.d.ts +12 -0
  75. package/dist/canvas/framework-adapters/registry.d.ts.map +1 -0
  76. package/dist/canvas/framework-adapters/svelte-sfc-v1.d.ts +3 -0
  77. package/dist/canvas/framework-adapters/svelte-sfc-v1.d.ts.map +1 -0
  78. package/dist/canvas/framework-adapters/types.d.ts +57 -0
  79. package/dist/canvas/framework-adapters/types.d.ts.map +1 -0
  80. package/dist/canvas/framework-adapters/vue-sfc-v1.d.ts +3 -0
  81. package/dist/canvas/framework-adapters/vue-sfc-v1.d.ts.map +1 -0
  82. package/dist/canvas/kits/catalog.d.ts +5 -0
  83. package/dist/canvas/kits/catalog.d.ts.map +1 -0
  84. package/dist/canvas/library-adapters/react/index.d.ts +3 -0
  85. package/dist/canvas/library-adapters/react/index.d.ts.map +1 -0
  86. package/dist/canvas/library-adapters/registry.d.ts +11 -0
  87. package/dist/canvas/library-adapters/registry.d.ts.map +1 -0
  88. package/dist/canvas/library-adapters/types.d.ts +43 -0
  89. package/dist/canvas/library-adapters/types.d.ts.map +1 -0
  90. package/dist/canvas/repo-store.d.ts +2 -0
  91. package/dist/canvas/repo-store.d.ts.map +1 -1
  92. package/dist/canvas/starters/catalog.d.ts +34 -0
  93. package/dist/canvas/starters/catalog.d.ts.map +1 -0
  94. package/dist/canvas/token-references.d.ts +22 -0
  95. package/dist/canvas/token-references.d.ts.map +1 -0
  96. package/dist/canvas/types.d.ts +345 -6
  97. package/dist/canvas/types.d.ts.map +1 -1
  98. package/dist/challenges/action-loop.d.ts +13 -0
  99. package/dist/challenges/action-loop.d.ts.map +1 -0
  100. package/dist/challenges/capability-matrix.d.ts +3 -0
  101. package/dist/challenges/capability-matrix.d.ts.map +1 -0
  102. package/dist/challenges/evidence-bundle.d.ts +48 -0
  103. package/dist/challenges/evidence-bundle.d.ts.map +1 -0
  104. package/dist/challenges/governed-adapter-gateway.d.ts +4 -0
  105. package/dist/challenges/governed-adapter-gateway.d.ts.map +1 -0
  106. package/dist/challenges/human-yield-gate.d.ts +20 -0
  107. package/dist/challenges/human-yield-gate.d.ts.map +1 -0
  108. package/dist/challenges/index.d.ts +15 -0
  109. package/dist/challenges/index.d.ts.map +1 -0
  110. package/dist/challenges/interpreter.d.ts +3 -0
  111. package/dist/challenges/interpreter.d.ts.map +1 -0
  112. package/dist/challenges/optional-computer-use-bridge.d.ts +9 -0
  113. package/dist/challenges/optional-computer-use-bridge.d.ts.map +1 -0
  114. package/dist/challenges/orchestrator.d.ts +32 -0
  115. package/dist/challenges/orchestrator.d.ts.map +1 -0
  116. package/dist/challenges/outcome-recorder.d.ts +8 -0
  117. package/dist/challenges/outcome-recorder.d.ts.map +1 -0
  118. package/dist/challenges/owned-environment-lane.d.ts +3 -0
  119. package/dist/challenges/owned-environment-lane.d.ts.map +1 -0
  120. package/dist/challenges/policy-gate.d.ts +9 -0
  121. package/dist/challenges/policy-gate.d.ts.map +1 -0
  122. package/dist/challenges/sanctioned-identity-lane.d.ts +3 -0
  123. package/dist/challenges/sanctioned-identity-lane.d.ts.map +1 -0
  124. package/dist/challenges/service-adapter-lane.d.ts +3 -0
  125. package/dist/challenges/service-adapter-lane.d.ts.map +1 -0
  126. package/dist/challenges/strategy-selector.d.ts +10 -0
  127. package/dist/challenges/strategy-selector.d.ts.map +1 -0
  128. package/dist/challenges/types.d.ts +277 -0
  129. package/dist/challenges/types.d.ts.map +1 -0
  130. package/dist/challenges/verification-gate.d.ts +15 -0
  131. package/dist/challenges/verification-gate.d.ts.map +1 -0
  132. package/dist/chunk-5FZQJRBQ.js +15256 -0
  133. package/dist/chunk-5FZQJRBQ.js.map +1 -0
  134. package/dist/chunk-W4IHGDXV.js +33519 -0
  135. package/dist/chunk-W4IHGDXV.js.map +1 -0
  136. package/dist/chunk-YBQECXZX.js +409 -0
  137. package/dist/chunk-YBQECXZX.js.map +1 -0
  138. package/dist/cli/args.d.ts +4 -4
  139. package/dist/cli/args.d.ts.map +1 -1
  140. package/dist/cli/commands/artifacts.d.ts.map +1 -1
  141. package/dist/cli/commands/canvas.d.ts +7 -7
  142. package/dist/cli/commands/canvas.d.ts.map +1 -1
  143. package/dist/cli/commands/daemon.d.ts +7 -0
  144. package/dist/cli/commands/daemon.d.ts.map +1 -1
  145. package/dist/cli/commands/desktop/accessibility-snapshot.d.ts +3 -0
  146. package/dist/cli/commands/desktop/accessibility-snapshot.d.ts.map +1 -0
  147. package/dist/cli/commands/desktop/active-window.d.ts +3 -0
  148. package/dist/cli/commands/desktop/active-window.d.ts.map +1 -0
  149. package/dist/cli/commands/desktop/capture-desktop.d.ts +3 -0
  150. package/dist/cli/commands/desktop/capture-desktop.d.ts.map +1 -0
  151. package/dist/cli/commands/desktop/capture-window.d.ts +3 -0
  152. package/dist/cli/commands/desktop/capture-window.d.ts.map +1 -0
  153. package/dist/cli/commands/desktop/shared.d.ts +19 -0
  154. package/dist/cli/commands/desktop/shared.d.ts.map +1 -0
  155. package/dist/cli/commands/desktop/status.d.ts +3 -0
  156. package/dist/cli/commands/desktop/status.d.ts.map +1 -0
  157. package/dist/cli/commands/desktop/windows.d.ts +3 -0
  158. package/dist/cli/commands/desktop/windows.d.ts.map +1 -0
  159. package/dist/cli/commands/devtools/dialog.d.ts +19 -0
  160. package/dist/cli/commands/devtools/dialog.d.ts.map +1 -0
  161. package/dist/cli/commands/devtools/screencast-start.d.ts +20 -0
  162. package/dist/cli/commands/devtools/screencast-start.d.ts.map +1 -0
  163. package/dist/cli/commands/devtools/screencast-stop.d.ts +17 -0
  164. package/dist/cli/commands/devtools/screencast-stop.d.ts.map +1 -0
  165. package/dist/cli/commands/devtools/screenshot.d.ts +2 -0
  166. package/dist/cli/commands/devtools/screenshot.d.ts.map +1 -1
  167. package/dist/cli/commands/interact/click.d.ts.map +1 -1
  168. package/dist/cli/commands/interact/pointer-down.d.ts +7 -0
  169. package/dist/cli/commands/interact/pointer-down.d.ts.map +1 -0
  170. package/dist/cli/commands/interact/pointer-drag.d.ts +7 -0
  171. package/dist/cli/commands/interact/pointer-drag.d.ts.map +1 -0
  172. package/dist/cli/commands/interact/pointer-move.d.ts +7 -0
  173. package/dist/cli/commands/interact/pointer-move.d.ts.map +1 -0
  174. package/dist/cli/commands/interact/pointer-shared.d.ts +6 -0
  175. package/dist/cli/commands/interact/pointer-shared.d.ts.map +1 -0
  176. package/dist/cli/commands/interact/pointer-up.d.ts +7 -0
  177. package/dist/cli/commands/interact/pointer-up.d.ts.map +1 -0
  178. package/dist/cli/commands/interact/upload.d.ts +18 -0
  179. package/dist/cli/commands/interact/upload.d.ts.map +1 -0
  180. package/dist/cli/commands/macro-resolve.d.ts +2 -0
  181. package/dist/cli/commands/macro-resolve.d.ts.map +1 -1
  182. package/dist/cli/commands/native.d.ts +10 -7
  183. package/dist/cli/commands/native.d.ts.map +1 -1
  184. package/dist/cli/commands/nav/review.d.ts +7 -0
  185. package/dist/cli/commands/nav/review.d.ts.map +1 -0
  186. package/dist/cli/commands/nav/snapshot.d.ts.map +1 -1
  187. package/dist/cli/commands/pages/open.d.ts.map +1 -1
  188. package/dist/cli/commands/product-video.d.ts +2 -0
  189. package/dist/cli/commands/product-video.d.ts.map +1 -1
  190. package/dist/cli/commands/research.d.ts +3 -0
  191. package/dist/cli/commands/research.d.ts.map +1 -1
  192. package/dist/cli/commands/run.d.ts +14 -0
  193. package/dist/cli/commands/run.d.ts.map +1 -1
  194. package/dist/cli/commands/serve.d.ts +1 -26
  195. package/dist/cli/commands/serve.d.ts.map +1 -1
  196. package/dist/cli/commands/session/connect.d.ts.map +1 -1
  197. package/dist/cli/commands/session/disconnect.d.ts.map +1 -1
  198. package/dist/cli/commands/session/inspector.d.ts +21 -0
  199. package/dist/cli/commands/session/inspector.d.ts.map +1 -0
  200. package/dist/cli/commands/session/launch.d.ts.map +1 -1
  201. package/dist/cli/commands/shopping.d.ts +5 -0
  202. package/dist/cli/commands/shopping.d.ts.map +1 -1
  203. package/dist/cli/commands/status.d.ts +2 -14
  204. package/dist/cli/commands/status.d.ts.map +1 -1
  205. package/dist/cli/commands/targets/new.d.ts.map +1 -1
  206. package/dist/cli/daemon-autostart.d.ts +11 -0
  207. package/dist/cli/daemon-autostart.d.ts.map +1 -1
  208. package/dist/cli/daemon-client.d.ts +3 -0
  209. package/dist/cli/daemon-client.d.ts.map +1 -1
  210. package/dist/cli/daemon-commands.d.ts.map +1 -1
  211. package/dist/cli/daemon-state.d.ts +16 -0
  212. package/dist/cli/daemon-state.d.ts.map +1 -1
  213. package/dist/cli/daemon-status.d.ts +7 -2
  214. package/dist/cli/daemon-status.d.ts.map +1 -1
  215. package/dist/cli/daemon.d.ts +1 -0
  216. package/dist/cli/daemon.d.ts.map +1 -1
  217. package/dist/cli/help.d.ts +15 -4
  218. package/dist/cli/help.d.ts.map +1 -1
  219. package/dist/cli/index.js +2476 -1036
  220. package/dist/cli/index.js.map +1 -1
  221. package/dist/cli/install-autostart-output.d.ts +6 -0
  222. package/dist/cli/install-autostart-output.d.ts.map +1 -0
  223. package/dist/cli/install-autostart-reconciliation.d.ts +23 -0
  224. package/dist/cli/install-autostart-reconciliation.d.ts.map +1 -0
  225. package/dist/cli/installers/skills.d.ts +42 -6
  226. package/dist/cli/installers/skills.d.ts.map +1 -1
  227. package/dist/cli/output.d.ts +3 -0
  228. package/dist/cli/output.d.ts.map +1 -1
  229. package/dist/cli/remote-desktop-runtime.d.ts +15 -0
  230. package/dist/cli/remote-desktop-runtime.d.ts.map +1 -0
  231. package/dist/cli/remote-manager.d.ts +24 -2
  232. package/dist/cli/remote-manager.d.ts.map +1 -1
  233. package/dist/cli/transport-timeouts.d.ts +8 -0
  234. package/dist/cli/transport-timeouts.d.ts.map +1 -0
  235. package/dist/cli/utils/http.d.ts +9 -0
  236. package/dist/cli/utils/http.d.ts.map +1 -1
  237. package/dist/cli/utils/parse.d.ts +2 -0
  238. package/dist/cli/utils/parse.d.ts.map +1 -1
  239. package/dist/cli/utils/skills.d.ts +1 -2
  240. package/dist/cli/utils/skills.d.ts.map +1 -1
  241. package/dist/cli/utils/workflow-message.d.ts +2 -0
  242. package/dist/cli/utils/workflow-message.d.ts.map +1 -0
  243. package/dist/config.d.ts +47 -0
  244. package/dist/config.d.ts.map +1 -1
  245. package/dist/core/bootstrap.d.ts.map +1 -1
  246. package/dist/core/index.d.ts +1 -0
  247. package/dist/core/index.d.ts.map +1 -1
  248. package/dist/core/logging.d.ts +3 -1
  249. package/dist/core/logging.d.ts.map +1 -1
  250. package/dist/core/runtime-assemblies.d.ts +22 -0
  251. package/dist/core/runtime-assemblies.d.ts.map +1 -0
  252. package/dist/core/types.d.ts +15 -0
  253. package/dist/core/types.d.ts.map +1 -1
  254. package/dist/desktop/audit.d.ts +37 -0
  255. package/dist/desktop/audit.d.ts.map +1 -0
  256. package/dist/desktop/errors.d.ts +7 -0
  257. package/dist/desktop/errors.d.ts.map +1 -0
  258. package/dist/desktop/index.d.ts +6 -0
  259. package/dist/desktop/index.d.ts.map +1 -0
  260. package/dist/desktop/runtime.d.ts +26 -0
  261. package/dist/desktop/runtime.d.ts.map +1 -0
  262. package/dist/desktop/types.d.ts +76 -0
  263. package/dist/desktop/types.d.ts.map +1 -0
  264. package/dist/extension-extractor.d.ts +6 -0
  265. package/dist/extension-extractor.d.ts.map +1 -1
  266. package/dist/index.d.ts.map +1 -1
  267. package/dist/index.js +1103 -467
  268. package/dist/index.js.map +1 -1
  269. package/dist/integrations/figma/assets.d.ts +13 -0
  270. package/dist/integrations/figma/assets.d.ts.map +1 -0
  271. package/dist/integrations/figma/auth.d.ts +3 -0
  272. package/dist/integrations/figma/auth.d.ts.map +1 -0
  273. package/dist/integrations/figma/client.d.ts +42 -0
  274. package/dist/integrations/figma/client.d.ts.map +1 -0
  275. package/dist/integrations/figma/mappers.d.ts +23 -0
  276. package/dist/integrations/figma/mappers.d.ts.map +1 -0
  277. package/dist/integrations/figma/normalize.d.ts +99 -0
  278. package/dist/integrations/figma/normalize.d.ts.map +1 -0
  279. package/dist/integrations/figma/url.d.ts +17 -0
  280. package/dist/integrations/figma/url.d.ts.map +1 -0
  281. package/dist/integrations/figma/variables.d.ts +21 -0
  282. package/dist/integrations/figma/variables.d.ts.map +1 -0
  283. package/dist/macros/execute-runtime.d.ts +19 -0
  284. package/dist/macros/execute-runtime.d.ts.map +1 -0
  285. package/dist/macros/execute.d.ts +3 -1
  286. package/dist/macros/execute.d.ts.map +1 -1
  287. package/dist/opendevbrowser.d.ts.map +1 -1
  288. package/dist/opendevbrowser.js +1103 -467
  289. package/dist/opendevbrowser.js.map +1 -1
  290. package/dist/providers/blocker.d.ts.map +1 -1
  291. package/dist/providers/browser-fallback.d.ts +30 -0
  292. package/dist/providers/browser-fallback.d.ts.map +1 -0
  293. package/dist/providers/constraint.d.ts +45 -0
  294. package/dist/providers/constraint.d.ts.map +1 -0
  295. package/dist/providers/index.d.ts +11 -2
  296. package/dist/providers/index.d.ts.map +1 -1
  297. package/dist/providers/policy.d.ts.map +1 -1
  298. package/dist/providers/product-video-compiler.d.ts +92 -0
  299. package/dist/providers/product-video-compiler.d.ts.map +1 -0
  300. package/dist/providers/registry.d.ts +37 -1
  301. package/dist/providers/registry.d.ts.map +1 -1
  302. package/dist/providers/renderer.d.ts.map +1 -1
  303. package/dist/providers/research-compiler.d.ts +64 -0
  304. package/dist/providers/research-compiler.d.ts.map +1 -0
  305. package/dist/providers/research-executor.d.ts +27 -0
  306. package/dist/providers/research-executor.d.ts.map +1 -0
  307. package/dist/providers/runtime-bundle.d.ts +26 -0
  308. package/dist/providers/runtime-bundle.d.ts.map +1 -0
  309. package/dist/providers/runtime-factory.d.ts +6 -1
  310. package/dist/providers/runtime-factory.d.ts.map +1 -1
  311. package/dist/providers/runtime-policy.d.ts +24 -0
  312. package/dist/providers/runtime-policy.d.ts.map +1 -0
  313. package/dist/providers/shared/anti-bot-policy.d.ts +3 -2
  314. package/dist/providers/shared/anti-bot-policy.d.ts.map +1 -1
  315. package/dist/providers/shopping/index.d.ts +11 -1
  316. package/dist/providers/shopping/index.d.ts.map +1 -1
  317. package/dist/providers/shopping-compiler.d.ts +51 -0
  318. package/dist/providers/shopping-compiler.d.ts.map +1 -0
  319. package/dist/providers/shopping-executor.d.ts +18 -0
  320. package/dist/providers/shopping-executor.d.ts.map +1 -0
  321. package/dist/providers/shopping-postprocess.d.ts +46 -0
  322. package/dist/providers/shopping-postprocess.d.ts.map +1 -0
  323. package/dist/providers/shopping-workflow.d.ts +33 -0
  324. package/dist/providers/shopping-workflow.d.ts.map +1 -0
  325. package/dist/providers/social/platform.d.ts +2 -1
  326. package/dist/providers/social/platform.d.ts.map +1 -1
  327. package/dist/providers/social/search-quality.d.ts +16 -0
  328. package/dist/providers/social/search-quality.d.ts.map +1 -0
  329. package/dist/providers/social/youtube-resolver.d.ts +2 -1
  330. package/dist/providers/social/youtube-resolver.d.ts.map +1 -1
  331. package/dist/providers/social/youtube.d.ts.map +1 -1
  332. package/dist/providers/types.d.ts +116 -4
  333. package/dist/providers/types.d.ts.map +1 -1
  334. package/dist/providers/web/crawl-worker.d.ts.map +1 -1
  335. package/dist/providers/web/extract.d.ts +16 -0
  336. package/dist/providers/web/extract.d.ts.map +1 -1
  337. package/dist/providers/web/index.d.ts.map +1 -1
  338. package/dist/providers/workflow-contracts.d.ts +53 -0
  339. package/dist/providers/workflow-contracts.d.ts.map +1 -0
  340. package/dist/providers/workflows.d.ts +30 -6
  341. package/dist/providers/workflows.d.ts.map +1 -1
  342. package/dist/{providers-G3LRHQXX.js → providers-G36AM3Z2.js} +2 -2
  343. package/dist/public-surface/generated-manifest.d.ts +1168 -0
  344. package/dist/public-surface/generated-manifest.d.ts.map +1 -0
  345. package/dist/public-surface/source.d.ts +437 -0
  346. package/dist/public-surface/source.d.ts.map +1 -0
  347. package/dist/relay/protocol.d.ts +25 -3
  348. package/dist/relay/protocol.d.ts.map +1 -1
  349. package/dist/relay/relay-endpoints.d.ts +21 -0
  350. package/dist/relay/relay-endpoints.d.ts.map +1 -1
  351. package/dist/relay/relay-server.d.ts +18 -0
  352. package/dist/relay/relay-server.d.ts.map +1 -1
  353. package/dist/skills/bundled-skill-directories.d.ts +8 -0
  354. package/dist/skills/bundled-skill-directories.d.ts.map +1 -0
  355. package/dist/skills/skill-loader.d.ts +9 -1
  356. package/dist/skills/skill-loader.d.ts.map +1 -1
  357. package/dist/skills/skill-loader.js +7 -0
  358. package/dist/skills/skill-nudge.d.ts.map +1 -1
  359. package/dist/skills/types.d.ts +31 -0
  360. package/dist/skills/types.d.ts.map +1 -1
  361. package/dist/snapshot/ops-snapshot.d.ts +1 -1
  362. package/dist/snapshot/ops-snapshot.d.ts.map +1 -1
  363. package/dist/snapshot/refs.d.ts +6 -1
  364. package/dist/snapshot/refs.d.ts.map +1 -1
  365. package/dist/snapshot/snapshotter.d.ts.map +1 -1
  366. package/dist/tools/connect.d.ts.map +1 -1
  367. package/dist/tools/deps.d.ts +4 -0
  368. package/dist/tools/deps.d.ts.map +1 -1
  369. package/dist/tools/desktop-shared.d.ts +6 -0
  370. package/dist/tools/desktop-shared.d.ts.map +1 -0
  371. package/dist/tools/desktop_accessibility_snapshot.d.ts +4 -0
  372. package/dist/tools/desktop_accessibility_snapshot.d.ts.map +1 -0
  373. package/dist/tools/desktop_active_window.d.ts +4 -0
  374. package/dist/tools/desktop_active_window.d.ts.map +1 -0
  375. package/dist/tools/desktop_capture_desktop.d.ts +4 -0
  376. package/dist/tools/desktop_capture_desktop.d.ts.map +1 -0
  377. package/dist/tools/desktop_capture_window.d.ts +4 -0
  378. package/dist/tools/desktop_capture_window.d.ts.map +1 -0
  379. package/dist/tools/desktop_status.d.ts +4 -0
  380. package/dist/tools/desktop_status.d.ts.map +1 -0
  381. package/dist/tools/desktop_windows.d.ts +4 -0
  382. package/dist/tools/desktop_windows.d.ts.map +1 -0
  383. package/dist/tools/dialog.d.ts +4 -0
  384. package/dist/tools/dialog.d.ts.map +1 -0
  385. package/dist/tools/index.d.ts +3 -0
  386. package/dist/tools/index.d.ts.map +1 -1
  387. package/dist/tools/launch.d.ts.map +1 -1
  388. package/dist/tools/macro_resolve.d.ts.map +1 -1
  389. package/dist/tools/pointer_down.d.ts +4 -0
  390. package/dist/tools/pointer_down.d.ts.map +1 -0
  391. package/dist/tools/pointer_drag.d.ts +4 -0
  392. package/dist/tools/pointer_drag.d.ts.map +1 -0
  393. package/dist/tools/pointer_move.d.ts +4 -0
  394. package/dist/tools/pointer_move.d.ts.map +1 -0
  395. package/dist/tools/pointer_up.d.ts +4 -0
  396. package/dist/tools/pointer_up.d.ts.map +1 -0
  397. package/dist/tools/product_video_run.d.ts.map +1 -1
  398. package/dist/tools/prompting_guide.d.ts.map +1 -1
  399. package/dist/tools/research_run.d.ts.map +1 -1
  400. package/dist/tools/review.d.ts +4 -0
  401. package/dist/tools/review.d.ts.map +1 -0
  402. package/dist/tools/screencast_start.d.ts +4 -0
  403. package/dist/tools/screencast_start.d.ts.map +1 -0
  404. package/dist/tools/screencast_stop.d.ts +4 -0
  405. package/dist/tools/screencast_stop.d.ts.map +1 -0
  406. package/dist/tools/screenshot.d.ts.map +1 -1
  407. package/dist/tools/session_inspector.d.ts +4 -0
  408. package/dist/tools/session_inspector.d.ts.map +1 -0
  409. package/dist/tools/shopping_run.d.ts.map +1 -1
  410. package/dist/tools/skill_list.d.ts.map +1 -1
  411. package/dist/tools/skill_load.d.ts.map +1 -1
  412. package/dist/tools/upload.d.ts +4 -0
  413. package/dist/tools/upload.d.ts.map +1 -0
  414. package/dist/tools/workflow-runtime.d.ts +4 -1
  415. package/dist/tools/workflow-runtime.d.ts.map +1 -1
  416. package/dist/utils/package-assets.d.ts +4 -0
  417. package/dist/utils/package-assets.d.ts.map +1 -0
  418. package/extension/canvas.html +379 -9
  419. package/extension/dist/annotate-content.js +62 -32
  420. package/extension/dist/annotation-payload.js +57 -21
  421. package/extension/dist/background.js +406 -61
  422. package/extension/dist/canvas/canvas-runtime.js +481 -52
  423. package/extension/dist/canvas/model.js +129 -1
  424. package/extension/dist/canvas-page.js +1882 -74
  425. package/extension/dist/ops/dom-bridge.js +139 -0
  426. package/extension/dist/ops/ops-runtime.js +2854 -295
  427. package/extension/dist/ops/ops-session-store.js +83 -5
  428. package/extension/dist/ops/snapshot-builder.js +2 -2
  429. package/extension/dist/ops/snapshot-shared.js +2 -2
  430. package/extension/dist/ops/target-session-coordinator.js +5 -3
  431. package/extension/dist/popup.js +50 -15
  432. package/extension/dist/services/CDPRouter.js +1567 -63
  433. package/extension/dist/services/ConnectionManager.js +436 -78
  434. package/extension/dist/services/RelayClient.js +70 -30
  435. package/extension/dist/services/TabManager.js +83 -10
  436. package/extension/dist/services/TargetSessionMap.js +127 -3
  437. package/extension/dist/services/attach-errors.js +20 -0
  438. package/extension/dist/services/cdp-router-commands.js +135 -8
  439. package/extension/dist/services/url-restrictions.js +9 -13
  440. package/extension/manifest.json +2 -2
  441. package/extension/popup.html +7 -6
  442. package/package.json +15 -7
  443. package/skills/AGENTS.md +9 -8
  444. package/skills/opendevbrowser-best-practices/SKILL.md +118 -9
  445. package/skills/opendevbrowser-best-practices/artifacts/browser-agent-known-issues-matrix.md +1 -0
  446. package/skills/opendevbrowser-best-practices/artifacts/command-channel-reference.md +26 -12
  447. package/skills/opendevbrowser-best-practices/artifacts/parity-gates.md +9 -2
  448. package/skills/opendevbrowser-best-practices/artifacts/provider-workflows.md +6 -0
  449. package/skills/opendevbrowser-best-practices/artifacts/skill-runtime-surface-matrix.md +58 -0
  450. package/skills/opendevbrowser-best-practices/assets/templates/skill-runtime-pack-matrix.json +674 -0
  451. package/skills/opendevbrowser-best-practices/assets/templates/surface-audit-checklist.json +9 -4
  452. package/skills/opendevbrowser-best-practices/scripts/odb-workflow.sh +89 -20
  453. package/skills/opendevbrowser-best-practices/scripts/resolve-odb-cli.sh +100 -0
  454. package/skills/opendevbrowser-best-practices/scripts/run-robustness-audit.sh +1 -0
  455. package/skills/opendevbrowser-best-practices/scripts/validate-skill-assets.sh +256 -116
  456. package/skills/opendevbrowser-best-practices/scripts/validator-fixture-cli.sh +208 -0
  457. package/skills/opendevbrowser-continuity-ledger/SKILL.md +14 -1
  458. package/skills/opendevbrowser-continuity-ledger/scripts/validate-skill-assets.sh +61 -0
  459. package/skills/opendevbrowser-data-extraction/SKILL.md +6 -0
  460. package/skills/opendevbrowser-data-extraction/scripts/validate-skill-assets.sh +112 -0
  461. package/skills/opendevbrowser-design-agent/SKILL.md +275 -0
  462. package/skills/opendevbrowser-design-agent/artifacts/app-shell-and-state-wiring.md +84 -0
  463. package/skills/opendevbrowser-design-agent/artifacts/async-search-state-ownership.md +58 -0
  464. package/skills/opendevbrowser-design-agent/artifacts/component-pattern-index.md +130 -0
  465. package/skills/opendevbrowser-design-agent/artifacts/design-contract-playbook.md +157 -0
  466. package/skills/opendevbrowser-design-agent/artifacts/design-release-gate.md +40 -0
  467. package/skills/opendevbrowser-design-agent/artifacts/design-workflows.md +153 -0
  468. package/skills/opendevbrowser-design-agent/artifacts/existing-surface-adaptation.md +56 -0
  469. package/skills/opendevbrowser-design-agent/artifacts/external-pattern-synthesis.md +103 -0
  470. package/skills/opendevbrowser-design-agent/artifacts/frontend-evaluation-rubric.md +61 -0
  471. package/skills/opendevbrowser-design-agent/artifacts/implementation-anti-patterns.md +163 -0
  472. package/skills/opendevbrowser-design-agent/artifacts/isolated-preview-validation.md +68 -0
  473. package/skills/opendevbrowser-design-agent/artifacts/loading-and-feedback-surfaces.md +56 -0
  474. package/skills/opendevbrowser-design-agent/artifacts/opendevbrowser-ui-example-map.md +44 -0
  475. package/skills/opendevbrowser-design-agent/artifacts/performance-audit-playbook.md +70 -0
  476. package/skills/opendevbrowser-design-agent/artifacts/research-harvest-workflow.md +81 -0
  477. package/skills/opendevbrowser-design-agent/artifacts/scroll-reveal-surface-planning.md +64 -0
  478. package/skills/opendevbrowser-design-agent/artifacts/state-ownership-matrix.md +36 -0
  479. package/skills/opendevbrowser-design-agent/artifacts/theming-and-token-ownership.md +43 -0
  480. package/skills/opendevbrowser-design-agent/assets/templates/canvas-generation-plan.design.v1.json +58 -0
  481. package/skills/opendevbrowser-design-agent/assets/templates/design-audit-report.v1.md +34 -0
  482. package/skills/opendevbrowser-design-agent/assets/templates/design-brief.v1.md +40 -0
  483. package/skills/opendevbrowser-design-agent/assets/templates/design-contract.v1.json +226 -0
  484. package/skills/opendevbrowser-design-agent/assets/templates/design-release-gate.v1.json +35 -0
  485. package/skills/opendevbrowser-design-agent/assets/templates/design-review-checklist.json +57 -0
  486. package/skills/opendevbrowser-design-agent/assets/templates/real-surface-design-matrix.json +32 -0
  487. package/skills/opendevbrowser-design-agent/assets/templates/reference-pattern-board.v1.json +31 -0
  488. package/skills/opendevbrowser-design-agent/scripts/design-workflow.sh +171 -0
  489. package/skills/opendevbrowser-design-agent/scripts/extract-canvas-plan.sh +56 -0
  490. package/skills/opendevbrowser-design-agent/scripts/validate-skill-assets.sh +223 -0
  491. package/skills/opendevbrowser-form-testing/SKILL.md +19 -3
  492. package/skills/opendevbrowser-form-testing/artifacts/form-workflows.md +5 -4
  493. package/skills/opendevbrowser-form-testing/assets/templates/challenge-decision-tree.json +2 -0
  494. package/skills/opendevbrowser-form-testing/scripts/validate-skill-assets.sh +109 -0
  495. package/skills/opendevbrowser-login-automation/SKILL.md +21 -3
  496. package/skills/opendevbrowser-login-automation/artifacts/login-workflows.md +5 -4
  497. package/skills/opendevbrowser-login-automation/assets/templates/auth-signals.json +5 -0
  498. package/skills/opendevbrowser-login-automation/assets/templates/login-scenario-matrix.json +3 -2
  499. package/skills/opendevbrowser-login-automation/scripts/run-login-workflow.sh +17 -1
  500. package/skills/opendevbrowser-login-automation/scripts/validate-skill-assets.sh +133 -0
  501. package/skills/opendevbrowser-product-presentation-asset/SKILL.md +23 -11
  502. package/skills/opendevbrowser-product-presentation-asset/artifacts/asset-pack-assembly.md +5 -3
  503. package/skills/opendevbrowser-product-presentation-asset/assets/templates/shot-list.md +2 -0
  504. package/skills/opendevbrowser-product-presentation-asset/assets/templates/video-assembly.md +3 -2
  505. package/skills/opendevbrowser-product-presentation-asset/scripts/capture-screenshots.sh +5 -1
  506. package/skills/opendevbrowser-product-presentation-asset/scripts/collect-product.sh +6 -2
  507. package/skills/opendevbrowser-product-presentation-asset/scripts/download-images.sh +5 -1
  508. package/skills/opendevbrowser-product-presentation-asset/scripts/render-video-brief.sh +20 -7
  509. package/skills/opendevbrowser-product-presentation-asset/scripts/validate-skill-assets.sh +39 -0
  510. package/skills/opendevbrowser-product-presentation-asset/scripts/write-manifest.sh +5 -1
  511. package/skills/opendevbrowser-research/SKILL.md +14 -6
  512. package/skills/opendevbrowser-research/scripts/render-output.sh +5 -1
  513. package/skills/opendevbrowser-research/scripts/run-research.sh +5 -1
  514. package/skills/opendevbrowser-research/scripts/validate-skill-assets.sh +45 -0
  515. package/skills/opendevbrowser-research/scripts/write-artifacts.sh +5 -1
  516. package/skills/opendevbrowser-shopping/SKILL.md +20 -1
  517. package/skills/opendevbrowser-shopping/scripts/normalize-offers.sh +6 -2
  518. package/skills/opendevbrowser-shopping/scripts/run-deal-hunt.sh +5 -1
  519. package/skills/opendevbrowser-shopping/scripts/run-shopping.sh +5 -1
  520. package/skills/opendevbrowser-shopping/scripts/validate-skill-assets.sh +54 -0
  521. package/dist/chunk-5J3IFL3X.js +0 -16706
  522. package/dist/chunk-5J3IFL3X.js.map +0 -1
  523. package/dist/chunk-D633UO34.js +0 -8149
  524. package/dist/chunk-D633UO34.js.map +0 -1
  525. package/dist/chunk-V7KUDHDG.js +0 -276
  526. package/dist/chunk-V7KUDHDG.js.map +0 -1
  527. package/dist/runtime-factory-BICHDPE7.js +0 -13
  528. /package/dist/{providers-G3LRHQXX.js.map → providers-G36AM3Z2.js.map} +0 -0
  529. /package/dist/{runtime-factory-BICHDPE7.js.map → skills/skill-loader.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,12 +223,20 @@ 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",
37
231
  "canvas.applyRuntimePreviewBridge",
38
232
  "export.clonePage",
39
233
  "export.cloneComponent",
40
234
  "devtools.perf",
41
235
  "page.screenshot"
42
236
  ]);
237
+ const DIALOG_SCOPED_COMMANDS = new Set([
238
+ "page.dialog"
239
+ ]);
43
240
  export class OpsRuntime {
44
241
  sendEnvelope;
45
242
  cdp;
@@ -49,6 +246,10 @@ export class OpsRuntime {
49
246
  dom = new DomBridge();
50
247
  sessions = new OpsSessionStore();
51
248
  encoder = new TextEncoder();
249
+ popupOpenerTabIds = new Map();
250
+ popupAttachDiagnostics = new Map();
251
+ commandCreatedTabs = new Map();
252
+ dialogQueues = new Map();
52
253
  closingTimers = new Map();
53
254
  parallelWaiters = new Map();
54
255
  constructor(options) {
@@ -56,10 +257,29 @@ export class OpsRuntime {
56
257
  this.cdp = options.cdp;
57
258
  this.getCanvasPageState = options.getCanvasPageState;
58
259
  this.performCanvasPageAction = options.performCanvasPageAction;
260
+ chrome.tabs.onCreated.addListener(this.handleTabCreated);
59
261
  chrome.tabs.onRemoved.addListener(this.handleTabRemoved);
60
262
  chrome.tabs.onUpdated.addListener(this.handleTabUpdated);
263
+ chrome.webNavigation?.onCreatedNavigationTarget?.addListener?.(this.handleCreatedNavigationTarget);
61
264
  chrome.debugger.onEvent.addListener(this.handleDebuggerEvent);
62
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;
63
283
  }
64
284
  handleMessage(message) {
65
285
  if (message.type === "ops_hello") {
@@ -122,18 +342,125 @@ export class OpsRuntime {
122
342
  const clientId = message.clientId;
123
343
  if (!clientId)
124
344
  return;
345
+ this.cdp.markClientClosed();
125
346
  const sessions = this.sessions.listOwnedBy(clientId);
126
347
  for (const session of sessions) {
127
- this.markSessionClosing(session, "ops_session_expired");
348
+ if (this.markSessionClosing(session, "ops_session_expired")) {
349
+ this.emitSessionEvent(session, "ops_session_released");
350
+ }
128
351
  }
129
352
  }
130
353
  handleTabRemoved = (tabId) => {
354
+ this.forgetCommandCreatedTab(tabId);
355
+ this.popupOpenerTabIds.delete(tabId);
131
356
  this.handleClosedTarget(tabId, "ops_tab_closed");
132
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
+ }
133
444
  handleTabUpdated = (tabId, changeInfo, tab) => {
134
445
  const session = this.sessions.getByTabId(tabId);
135
- 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
+ }
136
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
+ }
137
464
  if (changeInfo.discarded === true || tab.discarded === true) {
138
465
  session.discardedSignals += 1;
139
466
  }
@@ -148,10 +475,60 @@ export class OpsRuntime {
148
475
  return;
149
476
  void this.handleDebuggerDetachForTab(source.tabId);
150
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
+ }
151
527
  handleDebuggerEvent = (source, method, params) => {
152
- if (typeof source.tabId !== "number")
528
+ const eventTabId = this.cdp.resolveSourceTabId(source);
529
+ if (eventTabId === null)
153
530
  return;
154
- const session = this.sessions.getByTabId(source.tabId);
531
+ const session = this.sessions.getByTabId(eventTabId);
155
532
  if (!session)
156
533
  return;
157
534
  if (method === "Runtime.consoleAPICalled") {
@@ -180,6 +557,29 @@ export class OpsRuntime {
180
557
  }
181
558
  return;
182
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
+ }
183
583
  if (method === "Network.requestWillBeSent") {
184
584
  const payload = params;
185
585
  const requestId = payload.requestId;
@@ -228,6 +628,130 @@ export class OpsRuntime {
228
628
  }
229
629
  }
230
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
+ }
231
755
  async handleRequest(message) {
232
756
  const clientId = message.clientId;
233
757
  if (!clientId) {
@@ -284,6 +808,9 @@ export class OpsRuntime {
284
808
  case "nav.snapshot":
285
809
  await this.withSession(message, clientId, (session) => this.handleSnapshot(message, session));
286
810
  return;
811
+ case "nav.review":
812
+ await this.withSession(message, clientId, (session) => this.handleReview(message, session));
813
+ return;
287
814
  case "interact.click":
288
815
  await this.withSession(message, clientId, (session) => this.handleClick(message, session));
289
816
  return;
@@ -311,6 +838,21 @@ export class OpsRuntime {
311
838
  case "interact.scrollIntoView":
312
839
  await this.withSession(message, clientId, (session) => this.handleScrollIntoView(message, session));
313
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;
314
856
  case "dom.getHtml":
315
857
  await this.withSession(message, clientId, (session) => this.handleDomGetHtml(message, session));
316
858
  return;
@@ -332,6 +874,21 @@ export class OpsRuntime {
332
874
  case "dom.isChecked":
333
875
  await this.withSession(message, clientId, (session) => this.handleDomIsChecked(message, session));
334
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;
335
892
  case "canvas.applyRuntimePreviewBridge":
336
893
  await this.withSession(message, clientId, (session) => this.handleCanvasRuntimePreviewBridge(message, session));
337
894
  return;
@@ -347,6 +904,9 @@ export class OpsRuntime {
347
904
  case "page.screenshot":
348
905
  await this.withSession(message, clientId, (session) => this.handleScreenshot(message, session));
349
906
  return;
907
+ case "page.dialog":
908
+ await this.withSession(message, clientId, (session) => this.handleDialog(message, session));
909
+ return;
350
910
  case "devtools.consolePoll":
351
911
  await this.withSession(message, clientId, (session) => this.handleConsolePoll(message, session));
352
912
  return;
@@ -361,6 +921,13 @@ export class OpsRuntime {
361
921
  const payload = isRecord(message.payload) ? message.payload : {};
362
922
  const parallelismPolicy = parseParallelismPolicy(payload.parallelismPolicy);
363
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;
364
931
  if (startUrl) {
365
932
  try {
366
933
  const restriction = getRestrictionMessage(new URL(startUrl));
@@ -374,15 +941,31 @@ export class OpsRuntime {
374
941
  return;
375
942
  }
376
943
  }
377
- const activeTab = startUrl
944
+ let activeTab = startUrl
378
945
  ? await this.tabs.createTab(startUrl, true)
379
- : 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
+ }
380
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
+ }
381
964
  this.sendError(message, buildError("ops_unavailable", "No active tab to attach.", true));
382
965
  return;
383
966
  }
384
967
  const activeTabId = activeTab.id;
385
- const resolvedTab = startUrl
968
+ let resolvedTab = startUrl
386
969
  ? await this.tabs.waitForTabComplete(activeTabId)
387
970
  .catch(() => undefined)
388
971
  .then(async () => await this.tabs.getTab(activeTabId) ?? activeTab)
@@ -395,16 +978,29 @@ export class OpsRuntime {
395
978
  }
396
979
  }
397
980
  try {
398
- await this.cdp.attach(activeTabId);
981
+ const refreshedTab = isStartUrlConnect
982
+ ? await this.attachStartUrlConnectTab(activeTabId)
983
+ : await this.attachLaunchTargetTab(activeTabId, false);
984
+ if (refreshedTab) {
985
+ resolvedTab = refreshedTab;
986
+ }
399
987
  }
400
988
  catch (error) {
401
989
  const detail = error instanceof Error ? error.message : "Debugger attach failed";
402
- this.sendError(message, buildError("cdp_attach_failed", detail, false));
990
+ this.sendError(message, buildError("cdp_attach_failed", detail, false, this.getDirectAttachErrorDetails(error)));
403
991
  return;
404
992
  }
405
993
  if (!startUrl) {
406
994
  await this.tabs.waitForTabComplete(activeTab.id).catch(() => undefined);
407
995
  }
996
+ try {
997
+ await this.enableTargetDomains(activeTabId, true);
998
+ }
999
+ catch (error) {
1000
+ const detail = error instanceof Error ? error.message : "Debugger attach failed";
1001
+ this.sendError(message, buildError("cdp_attach_failed", detail, false, this.getDirectAttachErrorDetails(error)));
1002
+ return;
1003
+ }
408
1004
  const leaseId = typeof message.leaseId === "string" && message.leaseId.trim().length > 0
409
1005
  ? message.leaseId.trim()
410
1006
  : createId();
@@ -413,15 +1009,8 @@ export class OpsRuntime {
413
1009
  title: resolvedTab.title ?? undefined
414
1010
  }, {
415
1011
  parallelismPolicy
416
- });
417
- await this.enableSessionDomains(session);
418
- this.sendEvent({
419
- type: "ops_event",
420
- clientId,
421
- opsSessionId: session.id,
422
- event: "ops_session_created",
423
- payload: { tabId: session.tabId, targetId: session.targetId }
424
- });
1012
+ }, requestedSessionId);
1013
+ this.emitSessionEvent(session, "ops_session_created");
425
1014
  this.sendResponse(message, {
426
1015
  opsSessionId: session.id,
427
1016
  activeTargetId: session.activeTargetId,
@@ -441,12 +1030,18 @@ export class OpsRuntime {
441
1030
  const session = this.getSessionForMessage(message, clientId);
442
1031
  if (!session)
443
1032
  return;
444
- 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;
445
1039
  this.sendResponse(message, {
446
1040
  mode: "extension",
447
- activeTargetId: session.activeTargetId || null,
448
- url: tab?.url ?? undefined,
449
- title: tab?.title ?? undefined,
1041
+ activeTargetId: reportedTargetId,
1042
+ url: resolveReportedTargetUrl(reportedTarget, tab),
1043
+ title: resolveReportedTargetTitle(reportedTarget, tab),
1044
+ dialog: this.serializeDialogState(session, reportedTargetId),
450
1045
  leaseId: session.leaseId,
451
1046
  state: session.state
452
1047
  });
@@ -454,14 +1049,26 @@ export class OpsRuntime {
454
1049
  async handleTargetsList(message, session) {
455
1050
  const payload = isRecord(message.payload) ? message.payload : {};
456
1051
  const includeUrls = payload.includeUrls === true;
457
- const targets = await Promise.all(Array.from(session.targets.values()).map(async (target) => {
458
- const tab = await this.tabs.getTab(target.tabId);
459
- const synthetic = session.syntheticTargets.get(target.targetId);
460
- return {
1052
+ const targetContexts = [
1053
+ ...Array.from(session.targets.values()).map((target) => ({
461
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) => ({
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,
462
1069
  type: "page",
463
- title: resolveReportedTargetTitle(target, tab?.title, synthetic),
464
- url: includeUrls ? resolveReportedTargetUrl(target, tab?.url, synthetic) : undefined
1070
+ title: resolveReportedTargetTitle(target, tab),
1071
+ url: includeUrls ? resolveReportedTargetUrl(target, tab) : undefined
465
1072
  };
466
1073
  }));
467
1074
  this.sendResponse(message, { activeTargetId: session.activeTargetId || null, targets });
@@ -469,22 +1076,125 @@ export class OpsRuntime {
469
1076
  async handleTargetsUse(message, session) {
470
1077
  const payload = isRecord(message.payload) ? message.payload : {};
471
1078
  const targetId = typeof payload.targetId === "string" ? payload.targetId : null;
472
- if (!targetId || !session.targets.has(targetId)) {
1079
+ if (!targetId || !this.hasOpsTarget(session, targetId)) {
473
1080
  this.sendError(message, buildError("invalid_request", "Unknown targetId", false));
474
1081
  return;
475
1082
  }
476
- session.activeTargetId = targetId;
477
- const target = session.targets.get(targetId) ?? null;
478
- if (target) {
479
- 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
+ }
480
1094
  }
481
- const tab = target ? await this.tabs.getTab(target.tabId) : null;
482
- const synthetic = target ? session.syntheticTargets.get(target.targetId) : undefined;
483
- this.sendResponse(message, {
484
- activeTargetId: targetId,
485
- url: target ? resolveReportedTargetUrl(target, tab?.url, synthetic) : undefined,
486
- title: target ? resolveReportedTargetTitle(target, tab?.title, synthetic) : undefined
487
- });
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);
1111
+ return;
1112
+ }
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
+ }
1196
+ }
1197
+ await this.activateTargetAndRespond(message, session, targetId);
488
1198
  }
489
1199
  async handleTargetsRegisterCanvas(message, session) {
490
1200
  const payload = isRecord(message.payload) ? message.payload : {};
@@ -493,37 +1203,59 @@ export class OpsRuntime {
493
1203
  this.sendError(message, buildError("invalid_request", "Missing targetId", false));
494
1204
  return;
495
1205
  }
1206
+ try {
1207
+ this.sendResponse(message, await this.registerCanvasTarget(session, targetId));
1208
+ }
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));
1228
+ return;
1229
+ }
1230
+ }
1231
+ async registerCanvasTarget(session, targetId) {
496
1232
  const tabId = parseTabTargetId(targetId);
497
1233
  if (tabId === null) {
498
- this.sendError(message, buildError("invalid_request", "Canvas targetId must be tab-<id>.", false));
499
- return;
1234
+ throw new Error("Canvas targetId must be tab-<id>.");
500
1235
  }
501
1236
  let tab = await this.tabs.getTab(tabId);
502
1237
  if (!tab) {
503
- this.sendError(message, buildError("invalid_request", "Unknown targetId", false));
504
- return;
1238
+ throw new Error("Unknown targetId");
505
1239
  }
506
1240
  await this.tabs.waitForTabComplete(tabId, 5000).catch(() => undefined);
507
1241
  tab = await this.tabs.getTab(tabId) ?? tab;
508
1242
  if (!this.isAllowedCanvasTargetUrl(tab.url)) {
509
- this.sendError(message, buildError("restricted_url", "Only the extension canvas tab can be registered.", false));
510
- return;
1243
+ throw new Error("Only the extension canvas tab can be registered.");
511
1244
  }
512
1245
  const existing = session.targets.get(targetId);
513
1246
  if (existing) {
514
1247
  existing.url = tab.url ?? existing.url;
515
1248
  existing.title = tab.title ?? existing.title;
516
1249
  session.activeTargetId = targetId;
517
- this.sendResponse(message, {
1250
+ return {
518
1251
  targetId,
519
1252
  url: existing.url,
520
1253
  title: existing.title,
521
1254
  adopted: false
522
- });
523
- return;
1255
+ };
524
1256
  }
525
1257
  try {
526
- await this.cdp.attach(tabId);
1258
+ await this.attachTargetTab(tabId);
527
1259
  await this.enableTargetDomains(tabId);
528
1260
  }
529
1261
  catch (error) {
@@ -534,33 +1266,43 @@ export class OpsRuntime {
534
1266
  }
535
1267
  const target = this.sessions.addTarget(session.id, tabId, { url: tab.url ?? undefined, title: tab.title ?? undefined });
536
1268
  session.activeTargetId = target.targetId;
537
- this.sendResponse(message, {
1269
+ return {
538
1270
  targetId: target.targetId,
539
1271
  url: target.url,
540
1272
  title: target.title,
541
1273
  adopted: true
542
- });
1274
+ };
543
1275
  }
544
1276
  async handleTargetsNew(message, session) {
545
1277
  const payload = isRecord(message.payload) ? message.payload : {};
546
1278
  const url = typeof payload.url === "string" ? payload.url : undefined;
547
- const tab = await this.tabs.createTab(url, true);
1279
+ const tab = await this.tabs.createTab(url, false);
548
1280
  if (!tab?.id) {
549
1281
  this.sendError(message, buildError("execution_failed", "Target creation failed", false));
550
1282
  return;
551
1283
  }
552
- await this.tabs.waitForTabComplete(tab.id).catch(() => undefined);
1284
+ this.rememberCommandCreatedTab(session.id, tab.id, "targets.new");
553
1285
  try {
554
- await this.cdp.attach(tab.id);
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 });
555
1302
  }
556
- catch (error) {
557
- const detail = error instanceof Error ? error.message : "Debugger attach failed";
558
- this.sendError(message, buildError("cdp_attach_failed", detail, false));
559
- return;
1303
+ finally {
1304
+ this.forgetCommandCreatedTab(tab.id);
560
1305
  }
561
- const target = this.sessions.addTarget(session.id, tab.id, { url: tab.url ?? undefined, title: tab.title ?? undefined });
562
- session.activeTargetId = target.targetId;
563
- this.sendResponse(message, { targetId: target.targetId });
564
1306
  }
565
1307
  async handleTargetsClose(message, session) {
566
1308
  const payload = isRecord(message.payload) ? message.payload : {};
@@ -569,13 +1311,27 @@ export class OpsRuntime {
569
1311
  this.sendError(message, buildError("invalid_request", "Missing targetId", false));
570
1312
  return;
571
1313
  }
572
- const target = session.targets.get(targetId);
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
+ }
573
1329
  if (!target) {
574
1330
  this.sendError(message, buildError("invalid_request", "Unknown targetId", false));
575
1331
  return;
576
1332
  }
577
1333
  this.sessions.removeTarget(session.id, targetId);
578
- await this.closeTabBestEffort(target.tabId);
1334
+ void this.closeTabBestEffort(target.tabId);
579
1335
  if (target.targetId === session.targetId || session.targets.size === 0) {
580
1336
  this.sendResponse(message, { ok: true });
581
1337
  this.scheduleSessionCleanup(session.id, "ops_session_closed");
@@ -597,35 +1353,44 @@ export class OpsRuntime {
597
1353
  return;
598
1354
  }
599
1355
  const url = typeof payload.url === "string" ? payload.url : undefined;
600
- const tab = await this.tabs.createTab(url, true);
1356
+ const tab = await this.tabs.createTab(url, false);
601
1357
  if (!tab?.id) {
602
1358
  this.sendError(message, buildError("execution_failed", "Target creation failed", false));
603
1359
  return;
604
1360
  }
605
- await this.tabs.waitForTabComplete(tab.id).catch(() => undefined);
1361
+ this.rememberCommandCreatedTab(session.id, tab.id, "page.open");
606
1362
  try {
607
- 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 });
608
1380
  }
609
- catch (error) {
610
- const detail = error instanceof Error ? error.message : "Debugger attach failed";
611
- this.sendError(message, buildError("cdp_attach_failed", detail, false));
612
- return;
1381
+ finally {
1382
+ this.forgetCommandCreatedTab(tab.id);
613
1383
  }
614
- const target = this.sessions.addTarget(session.id, tab.id, { url: tab.url ?? undefined, title: tab.title ?? undefined });
615
- this.sessions.setName(session.id, target.targetId, name);
616
- session.activeTargetId = target.targetId;
617
- this.sendResponse(message, { targetId: target.targetId, created: true, url: target.url, title: target.title });
618
1384
  }
619
1385
  async handlePageList(message, session) {
620
1386
  const pages = await Promise.all(this.sessions.listNamedTargets(session.id).map(async ({ name, targetId }) => {
621
- const target = session.targets.get(targetId);
1387
+ const target = this.resolveTargetContext(session, targetId);
622
1388
  const tab = target ? await this.tabs.getTab(target.tabId) : null;
623
- const synthetic = session.syntheticTargets.get(targetId);
624
1389
  return {
625
1390
  name,
626
1391
  targetId,
627
- url: resolveReportedTargetUrl(target, tab?.url, synthetic),
628
- title: resolveReportedTargetTitle(target, tab?.title, synthetic)
1392
+ url: resolveReportedTargetUrl(target, tab),
1393
+ title: resolveReportedTargetTitle(target, tab)
629
1394
  };
630
1395
  }));
631
1396
  this.sendResponse(message, { pages });
@@ -645,7 +1410,7 @@ export class OpsRuntime {
645
1410
  const target = session.targets.get(targetId);
646
1411
  if (target) {
647
1412
  this.sessions.removeTarget(session.id, targetId);
648
- await this.closeTabBestEffort(target.tabId);
1413
+ void this.closeTabBestEffort(target.tabId);
649
1414
  if (target.targetId === session.targetId || session.targets.size === 0) {
650
1415
  this.sendResponse(message, { ok: true });
651
1416
  this.scheduleSessionCleanup(session.id, "ops_session_closed");
@@ -661,11 +1426,14 @@ export class OpsRuntime {
661
1426
  this.sendError(message, buildError("invalid_request", "Missing url", false));
662
1427
  return;
663
1428
  }
1429
+ const syntheticHtml = decodeHtmlDataUrl(url);
664
1430
  try {
665
- const restriction = getRestrictionMessage(new URL(url));
666
- if (restriction) {
667
- this.sendError(message, buildError("restricted_url", restriction, false));
668
- 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
+ }
669
1437
  }
670
1438
  }
671
1439
  catch {
@@ -679,15 +1447,18 @@ export class OpsRuntime {
679
1447
  return;
680
1448
  await this.tabs.activateTab(target.tabId).catch(() => undefined);
681
1449
  const targetRecord = session.targets.get(target.targetId);
682
- const syntheticHtml = decodeHtmlDataUrl(url);
683
1450
  if (syntheticHtml !== null) {
684
1451
  const result = await executeInTab(target.tabId, replaceDocumentWithHtmlScript, [{ html: syntheticHtml }]);
685
1452
  session.refStore.clearTarget(target.targetId);
686
- session.syntheticTargets.set(target.targetId, {
1453
+ this.sessions.upsertSyntheticTarget(session.id, {
1454
+ targetId: target.targetId,
1455
+ tabId: target.tabId,
1456
+ type: "page",
687
1457
  url,
688
1458
  title: typeof result?.title === "string" && result.title.trim().length > 0
689
1459
  ? result.title
690
- : targetRecord?.title
1460
+ : targetRecord?.title,
1461
+ attachedAt: Date.now()
691
1462
  });
692
1463
  this.sendResponse(message, {
693
1464
  finalUrl: url,
@@ -703,7 +1474,7 @@ export class OpsRuntime {
703
1474
  });
704
1475
  await this.tabs.waitForTabComplete(target.tabId, timeoutMs).catch(() => undefined);
705
1476
  const refreshed = await this.tabs.getTab(target.tabId);
706
- session.syntheticTargets.delete(target.targetId);
1477
+ this.sessions.removeSyntheticTarget(session.id, target.targetId);
707
1478
  if (targetRecord) {
708
1479
  session.targets.set(target.targetId, {
709
1480
  ...targetRecord,
@@ -726,11 +1497,16 @@ export class OpsRuntime {
726
1497
  return;
727
1498
  if (typeof payload.ref === "string") {
728
1499
  const state = payload.state === "visible" || payload.state === "hidden" ? payload.state : "attached";
729
- const selector = this.resolveSelector(session, payload.ref, message);
730
- if (!selector)
1500
+ const resolved = this.resolveRefFromPayload(session, payload.ref, message);
1501
+ if (!resolved)
731
1502
  return;
732
1503
  try {
733
- await this.waitForSelector(target, 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
+ }
734
1510
  this.sendResponse(message, { timingMs: Date.now() - start });
735
1511
  }
736
1512
  catch (error) {
@@ -748,64 +1524,90 @@ export class OpsRuntime {
748
1524
  }
749
1525
  async handleSnapshot(message, session) {
750
1526
  const payload = isRecord(message.payload) ? message.payload : {};
751
- const mode = payload.mode === "actionables" ? "actionables" : "outline";
752
- const maxChars = typeof payload.maxChars === "number" ? payload.maxChars : 16000;
753
- const cursor = typeof payload.cursor === "string" ? payload.cursor : undefined;
754
- const maxNodes = typeof payload.maxNodes === "number" ? payload.maxNodes : undefined;
755
- const target = this.requireActiveTarget(session, message);
756
- 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)
757
1534
  return;
758
- const start = Date.now();
759
- const entriesData = await buildSnapshot((method, params) => this.cdp.sendCommand({ tabId: target.tabId }, method, params), mode, true, maxNodes);
760
- const snapshot = session.refStore.setSnapshot(target.targetId, entriesData.entries);
761
- const startIndex = parseCursor(cursor);
762
- const { content, truncated, nextCursor } = paginate(entriesData.lines, startIndex, maxChars);
763
- const contentBytes = this.encoder.encode(content).length;
764
- if (contentBytes > MAX_SNAPSHOT_BYTES) {
765
- this.sendError(message, buildError("snapshot_too_large", "Snapshot exceeded max size.", false, {
766
- maxSnapshotBytes: MAX_SNAPSHOT_BYTES,
767
- actualBytes: contentBytes
768
- }));
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)
769
1556
  return;
770
- }
771
- const tab = await this.tabs.getTab(target.tabId);
772
- const targetRecord = session.targets.get(target.targetId);
773
- const synthetic = session.syntheticTargets.get(target.targetId);
774
1557
  this.sendResponse(message, {
1558
+ sessionId: session.id,
1559
+ targetId: snapshot.target.targetId,
1560
+ mode: "extension",
775
1561
  snapshotId: snapshot.snapshotId,
776
- url: resolveReportedTargetUrl(targetRecord ?? null, tab?.url, synthetic),
777
- title: resolveReportedTargetTitle(targetRecord ?? null, tab?.title, synthetic),
778
- content,
779
- truncated,
780
- nextCursor,
781
- refCount: snapshot.count,
782
- timingMs: Date.now() - start,
783
- 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 } : {})
784
1571
  });
785
1572
  }
786
1573
  async handleClick(message, session) {
787
- const selector = this.resolveSelector(session, message.payload, message);
788
- if (!selector)
789
- return;
790
- const target = this.requireActiveTarget(session, message);
791
- if (!target)
1574
+ const resolved = this.resolveRefFromPayload(session, message.payload, message);
1575
+ if (!resolved)
792
1576
  return;
793
1577
  const start = Date.now();
794
- const before = await this.tabs.getTab(target.tabId);
795
- await this.runElementAction(target, selector, { type: "click" }, () => this.dom.click(target.tabId, selector));
796
- 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);
797
1597
  const navigated = Boolean(before?.url && after?.url && before.url !== after.url);
798
1598
  this.sendResponse(message, { timingMs: Date.now() - start, navigated });
799
1599
  }
800
1600
  async handleHover(message, session) {
801
- const selector = this.resolveSelector(session, message.payload, message);
802
- if (!selector)
803
- return;
804
- const target = this.requireActiveTarget(session, message);
805
- if (!target)
1601
+ const resolved = this.resolveRefFromPayload(session, message.payload, message);
1602
+ if (!resolved)
806
1603
  return;
807
1604
  const start = Date.now();
808
- await this.runElementAction(target, selector, { type: "hover" }, () => 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
+ }
809
1611
  this.sendResponse(message, { timingMs: Date.now() - start });
810
1612
  }
811
1613
  async handlePress(message, session) {
@@ -818,22 +1620,33 @@ export class OpsRuntime {
818
1620
  const target = this.requireActiveTarget(session, message);
819
1621
  if (!target)
820
1622
  return;
821
- const selector = typeof payload.ref === "string" ? this.resolveSelector(session, payload.ref, message) : null;
822
- if (payload.ref && !selector)
1623
+ const resolved = typeof payload.ref === "string" ? this.resolveRefFromPayload(session, payload.ref, message) : null;
1624
+ if (payload.ref && !resolved)
823
1625
  return;
824
1626
  const start = Date.now();
825
- await this.runCanvasPageAction(target, { type: "press", key }, selector, () => 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
+ }
826
1637
  this.sendResponse(message, { timingMs: Date.now() - start });
827
1638
  }
828
1639
  async handleCheck(message, session, checked) {
829
- const selector = this.resolveSelector(session, message.payload, message);
830
- if (!selector)
831
- return;
832
- const target = this.requireActiveTarget(session, message);
833
- if (!target)
1640
+ const resolved = this.resolveRefFromPayload(session, message.payload, message);
1641
+ if (!resolved)
834
1642
  return;
835
1643
  const start = Date.now();
836
- await this.runElementAction(target, selector, { type: "setChecked", checked }, () => 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
+ }
837
1650
  this.sendResponse(message, { timingMs: Date.now() - start });
838
1651
  }
839
1652
  async handleType(message, session) {
@@ -844,14 +1657,16 @@ export class OpsRuntime {
844
1657
  this.sendError(message, buildError("invalid_request", "Missing ref or text", false));
845
1658
  return;
846
1659
  }
847
- const selector = this.resolveSelector(session, ref, message);
848
- if (!selector)
849
- return;
850
- const target = this.requireActiveTarget(session, message);
851
- if (!target)
1660
+ const resolved = this.resolveRefFromPayload(session, ref, message);
1661
+ if (!resolved)
852
1662
  return;
853
1663
  const start = Date.now();
854
- await this.runElementAction(target, selector, { type: "type", value: text, clear: payload.clear === true, submit: payload.submit === true }, () => 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
+ }
855
1670
  this.sendResponse(message, { timingMs: Date.now() - start });
856
1671
  }
857
1672
  async handleSelect(message, session) {
@@ -862,37 +1677,126 @@ export class OpsRuntime {
862
1677
  this.sendError(message, buildError("invalid_request", "Missing ref or values", false));
863
1678
  return;
864
1679
  }
865
- const selector = this.resolveSelector(session, ref, message);
866
- if (!selector)
867
- return;
868
- const target = this.requireActiveTarget(session, message);
869
- if (!target)
1680
+ const resolved = this.resolveRefFromPayload(session, ref, message);
1681
+ if (!resolved)
870
1682
  return;
871
- await this.runElementAction(target, selector, { type: "select", values: values }, () => 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
+ }
872
1689
  this.sendResponse(message, {});
873
1690
  }
874
1691
  async handleScroll(message, session) {
875
1692
  const payload = isRecord(message.payload) ? message.payload : {};
876
1693
  const dy = typeof payload.dy === "number" ? payload.dy : 0;
877
1694
  const ref = typeof payload.ref === "string" ? payload.ref : undefined;
878
- const selector = ref ? this.resolveSelector(session, ref, message) ?? undefined : undefined;
879
- if (ref && !selector)
880
- return;
881
1695
  const target = this.requireActiveTarget(session, message);
882
1696
  if (!target)
883
1697
  return;
884
- await this.runCanvasPageAction(target, { type: "scroll", dy }, selector ?? null, () => 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
+ }
885
1708
  this.sendResponse(message, {});
886
1709
  }
887
1710
  async handleScrollIntoView(message, session) {
888
- const selector = this.resolveSelector(session, message.payload, message);
889
- 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));
890
1787
  return;
1788
+ }
891
1789
  const target = this.requireActiveTarget(session, message);
892
1790
  if (!target)
893
1791
  return;
894
1792
  const start = Date.now();
895
- await this.runElementAction(target, selector, { type: "scrollIntoView" }, () => 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);
896
1800
  this.sendResponse(message, { timingMs: Date.now() - start });
897
1801
  }
898
1802
  async handleDomGetHtml(message, session) {
@@ -903,13 +1807,12 @@ export class OpsRuntime {
903
1807
  this.sendError(message, buildError("invalid_request", "Missing ref", false));
904
1808
  return;
905
1809
  }
906
- const selector = this.resolveSelector(session, ref, message);
907
- if (!selector)
1810
+ const resolved = this.resolveRefFromPayload(session, ref, message);
1811
+ if (!resolved)
908
1812
  return;
909
- const target = this.requireActiveTarget(session, message);
910
- if (!target)
911
- return;
912
- const html = await this.runElementAction(target, selector, { type: "outerHTML" }, () => 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);
913
1816
  const truncated = html.length > maxChars;
914
1817
  const outerHTML = truncated ? html.slice(0, maxChars) : html;
915
1818
  this.sendResponse(message, { outerHTML, truncated });
@@ -922,13 +1825,12 @@ export class OpsRuntime {
922
1825
  this.sendError(message, buildError("invalid_request", "Missing ref", false));
923
1826
  return;
924
1827
  }
925
- const selector = this.resolveSelector(session, ref, message);
926
- if (!selector)
927
- return;
928
- const target = this.requireActiveTarget(session, message);
929
- if (!target)
1828
+ const resolved = this.resolveRefFromPayload(session, ref, message);
1829
+ if (!resolved)
930
1830
  return;
931
- const text = await this.runElementAction(target, selector, { type: "innerText" }, () => 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);
932
1834
  const truncated = text.length > maxChars;
933
1835
  this.sendResponse(message, { text: truncated ? text.slice(0, maxChars) : text, truncated });
934
1836
  }
@@ -940,13 +1842,12 @@ export class OpsRuntime {
940
1842
  this.sendError(message, buildError("invalid_request", "Missing ref or name", false));
941
1843
  return;
942
1844
  }
943
- const selector = this.resolveSelector(session, ref, message);
944
- if (!selector)
945
- return;
946
- const target = this.requireActiveTarget(session, message);
947
- if (!target)
1845
+ const resolved = this.resolveRefFromPayload(session, ref, message);
1846
+ if (!resolved)
948
1847
  return;
949
- const value = await this.runElementAction(target, selector, { type: "getAttr", name }, () => 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]);
950
1851
  this.sendResponse(message, { value });
951
1852
  }
952
1853
  async handleDomGetValue(message, session) {
@@ -956,47 +1857,144 @@ export class OpsRuntime {
956
1857
  this.sendError(message, buildError("invalid_request", "Missing ref", false));
957
1858
  return;
958
1859
  }
959
- const selector = this.resolveSelector(session, ref, message);
960
- if (!selector)
961
- return;
962
- const target = this.requireActiveTarget(session, message);
963
- if (!target)
1860
+ const resolved = this.resolveRefFromPayload(session, ref, message);
1861
+ if (!resolved)
964
1862
  return;
965
- const value = await this.runElementAction(target, selector, { type: "getValue" }, () => 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);
966
1866
  this.sendResponse(message, { value });
967
1867
  }
968
1868
  async handleDomIsVisible(message, session) {
969
- const selector = this.resolveSelector(session, message.payload, message);
970
- if (!selector)
971
- return;
972
- const target = this.requireActiveTarget(session, message);
973
- if (!target)
1869
+ const resolved = this.resolveRefFromPayload(session, message.payload, message);
1870
+ if (!resolved)
974
1871
  return;
975
- const visible = await this.runElementAction(target, selector, { type: "getSelectorState" }, async () => await this.dom.getSelectorState(target.tabId, selector));
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);
976
1875
  const isVisible = typeof visible === "object" && visible !== null && "visible" in visible
977
1876
  ? Boolean(visible.visible)
978
1877
  : Boolean(visible);
979
1878
  this.sendResponse(message, { value: isVisible });
980
1879
  }
981
1880
  async handleDomIsEnabled(message, session) {
982
- const selector = this.resolveSelector(session, message.payload, message);
983
- if (!selector)
1881
+ const resolved = this.resolveRefFromPayload(session, message.payload, message);
1882
+ if (!resolved)
1883
+ return;
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);
1887
+ this.sendResponse(message, { value: enabled });
1888
+ }
1889
+ async handleDomIsChecked(message, session) {
1890
+ const resolved = this.resolveRefFromPayload(session, message.payload, message);
1891
+ if (!resolved)
1892
+ return;
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);
1896
+ this.sendResponse(message, { value: checked });
1897
+ }
1898
+ async handleDomRefPoint(message, session) {
1899
+ const resolved = this.resolveRefFromPayload(session, message.payload, message);
1900
+ if (!resolved)
984
1901
  return;
1902
+ const point = await this.resolveRefPoint(resolved);
1903
+ this.sendResponse(message, point);
1904
+ }
1905
+ async handleCanvasOverlayMount(message, session) {
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";
985
1916
  const target = this.requireActiveTarget(session, message);
986
1917
  if (!target)
987
1918
  return;
988
- const enabled = await this.runElementAction(target, selector, { type: "isEnabled" }, () => this.dom.isEnabled(target.tabId, selector));
989
- this.sendResponse(message, { value: enabled });
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 }
1932
+ });
990
1933
  }
991
- async handleDomIsChecked(message, session) {
992
- const selector = this.resolveSelector(session, message.payload, message);
993
- if (!selector)
1934
+ async handleCanvasOverlayUnmount(message, session) {
1935
+ const payload = isRecord(message.payload) ? message.payload : {};
1936
+ const mountId = typeof payload.mountId === "string" ? payload.mountId.trim() : "";
1937
+ if (!mountId) {
1938
+ this.sendError(message, buildError("invalid_request", "Missing mountId", false));
994
1939
  return;
1940
+ }
995
1941
  const target = this.requireActiveTarget(session, message);
996
1942
  if (!target)
997
1943
  return;
998
- const checked = await this.runElementAction(target, selector, { type: "isChecked" }, () => this.dom.isChecked(target.tabId, selector));
999
- this.sendResponse(message, { value: checked });
1944
+ await this.dom.unmountCanvasOverlay(target.tabId, mountId);
1945
+ this.sendResponse(message, {
1946
+ ok: true,
1947
+ mountId,
1948
+ targetId: target.targetId,
1949
+ overlayState: "idle"
1950
+ });
1951
+ }
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
+ }
1963
+ const target = this.requireActiveTarget(session, message);
1964
+ if (!target)
1965
+ return;
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
+ });
1000
1998
  }
1001
1999
  async handleCanvasRuntimePreviewBridge(message, session) {
1002
2000
  const payload = isRecord(message.payload) ? message.payload : {};
@@ -1058,15 +2056,72 @@ export class OpsRuntime {
1058
2056
  const target = this.requireActiveTarget(session, message);
1059
2057
  if (!target)
1060
2058
  return;
1061
- const result = await this.cdp.sendCommand({ tabId: target.tabId }, "Performance.getMetrics", {});
2059
+ const result = await this.cdp.sendCommand(target.debuggee, "Performance.getMetrics", {});
1062
2060
  this.sendResponse(message, { metrics: Array.isArray(result.metrics) ? result.metrics : [] });
1063
2061
  }
1064
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
+ }
1065
2091
  const target = this.requireActiveTarget(session, message);
1066
2092
  if (!target)
1067
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
+ }
1068
2123
  try {
1069
- 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");
1070
2125
  if (result?.data) {
1071
2126
  this.sendResponse(message, { base64: result.data });
1072
2127
  return;
@@ -1082,16 +2137,112 @@ export class OpsRuntime {
1082
2137
  }
1083
2138
  this.sendError(message, buildError("execution_failed", "Screenshot failed", false));
1084
2139
  }
1085
- async handleConsolePoll(message, session) {
2140
+ async handleUpload(message, session) {
1086
2141
  const payload = isRecord(message.payload) ? message.payload : {};
1087
- const sinceSeq = typeof payload.sinceSeq === "number" ? payload.sinceSeq : 0;
1088
- const max = typeof payload.max === "number" ? payload.max : 50;
1089
- const events = session.consoleEvents.filter((event) => event.seq > sinceSeq).slice(0, max);
1090
- const lastEvent = events.at(-1);
1091
- const nextSeq = lastEvent ? lastEvent.seq : sinceSeq;
1092
- this.sendResponse(message, { events, nextSeq });
1093
- }
1094
- async handleNetworkPoll(message, session) {
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
+ }
2236
+ async handleConsolePoll(message, session) {
2237
+ const payload = isRecord(message.payload) ? message.payload : {};
2238
+ const sinceSeq = typeof payload.sinceSeq === "number" ? payload.sinceSeq : 0;
2239
+ const max = typeof payload.max === "number" ? payload.max : 50;
2240
+ const events = session.consoleEvents.filter((event) => event.seq > sinceSeq).slice(0, max);
2241
+ const lastEvent = events.at(-1);
2242
+ const nextSeq = lastEvent ? lastEvent.seq : sinceSeq;
2243
+ this.sendResponse(message, { events, nextSeq });
2244
+ }
2245
+ async handleNetworkPoll(message, session) {
1095
2246
  const payload = isRecord(message.payload) ? message.payload : {};
1096
2247
  const sinceSeq = typeof payload.sinceSeq === "number" ? payload.sinceSeq : 0;
1097
2248
  const max = typeof payload.max === "number" ? payload.max : 50;
@@ -1134,7 +2285,7 @@ export class OpsRuntime {
1134
2285
  if (!target)
1135
2286
  return;
1136
2287
  try {
1137
- await this.cdp.sendCommand({ tabId: target.tabId }, "Network.setCookies", { cookies: normalized });
2288
+ await this.cdp.sendCommand(target.debuggee, "Network.setCookies", { cookies: normalized });
1138
2289
  }
1139
2290
  catch (error) {
1140
2291
  const detail = error instanceof Error ? error.message : "Cookie import failed";
@@ -1167,7 +2318,7 @@ export class OpsRuntime {
1167
2318
  return;
1168
2319
  let rawCookies = [];
1169
2320
  try {
1170
- 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 } : {});
1171
2322
  rawCookies = Array.isArray(response.cookies) ? response.cookies : [];
1172
2323
  }
1173
2324
  catch (error) {
@@ -1184,23 +2335,323 @@ export class OpsRuntime {
1184
2335
  count: cookies.length
1185
2336
  });
1186
2337
  }
1187
- async enableSessionDomains(session) {
1188
- await this.enableTargetDomains(session.tabId);
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
+ }
1189
2365
  }
1190
- async enableTargetDomains(tabId) {
2366
+ async attachCreatedTargetTab(tabId) {
1191
2367
  try {
1192
- await this.cdp.sendCommand({ tabId }, "Runtime.enable", {});
1193
- await this.cdp.sendCommand({ tabId }, "Network.enable", {});
1194
- await this.cdp.sendCommand({ tabId }, "Performance.enable", {});
2368
+ await this.attachTargetTab(tabId);
2369
+ return;
1195
2370
  }
1196
2371
  catch (error) {
1197
- logError("ops.enable_domains", error, { code: "enable_domains_failed" });
2372
+ if (!isAttachBlockedError(error)) {
2373
+ throw error;
2374
+ }
1198
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) {
2575
+ try {
2576
+ await this.cdp.setDiscoverTargetsEnabled?.(true);
2577
+ }
2578
+ catch (error) {
2579
+ logError("ops.discover_targets", error, {
2580
+ code: "discover_targets_enable_failed",
2581
+ extra: baseDetails
2582
+ });
2583
+ }
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
+ });
1199
2646
  }
1200
2647
  async withSession(message, clientId, handler) {
1201
2648
  const session = this.getSessionForMessage(message, clientId);
1202
2649
  if (!session)
1203
2650
  return;
2651
+ if (DIALOG_SCOPED_COMMANDS.has(message.command)) {
2652
+ await this.withDialogQueue(message, session, handler);
2653
+ return;
2654
+ }
1204
2655
  if (!TARGET_SCOPED_COMMANDS.has(message.command)) {
1205
2656
  session.queue = session.queue.then(() => handler(session), () => handler(session));
1206
2657
  await session.queue;
@@ -1222,6 +2673,9 @@ export class OpsRuntime {
1222
2673
  const requested = typeof payload.targetId === "string" ? payload.targetId.trim() : "";
1223
2674
  return requested || session.activeTargetId || session.targetId;
1224
2675
  }
2676
+ dialogQueueKey(sessionId, targetId) {
2677
+ return `${sessionId}:${targetId}`;
2678
+ }
1225
2679
  sessionQueueAgeMs(session) {
1226
2680
  let oldest = null;
1227
2681
  for (const value of session.targetQueueOldestAt.values()) {
@@ -1352,83 +2806,814 @@ export class OpsRuntime {
1352
2806
  acquired = true;
1353
2807
  await handler(session);
1354
2808
  }
1355
- finally {
1356
- if (acquired) {
1357
- this.releaseParallelSlot(session);
2809
+ finally {
2810
+ if (acquired) {
2811
+ this.releaseParallelSlot(session);
2812
+ }
2813
+ releaseQueue();
2814
+ const depth = (session.targetQueueDepth.get(targetId) ?? 1) - 1;
2815
+ if (depth <= 0) {
2816
+ session.targetQueueDepth.delete(targetId);
2817
+ session.targetQueueOldestAt.delete(targetId);
2818
+ }
2819
+ else {
2820
+ session.targetQueueDepth.set(targetId, depth);
2821
+ }
2822
+ session.pendingParallel = Math.max(0, session.pendingParallel - 1);
2823
+ if (session.targetQueues.get(targetId) === tail) {
2824
+ session.targetQueues.delete(targetId);
2825
+ }
2826
+ }
2827
+ }
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);
1358
3296
  }
1359
- releaseQueue();
1360
- const depth = (session.targetQueueDepth.get(targetId) ?? 1) - 1;
1361
- if (depth <= 0) {
1362
- session.targetQueueDepth.delete(targetId);
1363
- session.targetQueueOldestAt.delete(targetId);
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;
1364
3315
  }
1365
- else {
1366
- session.targetQueueDepth.set(targetId, depth);
3316
+ if (sessionId) {
3317
+ break;
1367
3318
  }
1368
- session.pendingParallel = Math.max(0, session.pendingParallel - 1);
1369
- if (session.targetQueues.get(targetId) === tail) {
1370
- session.targetQueues.delete(targetId);
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;
1371
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);
1372
3351
  }
3352
+ this.clearPopupAttachDiagnostic(session.id, target.targetId);
3353
+ return true;
1373
3354
  }
1374
- getSessionForMessage(message, clientId) {
1375
- const opsSessionId = message.opsSessionId;
1376
- if (!opsSessionId) {
1377
- this.sendError(message, buildError("invalid_request", "Missing opsSessionId", false));
1378
- return null;
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 {};
1379
3375
  }
1380
- const session = this.sessions.get(opsSessionId);
1381
- if (!session) {
1382
- this.sendError(message, buildError("invalid_session", "Unknown ops session", false));
1383
- return null;
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})`;
1384
3419
  }
1385
- if (session.state === "closing") {
1386
- const leaseId = typeof message.leaseId === "string" ? message.leaseId : "";
1387
- if (leaseId && leaseId === session.leaseId) {
1388
- this.reclaimSession(session, clientId);
1389
- }
1390
- else {
1391
- this.sendError(message, buildError("not_owner", "Client does not own session", false));
1392
- return null;
1393
- }
3420
+ if (!details?.stage) {
3421
+ return "";
1394
3422
  }
1395
- if (session.ownerClientId !== clientId) {
1396
- this.sendError(message, buildError("not_owner", "Client does not own session", false));
1397
- return null;
3423
+ return ` (origin: ${details.origin}; stage: ${details.stage})`;
3424
+ }
3425
+ toDirectAttachDiagnosticDetails(diagnostic) {
3426
+ if (!diagnostic) {
3427
+ return undefined;
1398
3428
  }
1399
- if (typeof message.leaseId !== "string" || message.leaseId !== session.leaseId) {
1400
- this.sendError(message, buildError("not_owner", "Lease does not match session owner", false));
1401
- return null;
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);
1402
3441
  }
1403
- session.lastUsedAt = Date.now();
1404
- return session;
3442
+ return this.decorateCdpFailure(error, this.toDirectAttachDiagnosticDetails(diagnostic) ?? {});
1405
3443
  }
1406
- requestedTargetId(session, message) {
1407
- const payload = isRecord(message.payload) ? message.payload : {};
1408
- if (typeof payload.targetId === "string" && payload.targetId.trim().length > 0) {
1409
- return payload.targetId.trim();
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 {};
1410
3553
  }
1411
- return session.activeTargetId || null;
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
+ };
1412
3565
  }
1413
3566
  requireActiveTarget(session, message) {
1414
- const targetId = this.requestedTargetId(session, message);
3567
+ const explicitTargetId = this.extractPayloadTargetId(message.payload);
3568
+ const targetId = explicitTargetId ?? session.activeTargetId ?? session.targetId;
1415
3569
  if (!targetId) {
1416
3570
  this.sendError(message, buildError("invalid_request", "No active target", false));
1417
3571
  return null;
1418
3572
  }
1419
- const target = session.targets.get(targetId);
3573
+ const target = this.resolveRequestedTargetContext(session, targetId, explicitTargetId !== null);
1420
3574
  if (!target) {
1421
3575
  this.sendError(message, buildError("invalid_request", "Active target missing", false));
1422
3576
  return null;
1423
3577
  }
1424
3578
  if (target.url) {
1425
3579
  const restriction = isRestrictedUrl(target.url);
1426
- if (restriction.restricted && !this.isAllowedCanvasTargetUrl(target.url)) {
3580
+ if (restriction.restricted && !this.isAllowedCanvasRestrictionTarget(session, targetId, target)) {
1427
3581
  this.sendError(message, buildError("restricted_url", restriction.message ?? "Restricted tab.", false));
1428
3582
  return null;
1429
3583
  }
1430
3584
  }
1431
- return { tabId: target.tabId, targetId: target.targetId, url: target.url };
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;
1432
3617
  }
1433
3618
  isAllowedCanvasTargetUrl(rawUrl) {
1434
3619
  if (typeof rawUrl !== "string" || rawUrl.length === 0) {
@@ -1517,7 +3702,73 @@ export class OpsRuntime {
1517
3702
  return null;
1518
3703
  }
1519
3704
  }
1520
- resolveSelector(session, refOrPayload, message) {
3705
+ resolveRefContext(session, ref, targetId) {
3706
+ const target = this.resolveTargetContext(session, targetId);
3707
+ if (!target) {
3708
+ return null;
3709
+ }
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)));
3738
+ return null;
3739
+ }
3740
+ }
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
+ };
3770
+ }
3771
+ resolveRefFromPayload(session, refOrPayload, message) {
1521
3772
  const ref = typeof refOrPayload === "string"
1522
3773
  ? refOrPayload
1523
3774
  : (isRecord(refOrPayload) && typeof refOrPayload.ref === "string" ? refOrPayload.ref : null);
@@ -1530,12 +3781,183 @@ export class OpsRuntime {
1530
3781
  this.sendError(message, buildError("invalid_request", "No active target", false));
1531
3782
  return null;
1532
3783
  }
1533
- const entry = session.refStore.resolve(targetId, ref);
1534
- if (!entry) {
1535
- 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));
3787
+ return null;
3788
+ }
3789
+ if (resolved.target.synthetic && !resolved.target.sessionId) {
3790
+ this.sendPopupAttachPendingError(message, session, resolved.target.targetId);
1536
3791
  return null;
1537
3792
  }
1538
- return entry.selector;
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 };
3940
+ }
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");
1539
3961
  }
1540
3962
  async waitForSelector(target, selector, state, timeoutMs) {
1541
3963
  const start = Date.now();
@@ -1551,6 +3973,22 @@ export class OpsRuntime {
1551
3973
  }
1552
3974
  throw new Error("Wait for selector timed out");
1553
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
+ }
1554
3992
  cleanupSession(session, event) {
1555
3993
  this.clearClosingTimer(session.id);
1556
3994
  const waiters = this.parallelWaiters.get(session.id);
@@ -1568,13 +4006,7 @@ export class OpsRuntime {
1568
4006
  for (const target of session.targets.values()) {
1569
4007
  void this.cdp.detachTab(target.tabId).catch(() => undefined);
1570
4008
  }
1571
- this.sendEvent({
1572
- type: "ops_event",
1573
- clientId: session.ownerClientId,
1574
- opsSessionId: session.id,
1575
- event,
1576
- payload: { tabId: session.tabId, targetId: session.targetId }
1577
- });
4009
+ this.emitSessionEvent(session, event);
1578
4010
  }
1579
4011
  handleClosedTarget(tabId, event) {
1580
4012
  const session = this.sessions.getByTabId(tabId);
@@ -1590,7 +4022,7 @@ export class OpsRuntime {
1590
4022
  this.cleanupSession(session, event);
1591
4023
  }
1592
4024
  }
1593
- handleDebuggerDetachForTab(tabId) {
4025
+ async handleDebuggerDetachForTab(tabId) {
1594
4026
  const session = this.sessions.getByTabId(tabId);
1595
4027
  if (!session)
1596
4028
  return;
@@ -1598,6 +4030,21 @@ export class OpsRuntime {
1598
4030
  // Root tab detach can be transient during child-target shutdown; tab removal handler owns root teardown.
1599
4031
  return;
1600
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
+ }
1601
4048
  this.handleClosedTarget(tabId, "ops_session_closed");
1602
4049
  }
1603
4050
  async closeTabBestEffort(tabId) {
@@ -1675,9 +4122,19 @@ export class OpsRuntime {
1675
4122
  sendEvent(event) {
1676
4123
  this.sendEnvelope(event);
1677
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
+ }
1678
4134
  markSessionClosing(session, reason) {
1679
- if (session.state === "closing")
1680
- return;
4135
+ if (session.state === "closing") {
4136
+ return false;
4137
+ }
1681
4138
  session.state = "closing";
1682
4139
  session.closingReason = reason;
1683
4140
  session.expiresAt = Date.now() + SESSION_TTL_MS;
@@ -1689,13 +4146,18 @@ export class OpsRuntime {
1689
4146
  }
1690
4147
  }, SESSION_TTL_MS);
1691
4148
  this.closingTimers.set(session.id, timeoutId);
4149
+ return true;
1692
4150
  }
1693
4151
  reclaimSession(session, clientId) {
4152
+ const wasClosing = session.state === "closing";
1694
4153
  session.ownerClientId = clientId;
1695
4154
  session.state = "active";
1696
4155
  session.expiresAt = undefined;
1697
4156
  session.closingReason = undefined;
1698
4157
  this.clearClosingTimer(session.id);
4158
+ if (wasClosing) {
4159
+ this.emitSessionEvent(session, "ops_session_reclaimed");
4160
+ }
1699
4161
  }
1700
4162
  clearClosingTimer(sessionId) {
1701
4163
  const timer = this.closingTimers.get(sessionId);
@@ -1909,6 +4371,18 @@ const toCookieListRecord = (entry) => {
1909
4371
  const isRecord = (value) => {
1910
4372
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
1911
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
+ };
1912
4386
  const parseCursor = (cursor) => {
1913
4387
  if (!cursor)
1914
4388
  return 0;
@@ -1943,13 +4417,23 @@ const paginate = (lines, startIndex, maxChars) => {
1943
4417
  };
1944
4418
  const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1945
4419
  const parseTabTargetId = (targetId) => {
1946
- const raw = targetId.startsWith("tab-") ? targetId.slice(4) : targetId;
1947
- const parsed = Number.parseInt(raw, 10);
1948
- if (!Number.isFinite(parsed) || parsed <= 0) {
4420
+ const match = /^tab-(\d+)$/.exec(targetId);
4421
+ if (!match) {
1949
4422
  return null;
1950
4423
  }
4424
+ const parsed = Number.parseInt(match[1], 10);
1951
4425
  return parsed;
1952
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
+ };
1953
4437
  const extractBodyHtml = (html) => {
1954
4438
  const bodyMatch = html.match(/<body\b[^>]*>[\s\S]*<\/body>/i);
1955
4439
  if (bodyMatch) {
@@ -2131,27 +4615,102 @@ const escapeCanvasAttribute = (value) => {
2131
4615
  .replaceAll("'", "&#39;");
2132
4616
  };
2133
4617
  const CANVAS_CAPTURE_UNITLESS_STYLES = new Set(["fontWeight", "lineHeight", "opacity", "zIndex"]);
2134
- const resolveReportedTargetUrl = (target, liveUrl, synthetic) => {
2135
- if (typeof synthetic?.url === "string" && isHtmlDataUrl(synthetic.url)) {
2136
- return synthetic.url;
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;
2137
4655
  }
2138
4656
  if (typeof target?.url === "string" && isHtmlDataUrl(target.url)) {
2139
4657
  return target.url;
2140
4658
  }
2141
- return liveUrl ?? target?.url;
4659
+ if (typeof target?.url === "string" && isCanvasExtensionUrl(target.url)) {
4660
+ return target.url;
4661
+ }
4662
+ return getReportedTabUrl(tab) ?? target?.url;
2142
4663
  };
2143
- const resolveReportedTargetTitle = (target, liveTitle, synthetic) => {
2144
- if (typeof synthetic?.title === "string" && synthetic.title.length > 0) {
2145
- return synthetic.title;
4664
+ const resolveReportedTargetTitle = (target, tab) => {
4665
+ if (target?.synthetic === true && typeof target.title === "string" && target.title.length > 0) {
4666
+ return target.title;
2146
4667
  }
2147
4668
  if (typeof target?.url === "string" && isHtmlDataUrl(target.url) && typeof target.title === "string" && target.title.length > 0) {
2148
4669
  return target.title;
2149
4670
  }
2150
- return liveTitle ?? target?.title;
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";
2151
4701
  };
2152
4702
  const isHtmlDataUrl = (url) => {
2153
4703
  return url.startsWith("data:text/html");
2154
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
+ };
2155
4714
  const decodeHtmlDataUrl = (url) => {
2156
4715
  if (!isHtmlDataUrl(url)) {
2157
4716
  return null;