devharness 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (627) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +185 -0
  3. package/build/atomic-write.d.ts +28 -0
  4. package/build/atomic-write.d.ts.map +1 -0
  5. package/build/atomic-write.js +126 -0
  6. package/build/atomic-write.js.map +1 -0
  7. package/build/cdp-manager.d.ts +524 -0
  8. package/build/cdp-manager.d.ts.map +1 -0
  9. package/build/cdp-manager.js +1828 -0
  10. package/build/cdp-manager.js.map +1 -0
  11. package/build/cdp-manager.test.d.ts +6 -0
  12. package/build/cdp-manager.test.d.ts.map +1 -0
  13. package/build/cdp-manager.test.js +546 -0
  14. package/build/cdp-manager.test.js.map +1 -0
  15. package/build/chrome-launcher.d.ts +400 -0
  16. package/build/chrome-launcher.d.ts.map +1 -0
  17. package/build/chrome-launcher.js +1071 -0
  18. package/build/chrome-launcher.js.map +1 -0
  19. package/build/chrome-launcher.test.d.ts +11 -0
  20. package/build/chrome-launcher.test.d.ts.map +1 -0
  21. package/build/chrome-launcher.test.js +204 -0
  22. package/build/chrome-launcher.test.js.map +1 -0
  23. package/build/clickable-cache.d.ts +73 -0
  24. package/build/clickable-cache.d.ts.map +1 -0
  25. package/build/clickable-cache.js +90 -0
  26. package/build/clickable-cache.js.map +1 -0
  27. package/build/command-recorder.d.ts +401 -0
  28. package/build/command-recorder.d.ts.map +1 -0
  29. package/build/command-recorder.js +701 -0
  30. package/build/command-recorder.js.map +1 -0
  31. package/build/config.d.ts +390 -0
  32. package/build/config.d.ts.map +1 -0
  33. package/build/config.js +826 -0
  34. package/build/config.js.map +1 -0
  35. package/build/config.test.d.ts +2 -0
  36. package/build/config.test.d.ts.map +1 -0
  37. package/build/config.test.js +112 -0
  38. package/build/config.test.js.map +1 -0
  39. package/build/connection-manager.d.ts +159 -0
  40. package/build/connection-manager.d.ts.map +1 -0
  41. package/build/connection-manager.js +432 -0
  42. package/build/connection-manager.js.map +1 -0
  43. package/build/connection-manager.test.d.ts +11 -0
  44. package/build/connection-manager.test.d.ts.map +1 -0
  45. package/build/connection-manager.test.js +114 -0
  46. package/build/connection-manager.test.js.map +1 -0
  47. package/build/console-monitor.d.ts +150 -0
  48. package/build/console-monitor.d.ts.map +1 -0
  49. package/build/console-monitor.js +454 -0
  50. package/build/console-monitor.js.map +1 -0
  51. package/build/dashboard/bundle.js +1 -0
  52. package/build/dashboard/dashboard-client.d.ts +42 -0
  53. package/build/dashboard/dashboard-client.d.ts.map +1 -0
  54. package/build/dashboard/dashboard-client.js +184 -0
  55. package/build/dashboard/dashboard-client.js.map +1 -0
  56. package/build/dashboard/dashboard-hub.d.ts +50 -0
  57. package/build/dashboard/dashboard-hub.d.ts.map +1 -0
  58. package/build/dashboard/dashboard-hub.js +654 -0
  59. package/build/dashboard/dashboard-hub.js.map +1 -0
  60. package/build/dashboard/hub-lock.d.ts +16 -0
  61. package/build/dashboard/hub-lock.d.ts.map +1 -0
  62. package/build/dashboard/hub-lock.js +77 -0
  63. package/build/dashboard/hub-lock.js.map +1 -0
  64. package/build/dashboard/index.d.ts +25 -0
  65. package/build/dashboard/index.d.ts.map +1 -0
  66. package/build/dashboard/index.js +77 -0
  67. package/build/dashboard/index.js.map +1 -0
  68. package/build/dashboard/types.d.ts +83 -0
  69. package/build/dashboard/types.d.ts.map +1 -0
  70. package/build/dashboard/types.js +6 -0
  71. package/build/dashboard/types.js.map +1 -0
  72. package/build/debug-logger.d.ts +85 -0
  73. package/build/debug-logger.d.ts.map +1 -0
  74. package/build/debug-logger.js +195 -0
  75. package/build/debug-logger.js.map +1 -0
  76. package/build/debugger-aware-wrapper.d.ts +37 -0
  77. package/build/debugger-aware-wrapper.d.ts.map +1 -0
  78. package/build/debugger-aware-wrapper.js +128 -0
  79. package/build/debugger-aware-wrapper.js.map +1 -0
  80. package/build/dom-change-monitor.d.ts +92 -0
  81. package/build/dom-change-monitor.d.ts.map +1 -0
  82. package/build/dom-change-monitor.js +437 -0
  83. package/build/dom-change-monitor.js.map +1 -0
  84. package/build/element-collector.d.ts +16 -0
  85. package/build/element-collector.d.ts.map +1 -0
  86. package/build/element-collector.js +207 -0
  87. package/build/element-collector.js.map +1 -0
  88. package/build/error-helpers.d.ts +43 -0
  89. package/build/error-helpers.d.ts.map +1 -0
  90. package/build/error-helpers.js +143 -0
  91. package/build/error-helpers.js.map +1 -0
  92. package/build/formatters/console-formatter.d.ts +160 -0
  93. package/build/formatters/console-formatter.d.ts.map +1 -0
  94. package/build/formatters/console-formatter.js +338 -0
  95. package/build/formatters/console-formatter.js.map +1 -0
  96. package/build/helpers/parser-plugins.d.ts +44 -0
  97. package/build/helpers/parser-plugins.d.ts.map +1 -0
  98. package/build/helpers/parser-plugins.js +102 -0
  99. package/build/helpers/parser-plugins.js.map +1 -0
  100. package/build/helpers/paths.d.ts +79 -0
  101. package/build/helpers/paths.d.ts.map +1 -0
  102. package/build/helpers/paths.js +168 -0
  103. package/build/helpers/paths.js.map +1 -0
  104. package/build/helpers/process-liveness.d.ts +24 -0
  105. package/build/helpers/process-liveness.d.ts.map +1 -0
  106. package/build/helpers/process-liveness.js +36 -0
  107. package/build/helpers/process-liveness.js.map +1 -0
  108. package/build/helpers/process-liveness.test.d.ts +2 -0
  109. package/build/helpers/process-liveness.test.d.ts.map +1 -0
  110. package/build/helpers/process-liveness.test.js +40 -0
  111. package/build/helpers/process-liveness.test.js.map +1 -0
  112. package/build/helpers/sequence-tree.d.ts +27 -0
  113. package/build/helpers/sequence-tree.d.ts.map +1 -0
  114. package/build/helpers/sequence-tree.js +80 -0
  115. package/build/helpers/sequence-tree.js.map +1 -0
  116. package/build/helpers/sequence-tree.test.d.ts +2 -0
  117. package/build/helpers/sequence-tree.test.d.ts.map +1 -0
  118. package/build/helpers/sequence-tree.test.js +122 -0
  119. package/build/helpers/sequence-tree.test.js.map +1 -0
  120. package/build/index.d.ts +3 -0
  121. package/build/index.d.ts.map +1 -0
  122. package/build/index.js +2007 -0
  123. package/build/index.js.map +1 -0
  124. package/build/interaction-recorder.d.ts +214 -0
  125. package/build/interaction-recorder.d.ts.map +1 -0
  126. package/build/interaction-recorder.js +2007 -0
  127. package/build/interaction-recorder.js.map +1 -0
  128. package/build/issue-tracker.d.ts +107 -0
  129. package/build/issue-tracker.d.ts.map +1 -0
  130. package/build/issue-tracker.js +582 -0
  131. package/build/issue-tracker.js.map +1 -0
  132. package/build/issue-tracker.test.d.ts +2 -0
  133. package/build/issue-tracker.test.d.ts.map +1 -0
  134. package/build/issue-tracker.test.js +88 -0
  135. package/build/issue-tracker.test.js.map +1 -0
  136. package/build/launch-chrome-port.test.d.ts +14 -0
  137. package/build/launch-chrome-port.test.d.ts.map +1 -0
  138. package/build/launch-chrome-port.test.js +97 -0
  139. package/build/launch-chrome-port.test.js.map +1 -0
  140. package/build/log-processor/orchestrator.d.ts +32 -0
  141. package/build/log-processor/orchestrator.d.ts.map +1 -0
  142. package/build/log-processor/orchestrator.js +27 -0
  143. package/build/log-processor/orchestrator.js.map +1 -0
  144. package/build/logpoint-execution-tracker.d.ts +57 -0
  145. package/build/logpoint-execution-tracker.d.ts.map +1 -0
  146. package/build/logpoint-execution-tracker.js +112 -0
  147. package/build/logpoint-execution-tracker.js.map +1 -0
  148. package/build/mcp-supervisor.d.ts +3 -0
  149. package/build/mcp-supervisor.d.ts.map +1 -0
  150. package/build/mcp-supervisor.js +211 -0
  151. package/build/mcp-supervisor.js.map +1 -0
  152. package/build/messages.d.ts +165 -0
  153. package/build/messages.d.ts.map +1 -0
  154. package/build/messages.js +574 -0
  155. package/build/messages.js.map +1 -0
  156. package/build/network-monitor.d.ts +172 -0
  157. package/build/network-monitor.d.ts.map +1 -0
  158. package/build/network-monitor.js +350 -0
  159. package/build/network-monitor.js.map +1 -0
  160. package/build/overlays.d.ts +49 -0
  161. package/build/overlays.d.ts.map +1 -0
  162. package/build/overlays.js +409 -0
  163. package/build/overlays.js.map +1 -0
  164. package/build/persistent-profiles.test.d.ts +15 -0
  165. package/build/persistent-profiles.test.d.ts.map +1 -0
  166. package/build/persistent-profiles.test.js +470 -0
  167. package/build/persistent-profiles.test.js.map +1 -0
  168. package/build/port-reserver.d.ts +32 -0
  169. package/build/port-reserver.d.ts.map +1 -0
  170. package/build/port-reserver.js +118 -0
  171. package/build/port-reserver.js.map +1 -0
  172. package/build/puppeteer-manager.d.ts +47 -0
  173. package/build/puppeteer-manager.d.ts.map +1 -0
  174. package/build/puppeteer-manager.js +116 -0
  175. package/build/puppeteer-manager.js.map +1 -0
  176. package/build/recording-coverage.test.d.ts +12 -0
  177. package/build/recording-coverage.test.d.ts.map +1 -0
  178. package/build/recording-coverage.test.js +72 -0
  179. package/build/recording-coverage.test.js.map +1 -0
  180. package/build/reference-validator.d.ts +47 -0
  181. package/build/reference-validator.d.ts.map +1 -0
  182. package/build/reference-validator.js +86 -0
  183. package/build/reference-validator.js.map +1 -0
  184. package/build/reference-validator.test.d.ts +5 -0
  185. package/build/reference-validator.test.d.ts.map +1 -0
  186. package/build/reference-validator.test.js +243 -0
  187. package/build/reference-validator.test.js.map +1 -0
  188. package/build/replay-cursor.d.ts +27 -0
  189. package/build/replay-cursor.d.ts.map +1 -0
  190. package/build/replay-cursor.js +240 -0
  191. package/build/replay-cursor.js.map +1 -0
  192. package/build/runners/docker-compose-runner.d.ts +69 -0
  193. package/build/runners/docker-compose-runner.d.ts.map +1 -0
  194. package/build/runners/docker-compose-runner.js +438 -0
  195. package/build/runners/docker-compose-runner.js.map +1 -0
  196. package/build/runners/docker-runner.d.ts +65 -0
  197. package/build/runners/docker-runner.d.ts.map +1 -0
  198. package/build/runners/docker-runner.js +414 -0
  199. package/build/runners/docker-runner.js.map +1 -0
  200. package/build/runners/index.d.ts +19 -0
  201. package/build/runners/index.d.ts.map +1 -0
  202. package/build/runners/index.js +35 -0
  203. package/build/runners/index.js.map +1 -0
  204. package/build/runners/native-runner.d.ts +95 -0
  205. package/build/runners/native-runner.d.ts.map +1 -0
  206. package/build/runners/native-runner.js +370 -0
  207. package/build/runners/native-runner.js.map +1 -0
  208. package/build/runners/types.d.ts +147 -0
  209. package/build/runners/types.d.ts.map +1 -0
  210. package/build/runners/types.js +25 -0
  211. package/build/runners/types.js.map +1 -0
  212. package/build/self-restart.d.ts +12 -0
  213. package/build/self-restart.d.ts.map +1 -0
  214. package/build/self-restart.js +41 -0
  215. package/build/self-restart.js.map +1 -0
  216. package/build/self-restart.test.d.ts +2 -0
  217. package/build/self-restart.test.d.ts.map +1 -0
  218. package/build/self-restart.test.js +34 -0
  219. package/build/self-restart.test.js.map +1 -0
  220. package/build/server-claims.d.ts +161 -0
  221. package/build/server-claims.d.ts.map +1 -0
  222. package/build/server-claims.js +382 -0
  223. package/build/server-claims.js.map +1 -0
  224. package/build/server-claims.test.d.ts +2 -0
  225. package/build/server-claims.test.d.ts.map +1 -0
  226. package/build/server-claims.test.js +197 -0
  227. package/build/server-claims.test.js.map +1 -0
  228. package/build/server-manager.d.ts +438 -0
  229. package/build/server-manager.d.ts.map +1 -0
  230. package/build/server-manager.js +1674 -0
  231. package/build/server-manager.js.map +1 -0
  232. package/build/server-manager.test.d.ts +17 -0
  233. package/build/server-manager.test.d.ts.map +1 -0
  234. package/build/server-manager.test.js +122 -0
  235. package/build/server-manager.test.js.map +1 -0
  236. package/build/server-ownership.test.d.ts +2 -0
  237. package/build/server-ownership.test.d.ts.map +1 -0
  238. package/build/server-ownership.test.js +238 -0
  239. package/build/server-ownership.test.js.map +1 -0
  240. package/build/server-watcher.d.ts +21 -0
  241. package/build/server-watcher.d.ts.map +1 -0
  242. package/build/server-watcher.js +105 -0
  243. package/build/server-watcher.js.map +1 -0
  244. package/build/session-detector.d.ts +47 -0
  245. package/build/session-detector.d.ts.map +1 -0
  246. package/build/session-detector.js +229 -0
  247. package/build/session-detector.js.map +1 -0
  248. package/build/session-detector.test.d.ts +9 -0
  249. package/build/session-detector.test.d.ts.map +1 -0
  250. package/build/session-detector.test.js +45 -0
  251. package/build/session-detector.test.js.map +1 -0
  252. package/build/sourcemap-handler.d.ts +114 -0
  253. package/build/sourcemap-handler.d.ts.map +1 -0
  254. package/build/sourcemap-handler.js +479 -0
  255. package/build/sourcemap-handler.js.map +1 -0
  256. package/build/supervisor/child-manager.d.ts +43 -0
  257. package/build/supervisor/child-manager.d.ts.map +1 -0
  258. package/build/supervisor/child-manager.js +148 -0
  259. package/build/supervisor/child-manager.js.map +1 -0
  260. package/build/supervisor/child-manager.test.d.ts +2 -0
  261. package/build/supervisor/child-manager.test.d.ts.map +1 -0
  262. package/build/supervisor/child-manager.test.js +107 -0
  263. package/build/supervisor/child-manager.test.js.map +1 -0
  264. package/build/supervisor/client-watcher.d.ts +48 -0
  265. package/build/supervisor/client-watcher.d.ts.map +1 -0
  266. package/build/supervisor/client-watcher.js +186 -0
  267. package/build/supervisor/client-watcher.js.map +1 -0
  268. package/build/supervisor/client-watcher.test.d.ts +2 -0
  269. package/build/supervisor/client-watcher.test.d.ts.map +1 -0
  270. package/build/supervisor/client-watcher.test.js +121 -0
  271. package/build/supervisor/client-watcher.test.js.map +1 -0
  272. package/build/supervisor/idle-config.d.ts +30 -0
  273. package/build/supervisor/idle-config.d.ts.map +1 -0
  274. package/build/supervisor/idle-config.js +76 -0
  275. package/build/supervisor/idle-config.js.map +1 -0
  276. package/build/supervisor/idle-config.test.d.ts +2 -0
  277. package/build/supervisor/idle-config.test.d.ts.map +1 -0
  278. package/build/supervisor/idle-config.test.js +119 -0
  279. package/build/supervisor/idle-config.test.js.map +1 -0
  280. package/build/supervisor/ndjson-reader.d.ts +47 -0
  281. package/build/supervisor/ndjson-reader.d.ts.map +1 -0
  282. package/build/supervisor/ndjson-reader.js +75 -0
  283. package/build/supervisor/ndjson-reader.js.map +1 -0
  284. package/build/supervisor/ndjson-reader.test.d.ts +2 -0
  285. package/build/supervisor/ndjson-reader.test.d.ts.map +1 -0
  286. package/build/supervisor/ndjson-reader.test.js +92 -0
  287. package/build/supervisor/ndjson-reader.test.js.map +1 -0
  288. package/build/supervisor/pidfile.d.ts +11 -0
  289. package/build/supervisor/pidfile.d.ts.map +1 -0
  290. package/build/supervisor/pidfile.js +29 -0
  291. package/build/supervisor/pidfile.js.map +1 -0
  292. package/build/supervisor/pidfile.test.d.ts +8 -0
  293. package/build/supervisor/pidfile.test.d.ts.map +1 -0
  294. package/build/supervisor/pidfile.test.js +46 -0
  295. package/build/supervisor/pidfile.test.js.map +1 -0
  296. package/build/supervisor/restart-coordinator.d.ts +73 -0
  297. package/build/supervisor/restart-coordinator.d.ts.map +1 -0
  298. package/build/supervisor/restart-coordinator.js +300 -0
  299. package/build/supervisor/restart-coordinator.js.map +1 -0
  300. package/build/supervisor/restart-coordinator.test.d.ts +2 -0
  301. package/build/supervisor/restart-coordinator.test.d.ts.map +1 -0
  302. package/build/supervisor/restart-coordinator.test.js +434 -0
  303. package/build/supervisor/restart-coordinator.test.js.map +1 -0
  304. package/build/test-support/fake-execute-tool-call.d.ts +11 -0
  305. package/build/test-support/fake-execute-tool-call.d.ts.map +1 -0
  306. package/build/test-support/fake-execute-tool-call.js +19 -0
  307. package/build/test-support/fake-execute-tool-call.js.map +1 -0
  308. package/build/tool-error.d.ts +14 -0
  309. package/build/tool-error.d.ts.map +1 -0
  310. package/build/tool-error.js +18 -0
  311. package/build/tool-error.js.map +1 -0
  312. package/build/tool-response.d.ts +293 -0
  313. package/build/tool-response.d.ts.map +1 -0
  314. package/build/tool-response.js +365 -0
  315. package/build/tool-response.js.map +1 -0
  316. package/build/tools/assert-tools.d.ts +62 -0
  317. package/build/tools/assert-tools.d.ts.map +1 -0
  318. package/build/tools/assert-tools.js +330 -0
  319. package/build/tools/assert-tools.js.map +1 -0
  320. package/build/tools/breakpoint-await-abort.test.d.ts +2 -0
  321. package/build/tools/breakpoint-await-abort.test.d.ts.map +1 -0
  322. package/build/tools/breakpoint-await-abort.test.js +124 -0
  323. package/build/tools/breakpoint-await-abort.test.js.map +1 -0
  324. package/build/tools/breakpoint-tools.d.ts +99 -0
  325. package/build/tools/breakpoint-tools.d.ts.map +1 -0
  326. package/build/tools/breakpoint-tools.js +1197 -0
  327. package/build/tools/breakpoint-tools.js.map +1 -0
  328. package/build/tools/config-tools.d.ts +58 -0
  329. package/build/tools/config-tools.d.ts.map +1 -0
  330. package/build/tools/config-tools.js +180 -0
  331. package/build/tools/config-tools.js.map +1 -0
  332. package/build/tools/connection-routing.test.d.ts +12 -0
  333. package/build/tools/connection-routing.test.d.ts.map +1 -0
  334. package/build/tools/connection-routing.test.js +206 -0
  335. package/build/tools/connection-routing.test.js.map +1 -0
  336. package/build/tools/console-tools.d.ts +84 -0
  337. package/build/tools/console-tools.d.ts.map +1 -0
  338. package/build/tools/console-tools.js +241 -0
  339. package/build/tools/console-tools.js.map +1 -0
  340. package/build/tools/content-tools.d.ts +74 -0
  341. package/build/tools/content-tools.d.ts.map +1 -0
  342. package/build/tools/content-tools.js +582 -0
  343. package/build/tools/content-tools.js.map +1 -0
  344. package/build/tools/dashboard-tools.d.ts +38 -0
  345. package/build/tools/dashboard-tools.d.ts.map +1 -0
  346. package/build/tools/dashboard-tools.js +123 -0
  347. package/build/tools/dashboard-tools.js.map +1 -0
  348. package/build/tools/dom-tools.d.ts +41 -0
  349. package/build/tools/dom-tools.d.ts.map +1 -0
  350. package/build/tools/dom-tools.js +277 -0
  351. package/build/tools/dom-tools.js.map +1 -0
  352. package/build/tools/download-tools.d.ts +55 -0
  353. package/build/tools/download-tools.d.ts.map +1 -0
  354. package/build/tools/download-tools.js +280 -0
  355. package/build/tools/download-tools.js.map +1 -0
  356. package/build/tools/execution-tools.d.ts +40 -0
  357. package/build/tools/execution-tools.d.ts.map +1 -0
  358. package/build/tools/execution-tools.js +121 -0
  359. package/build/tools/execution-tools.js.map +1 -0
  360. package/build/tools/input-abort.test.d.ts +2 -0
  361. package/build/tools/input-abort.test.d.ts.map +1 -0
  362. package/build/tools/input-abort.test.js +227 -0
  363. package/build/tools/input-abort.test.js.map +1 -0
  364. package/build/tools/input-ambiguous-selector.test.d.ts +2 -0
  365. package/build/tools/input-ambiguous-selector.test.d.ts.map +1 -0
  366. package/build/tools/input-ambiguous-selector.test.js +34 -0
  367. package/build/tools/input-ambiguous-selector.test.js.map +1 -0
  368. package/build/tools/input-tools.d.ts +168 -0
  369. package/build/tools/input-tools.d.ts.map +1 -0
  370. package/build/tools/input-tools.js +1397 -0
  371. package/build/tools/input-tools.js.map +1 -0
  372. package/build/tools/inspection-tools.d.ts +112 -0
  373. package/build/tools/inspection-tools.d.ts.map +1 -0
  374. package/build/tools/inspection-tools.js +637 -0
  375. package/build/tools/inspection-tools.js.map +1 -0
  376. package/build/tools/interpolation.d.ts +30 -0
  377. package/build/tools/interpolation.d.ts.map +1 -0
  378. package/build/tools/interpolation.js +143 -0
  379. package/build/tools/interpolation.js.map +1 -0
  380. package/build/tools/issues-abort-hygiene.test.d.ts +2 -0
  381. package/build/tools/issues-abort-hygiene.test.d.ts.map +1 -0
  382. package/build/tools/issues-abort-hygiene.test.js +79 -0
  383. package/build/tools/issues-abort-hygiene.test.js.map +1 -0
  384. package/build/tools/issues-tools.d.ts +98 -0
  385. package/build/tools/issues-tools.d.ts.map +1 -0
  386. package/build/tools/issues-tools.js +821 -0
  387. package/build/tools/issues-tools.js.map +1 -0
  388. package/build/tools/issues-tools.test.d.ts +8 -0
  389. package/build/tools/issues-tools.test.d.ts.map +1 -0
  390. package/build/tools/issues-tools.test.js +184 -0
  391. package/build/tools/issues-tools.test.js.map +1 -0
  392. package/build/tools/modal-tools.d.ts +73 -0
  393. package/build/tools/modal-tools.d.ts.map +1 -0
  394. package/build/tools/modal-tools.js +161 -0
  395. package/build/tools/modal-tools.js.map +1 -0
  396. package/build/tools/network-tools.d.ts +72 -0
  397. package/build/tools/network-tools.d.ts.map +1 -0
  398. package/build/tools/network-tools.js +432 -0
  399. package/build/tools/network-tools.js.map +1 -0
  400. package/build/tools/page-tools.d.ts +78 -0
  401. package/build/tools/page-tools.d.ts.map +1 -0
  402. package/build/tools/page-tools.js +255 -0
  403. package/build/tools/page-tools.js.map +1 -0
  404. package/build/tools/plugin-tools.d.ts +16 -0
  405. package/build/tools/plugin-tools.d.ts.map +1 -0
  406. package/build/tools/plugin-tools.js +17 -0
  407. package/build/tools/plugin-tools.js.map +1 -0
  408. package/build/tools/replay-abort.test.d.ts +2 -0
  409. package/build/tools/replay-abort.test.d.ts.map +1 -0
  410. package/build/tools/replay-abort.test.js +259 -0
  411. package/build/tools/replay-abort.test.js.map +1 -0
  412. package/build/tools/replay-add-conditional.test.d.ts +2 -0
  413. package/build/tools/replay-add-conditional.test.d.ts.map +1 -0
  414. package/build/tools/replay-add-conditional.test.js +205 -0
  415. package/build/tools/replay-add-conditional.test.js.map +1 -0
  416. package/build/tools/replay-background-run.test.d.ts +2 -0
  417. package/build/tools/replay-background-run.test.d.ts.map +1 -0
  418. package/build/tools/replay-background-run.test.js +176 -0
  419. package/build/tools/replay-background-run.test.js.map +1 -0
  420. package/build/tools/replay-capture-variables.test.d.ts +2 -0
  421. package/build/tools/replay-capture-variables.test.d.ts.map +1 -0
  422. package/build/tools/replay-capture-variables.test.js +331 -0
  423. package/build/tools/replay-capture-variables.test.js.map +1 -0
  424. package/build/tools/replay-conditional-idb-and-launch.test.d.ts +11 -0
  425. package/build/tools/replay-conditional-idb-and-launch.test.d.ts.map +1 -0
  426. package/build/tools/replay-conditional-idb-and-launch.test.js +429 -0
  427. package/build/tools/replay-conditional-idb-and-launch.test.js.map +1 -0
  428. package/build/tools/replay-conditional-timeout.test.d.ts +14 -0
  429. package/build/tools/replay-conditional-timeout.test.d.ts.map +1 -0
  430. package/build/tools/replay-conditional-timeout.test.js +126 -0
  431. package/build/tools/replay-conditional-timeout.test.js.map +1 -0
  432. package/build/tools/replay-declare.test.d.ts +2 -0
  433. package/build/tools/replay-declare.test.d.ts.map +1 -0
  434. package/build/tools/replay-declare.test.js +106 -0
  435. package/build/tools/replay-declare.test.js.map +1 -0
  436. package/build/tools/replay-declared-browser-cleanup.test.d.ts +2 -0
  437. package/build/tools/replay-declared-browser-cleanup.test.d.ts.map +1 -0
  438. package/build/tools/replay-declared-browser-cleanup.test.js +103 -0
  439. package/build/tools/replay-declared-browser-cleanup.test.js.map +1 -0
  440. package/build/tools/replay-declared-profile.test.d.ts +2 -0
  441. package/build/tools/replay-declared-profile.test.d.ts.map +1 -0
  442. package/build/tools/replay-declared-profile.test.js +116 -0
  443. package/build/tools/replay-declared-profile.test.js.map +1 -0
  444. package/build/tools/replay-executor.d.ts +556 -0
  445. package/build/tools/replay-executor.d.ts.map +1 -0
  446. package/build/tools/replay-executor.js +2347 -0
  447. package/build/tools/replay-executor.js.map +1 -0
  448. package/build/tools/replay-folder-save.test.d.ts +2 -0
  449. package/build/tools/replay-folder-save.test.d.ts.map +1 -0
  450. package/build/tools/replay-folder-save.test.js +63 -0
  451. package/build/tools/replay-folder-save.test.js.map +1 -0
  452. package/build/tools/replay-formatters.d.ts +148 -0
  453. package/build/tools/replay-formatters.d.ts.map +1 -0
  454. package/build/tools/replay-formatters.js +856 -0
  455. package/build/tools/replay-formatters.js.map +1 -0
  456. package/build/tools/replay-kill-chrome-on-finish.test.d.ts +2 -0
  457. package/build/tools/replay-kill-chrome-on-finish.test.d.ts.map +1 -0
  458. package/build/tools/replay-kill-chrome-on-finish.test.js +175 -0
  459. package/build/tools/replay-kill-chrome-on-finish.test.js.map +1 -0
  460. package/build/tools/replay-multi-connection.test.d.ts +2 -0
  461. package/build/tools/replay-multi-connection.test.d.ts.map +1 -0
  462. package/build/tools/replay-multi-connection.test.js +547 -0
  463. package/build/tools/replay-multi-connection.test.js.map +1 -0
  464. package/build/tools/replay-output-and-record-options.test.d.ts +2 -0
  465. package/build/tools/replay-output-and-record-options.test.d.ts.map +1 -0
  466. package/build/tools/replay-output-and-record-options.test.js +240 -0
  467. package/build/tools/replay-output-and-record-options.test.js.map +1 -0
  468. package/build/tools/replay-production-error-shape.test.d.ts +7 -0
  469. package/build/tools/replay-production-error-shape.test.d.ts.map +1 -0
  470. package/build/tools/replay-production-error-shape.test.js +93 -0
  471. package/build/tools/replay-production-error-shape.test.js.map +1 -0
  472. package/build/tools/replay-rebase.test.d.ts +2 -0
  473. package/build/tools/replay-rebase.test.d.ts.map +1 -0
  474. package/build/tools/replay-rebase.test.js +58 -0
  475. package/build/tools/replay-rebase.test.js.map +1 -0
  476. package/build/tools/replay-retry-classification.test.d.ts +9 -0
  477. package/build/tools/replay-retry-classification.test.d.ts.map +1 -0
  478. package/build/tools/replay-retry-classification.test.js +57 -0
  479. package/build/tools/replay-retry-classification.test.js.map +1 -0
  480. package/build/tools/replay-run-registry.d.ts +55 -0
  481. package/build/tools/replay-run-registry.d.ts.map +1 -0
  482. package/build/tools/replay-run-registry.js +65 -0
  483. package/build/tools/replay-run-registry.js.map +1 -0
  484. package/build/tools/replay-sequence-reload.test.d.ts +2 -0
  485. package/build/tools/replay-sequence-reload.test.d.ts.map +1 -0
  486. package/build/tools/replay-sequence-reload.test.js +82 -0
  487. package/build/tools/replay-sequence-reload.test.js.map +1 -0
  488. package/build/tools/replay-socket-scope.test.d.ts +2 -0
  489. package/build/tools/replay-socket-scope.test.d.ts.map +1 -0
  490. package/build/tools/replay-socket-scope.test.js +75 -0
  491. package/build/tools/replay-socket-scope.test.js.map +1 -0
  492. package/build/tools/replay-step-connection.test.d.ts +2 -0
  493. package/build/tools/replay-step-connection.test.d.ts.map +1 -0
  494. package/build/tools/replay-step-connection.test.js +292 -0
  495. package/build/tools/replay-step-connection.test.js.map +1 -0
  496. package/build/tools/replay-step-timeout.test.d.ts +2 -0
  497. package/build/tools/replay-step-timeout.test.d.ts.map +1 -0
  498. package/build/tools/replay-step-timeout.test.js +204 -0
  499. package/build/tools/replay-step-timeout.test.js.map +1 -0
  500. package/build/tools/replay-tags.test.d.ts +2 -0
  501. package/build/tools/replay-tags.test.d.ts.map +1 -0
  502. package/build/tools/replay-tags.test.js +99 -0
  503. package/build/tools/replay-tags.test.js.map +1 -0
  504. package/build/tools/replay-teardown-and-foreach.test.d.ts +13 -0
  505. package/build/tools/replay-teardown-and-foreach.test.d.ts.map +1 -0
  506. package/build/tools/replay-teardown-and-foreach.test.js +318 -0
  507. package/build/tools/replay-teardown-and-foreach.test.js.map +1 -0
  508. package/build/tools/replay-tool-name-validation.test.d.ts +2 -0
  509. package/build/tools/replay-tool-name-validation.test.d.ts.map +1 -0
  510. package/build/tools/replay-tool-name-validation.test.js +280 -0
  511. package/build/tools/replay-tool-name-validation.test.js.map +1 -0
  512. package/build/tools/replay-tools.d.ts +288 -0
  513. package/build/tools/replay-tools.d.ts.map +1 -0
  514. package/build/tools/replay-tools.js +2994 -0
  515. package/build/tools/replay-tools.js.map +1 -0
  516. package/build/tools/request-abort.test.d.ts +2 -0
  517. package/build/tools/request-abort.test.d.ts.map +1 -0
  518. package/build/tools/request-abort.test.js +130 -0
  519. package/build/tools/request-abort.test.js.map +1 -0
  520. package/build/tools/request-tools.d.ts +56 -0
  521. package/build/tools/request-tools.d.ts.map +1 -0
  522. package/build/tools/request-tools.js +174 -0
  523. package/build/tools/request-tools.js.map +1 -0
  524. package/build/tools/screenshot-tools.d.ts +135 -0
  525. package/build/tools/screenshot-tools.d.ts.map +1 -0
  526. package/build/tools/screenshot-tools.js +636 -0
  527. package/build/tools/screenshot-tools.js.map +1 -0
  528. package/build/tools/server-tools.d.ts +88 -0
  529. package/build/tools/server-tools.d.ts.map +1 -0
  530. package/build/tools/server-tools.js +485 -0
  531. package/build/tools/server-tools.js.map +1 -0
  532. package/build/tools/source-tools.d.ts +46 -0
  533. package/build/tools/source-tools.d.ts.map +1 -0
  534. package/build/tools/source-tools.js +47 -0
  535. package/build/tools/source-tools.js.map +1 -0
  536. package/build/tools/storage-tools.d.ts +141 -0
  537. package/build/tools/storage-tools.d.ts.map +1 -0
  538. package/build/tools/storage-tools.js +894 -0
  539. package/build/tools/storage-tools.js.map +1 -0
  540. package/build/tools/storage-tools.test.d.ts +13 -0
  541. package/build/tools/storage-tools.test.d.ts.map +1 -0
  542. package/build/tools/storage-tools.test.js +707 -0
  543. package/build/tools/storage-tools.test.js.map +1 -0
  544. package/build/tools/tab-tools.d.ts +42 -0
  545. package/build/tools/tab-tools.d.ts.map +1 -0
  546. package/build/tools/tab-tools.js +296 -0
  547. package/build/tools/tab-tools.js.map +1 -0
  548. package/build/tools/wait-tools.d.ts +85 -0
  549. package/build/tools/wait-tools.d.ts.map +1 -0
  550. package/build/tools/wait-tools.js +236 -0
  551. package/build/tools/wait-tools.js.map +1 -0
  552. package/build/tools/wait-tools.test.d.ts +9 -0
  553. package/build/tools/wait-tools.test.d.ts.map +1 -0
  554. package/build/tools/wait-tools.test.js +193 -0
  555. package/build/tools/wait-tools.test.js.map +1 -0
  556. package/build/types.d.ts +133 -0
  557. package/build/types.d.ts.map +1 -0
  558. package/build/types.js +5 -0
  559. package/build/types.js.map +1 -0
  560. package/build/ui-verifier.d.ts +88 -0
  561. package/build/ui-verifier.d.ts.map +1 -0
  562. package/build/ui-verifier.js +540 -0
  563. package/build/ui-verifier.js.map +1 -0
  564. package/build/utils/abort.d.ts +68 -0
  565. package/build/utils/abort.d.ts.map +1 -0
  566. package/build/utils/abort.js +159 -0
  567. package/build/utils/abort.js.map +1 -0
  568. package/build/utils/abort.test.d.ts +2 -0
  569. package/build/utils/abort.test.d.ts.map +1 -0
  570. package/build/utils/abort.test.js +174 -0
  571. package/build/utils/abort.test.js.map +1 -0
  572. package/build/utils/modal-detection-core.d.ts +150 -0
  573. package/build/utils/modal-detection-core.d.ts.map +1 -0
  574. package/build/utils/modal-detection-core.js +332 -0
  575. package/build/utils/modal-detection-core.js.map +1 -0
  576. package/build/utils/modal-detection-core.test.d.ts +5 -0
  577. package/build/utils/modal-detection-core.test.d.ts.map +1 -0
  578. package/build/utils/modal-detection-core.test.js +492 -0
  579. package/build/utils/modal-detection-core.test.js.map +1 -0
  580. package/build/utils/modal-detector.d.ts +29 -0
  581. package/build/utils/modal-detector.d.ts.map +1 -0
  582. package/build/utils/modal-detector.js +281 -0
  583. package/build/utils/modal-detector.js.map +1 -0
  584. package/build/utils/modal-dismissal.d.ts +32 -0
  585. package/build/utils/modal-dismissal.d.ts.map +1 -0
  586. package/build/utils/modal-dismissal.js +190 -0
  587. package/build/utils/modal-dismissal.js.map +1 -0
  588. package/build/utils/port-check.d.ts +31 -0
  589. package/build/utils/port-check.d.ts.map +1 -0
  590. package/build/utils/port-check.js +87 -0
  591. package/build/utils/port-check.js.map +1 -0
  592. package/build/utils/port-check.test.d.ts +2 -0
  593. package/build/utils/port-check.test.d.ts.map +1 -0
  594. package/build/utils/port-check.test.js +66 -0
  595. package/build/utils/port-check.test.js.map +1 -0
  596. package/build/utils/selector-resolver.d.ts +66 -0
  597. package/build/utils/selector-resolver.d.ts.map +1 -0
  598. package/build/utils/selector-resolver.js +200 -0
  599. package/build/utils/selector-resolver.js.map +1 -0
  600. package/build/validation-helpers.d.ts +41 -0
  601. package/build/validation-helpers.d.ts.map +1 -0
  602. package/build/validation-helpers.js +236 -0
  603. package/build/validation-helpers.js.map +1 -0
  604. package/build/window-sizing.d.ts +26 -0
  605. package/build/window-sizing.d.ts.map +1 -0
  606. package/build/window-sizing.js +87 -0
  607. package/build/window-sizing.js.map +1 -0
  608. package/build/window-sizing.test.d.ts +8 -0
  609. package/build/window-sizing.test.d.ts.map +1 -0
  610. package/build/window-sizing.test.js +113 -0
  611. package/build/window-sizing.test.js.map +1 -0
  612. package/docs/README.md +74 -0
  613. package/docs/automation.md +562 -0
  614. package/docs/debugging.md +229 -0
  615. package/docs/installation.md +133 -0
  616. package/docs/instructions.md +227 -0
  617. package/docs/mcp-instructions.md +15 -0
  618. package/docs/message-system.md +327 -0
  619. package/docs/messages.md +3480 -0
  620. package/docs/parser-plugins.md +132 -0
  621. package/docs/replay.md +1411 -0
  622. package/docs/troubleshooting.md +272 -0
  623. package/docs/working_with_llm_notes.md +5 -0
  624. package/package.json +87 -0
  625. package/skills/devharness/SKILL.md +195 -0
  626. package/skills/devharness/references/sequences.md +613 -0
  627. package/skills/devharness/references/tool-categories.md +99 -0
@@ -0,0 +1,2007 @@
1
+ /**
2
+ * Interaction Recorder
3
+ *
4
+ * Records user interactions (mouse, keyboard, navigation) from a browser page
5
+ * for later replay or conversion to test scripts.
6
+ *
7
+ * Actions:
8
+ * - recordInteraction: Start recording
9
+ * - stopInteraction: Stop recording, keep in memory
10
+ * - replayInteraction: Get recorded events by index
11
+ * - clearInteraction: Clear recording from memory
12
+ */
13
+ import { debugLog } from './debug-logger.js';
14
+ // =============================================================================
15
+ // State
16
+ // =============================================================================
17
+ // Active recording session (one per connection)
18
+ const activeSessions = new Map();
19
+ let nextRecordingId = 1;
20
+ // Cleanup handles for page listeners
21
+ const cleanupHandles = new Map();
22
+ // Cancel callbacks for active recordings
23
+ const cancelCallbacks = new Map();
24
+ /**
25
+ * Cancel an active recording without saving
26
+ */
27
+ export async function cancelRecording(connectionReference) {
28
+ const cancel = cancelCallbacks.get(connectionReference);
29
+ if (cancel) {
30
+ await cancel();
31
+ return true;
32
+ }
33
+ return false;
34
+ }
35
+ /**
36
+ * Cancel all active recordings
37
+ */
38
+ export async function cancelAllRecordings() {
39
+ const count = cancelCallbacks.size;
40
+ for (const cancel of cancelCallbacks.values()) {
41
+ await cancel();
42
+ }
43
+ return count;
44
+ }
45
+ // =============================================================================
46
+ // Type Guards
47
+ // =============================================================================
48
+ export function isMouseEvent(event) {
49
+ return 'x' in event && 'y' in event;
50
+ }
51
+ export function isKeyboardEvent(event) {
52
+ return 'key' in event && 'code' in event;
53
+ }
54
+ export function isNavigationEvent(event) {
55
+ return event.type === 'navigation' || event.type === 'reload';
56
+ }
57
+ export function isCommentEvent(event) {
58
+ return event.type === 'comment';
59
+ }
60
+ export function isPasteEvent(event) {
61
+ return event.type === 'paste';
62
+ }
63
+ // =============================================================================
64
+ // Recording Functions
65
+ // =============================================================================
66
+ /**
67
+ * Start recording interactions on a page.
68
+ * Returns a promise that resolves when the recording is completed (via UI or stopRecording).
69
+ */
70
+ export async function startRecording(page, connectionReference, options = {}) {
71
+ // If there's an orphaned recording (e.g., MCP didn't send abort signal), clean it up
72
+ if (activeSessions.has(connectionReference)) {
73
+ await cancelRecording(connectionReference);
74
+ }
75
+ const id = nextRecordingId++;
76
+ const startUrl = page.url();
77
+ const session = {
78
+ id,
79
+ connectionReference,
80
+ startTime: Date.now(),
81
+ startUrl,
82
+ events: [],
83
+ isRecording: true,
84
+ isPaused: false,
85
+ };
86
+ activeSessions.set(connectionReference, session);
87
+ // Note: When issueId is provided, the caller (workOn handler) has already shown
88
+ // the options overlay where the user chose to record. No need for another overlay here.
89
+ const showOverlayOption = options.showOverlay ?? true;
90
+ // Create a promise that will resolve when recording completes or is cancelled
91
+ let resolveRecording;
92
+ const recordingPromise = new Promise((resolve) => {
93
+ resolveRecording = resolve;
94
+ });
95
+ // Store cancel callback for external cancellation
96
+ const cancelFn = async () => {
97
+ // Clean up without creating a recording
98
+ const cleanup = cleanupHandles.get(connectionReference);
99
+ if (cleanup) {
100
+ await cleanup().catch(() => { });
101
+ cleanupHandles.delete(connectionReference);
102
+ }
103
+ activeSessions.delete(connectionReference);
104
+ cancelCallbacks.delete(connectionReference);
105
+ resolveRecording({ success: false, cancelled: true });
106
+ };
107
+ cancelCallbacks.set(connectionReference, cancelFn);
108
+ // Listen for abort signal from MCP (tool cancellation)
109
+ if (options.abortSignal) {
110
+ options.abortSignal.addEventListener('abort', () => {
111
+ cancelFn();
112
+ }, { once: true });
113
+ }
114
+ try {
115
+ // Set up navigation tracking via CDP
116
+ const client = await page.createCDPSession();
117
+ let currentUrl = startUrl;
118
+ // Listen for navigation events
119
+ client.on('Page.frameNavigated', (params) => {
120
+ if (params.frame.parentId)
121
+ return; // Only main frame
122
+ const newUrl = params.frame.url;
123
+ if (newUrl !== currentUrl) {
124
+ const navEvent = {
125
+ type: 'navigation',
126
+ url: newUrl,
127
+ previousUrl: currentUrl,
128
+ timestamp: Date.now(),
129
+ };
130
+ session.events.push(navEvent);
131
+ currentUrl = newUrl;
132
+ }
133
+ });
134
+ await client.send('Page.enable');
135
+ // Set up bindings for UI buttons to call back to server
136
+ await client.send('Runtime.addBinding', { name: '__cdpRecordingComplete' });
137
+ await client.send('Runtime.addBinding', { name: '__cdpRecordingCancel' });
138
+ // Binding to save events before navigation (so they're not lost on refresh)
139
+ await client.send('Runtime.addBinding', { name: '__cdpRecordingSaveEvents' });
140
+ client.on('Runtime.bindingCalled', async (event) => {
141
+ debugLog('recording', `Binding called: ${event.name}`);
142
+ if (event.name === '__cdpRecordingComplete') {
143
+ // UI stop button was clicked - process the recording
144
+ debugLog('recording', 'Processing recording completion...');
145
+ try {
146
+ const payload = JSON.parse(event.payload);
147
+ const browserEvents = payload.events;
148
+ const pausePeriods = payload.pausePeriods || [];
149
+ // Adjust event timestamps to exclude time spent in comment modals
150
+ // For each event, subtract the total pause time that occurred before it
151
+ const adjustedBrowserEvents = browserEvents.map(e => {
152
+ let adjustment = 0;
153
+ for (const pause of pausePeriods) {
154
+ if (e.timestamp > pause.end) {
155
+ // Event occurred after this pause ended - subtract full pause duration
156
+ adjustment += pause.end - pause.start;
157
+ }
158
+ else if (e.timestamp > pause.start) {
159
+ // Event occurred during pause (shouldn't happen normally, but handle it)
160
+ adjustment += e.timestamp - pause.start;
161
+ }
162
+ // If event occurred before pause started, no adjustment needed
163
+ }
164
+ return adjustment > 0 ? { ...e, timestamp: e.timestamp - adjustment } : e;
165
+ });
166
+ // Merge browser events with navigation events from session
167
+ const allEvents = [...session.events, ...adjustedBrowserEvents].sort((a, b) => a.timestamp - b.timestamp);
168
+ const endTime = Date.now();
169
+ const duration = endTime - session.startTime;
170
+ // Calculate summary
171
+ const summary = calculateSummary(allEvents);
172
+ // Store the recording
173
+ const stored = {
174
+ id: session.id,
175
+ connectionReference,
176
+ startTime: session.startTime,
177
+ endTime,
178
+ startUrl: session.startUrl,
179
+ duration,
180
+ events: allEvents,
181
+ summary,
182
+ };
183
+ // Cleanup
184
+ cleanupHandles.delete(connectionReference);
185
+ activeSessions.delete(connectionReference);
186
+ // Resolve the blocking promise - caller should close the tab
187
+ debugLog('recording', `Recording complete with ${allEvents.length} events`);
188
+ resolveRecording({ success: true, id: session.id, recording: stored, closeTab: true });
189
+ }
190
+ catch (e) {
191
+ debugLog('recording', `Error processing UI stop: ${e}`);
192
+ resolveRecording({ success: false, error: String(e) });
193
+ }
194
+ }
195
+ if (event.name === '__cdpRecordingCancel') {
196
+ // Cancel button was clicked - clean up without saving
197
+ cleanupHandles.delete(connectionReference);
198
+ activeSessions.delete(connectionReference);
199
+ cancelCallbacks.delete(connectionReference);
200
+ // Resolve with closeTab flag - caller should close the tab
201
+ resolveRecording({ success: false, cancelled: true, closeTab: true });
202
+ }
203
+ if (event.name === '__cdpRecordingSaveEvents') {
204
+ // Browser is about to navigate - save events to server-side session
205
+ try {
206
+ const payload = JSON.parse(event.payload);
207
+ const browserEvents = payload.events;
208
+ const pausePeriods = payload.pausePeriods || [];
209
+ // Adjust timestamps for pause periods
210
+ const adjustedEvents = browserEvents.map(e => {
211
+ let adjustment = 0;
212
+ for (const pause of pausePeriods) {
213
+ if (e.timestamp > pause.end) {
214
+ adjustment += pause.end - pause.start;
215
+ }
216
+ else if (e.timestamp > pause.start) {
217
+ adjustment += e.timestamp - pause.start;
218
+ }
219
+ }
220
+ return adjustment > 0 ? { ...e, timestamp: e.timestamp - adjustment } : e;
221
+ });
222
+ // Merge with existing session events (navigation events)
223
+ session.events = [...session.events, ...adjustedEvents].sort((a, b) => a.timestamp - b.timestamp);
224
+ }
225
+ catch (e) {
226
+ console.error('[cdp-tools] Error saving events before navigation:', e);
227
+ }
228
+ }
229
+ });
230
+ // Function to inject recording script - can be called multiple times (after navigation)
231
+ const injectRecordingScript = async () => {
232
+ await page.evaluate((showOverlayParam) => {
233
+ // Check if already injected (avoid duplicate listeners)
234
+ if (globalThis.__cdpRecordingInjected)
235
+ return;
236
+ globalThis.__cdpRecordingInjected = true;
237
+ globalThis.__cdpRecordingEvents = [];
238
+ globalThis.__cdpRecordingStart = Date.now();
239
+ globalThis.__cdpRecordingPaused = false;
240
+ globalThis.__cdpRecordingState = 'recording';
241
+ globalThis.__cdpPausePeriods = [];
242
+ const doc = globalThis.document;
243
+ // Save events to server before page unload (refresh/navigation)
244
+ // Use a flag to prevent double-saving (both beforeunload and pagehide can fire)
245
+ let eventsSaved = false;
246
+ const saveEventsBeforeUnload = () => {
247
+ if (eventsSaved)
248
+ return;
249
+ eventsSaved = true;
250
+ const events = globalThis.__cdpRecordingEvents || [];
251
+ const pausePeriods = globalThis.__cdpPausePeriods || [];
252
+ if (events.length > 0) {
253
+ try {
254
+ globalThis.__cdpRecordingSaveEvents(JSON.stringify({ events, pausePeriods }));
255
+ }
256
+ catch (e) {
257
+ // Binding may not be available
258
+ }
259
+ }
260
+ };
261
+ globalThis.addEventListener('beforeunload', saveEventsBeforeUnload);
262
+ // Also handle pagehide for bfcache
263
+ globalThis.addEventListener('pagehide', saveEventsBeforeUnload);
264
+ // Build selector helper - returns unique selector or undefined
265
+ // Strategy aligned with element-collector.ts getUniqueSelector
266
+ const buildSelector = (el) => {
267
+ if (!el || el === doc.body || el === doc.documentElement)
268
+ return undefined;
269
+ const tag = el.tagName?.toLowerCase();
270
+ // 1. ID is always unique and most specific
271
+ if (el.id)
272
+ return `#${el.id}`;
273
+ // 2. For inputs, use name attribute (stable form identifier)
274
+ if (el.name && (tag === 'input' || tag === 'textarea' || tag === 'select')) {
275
+ const selector = `${tag}[name="${el.name}"]`;
276
+ try {
277
+ if (doc.querySelectorAll(selector).length === 1)
278
+ return selector;
279
+ }
280
+ catch (e) { /* invalid selector */ }
281
+ }
282
+ // 3. aria-label is stable and descriptive
283
+ const ariaLabel = el.getAttribute?.('aria-label');
284
+ if (ariaLabel && ariaLabel.length <= 40) {
285
+ const selector = `${tag}[aria-label="${ariaLabel}"]`;
286
+ try {
287
+ if (doc.querySelectorAll(selector).length === 1)
288
+ return selector;
289
+ }
290
+ catch (e) { /* invalid selector */ }
291
+ }
292
+ // 4. href for links (prefer short/relative hrefs)
293
+ if (tag === 'a' && el.href) {
294
+ const href = el.getAttribute?.('href');
295
+ if (href && href !== '#' && !href.startsWith('javascript:') && href.length <= 60) {
296
+ const selector = `a[href="${href}"]`;
297
+ try {
298
+ if (doc.querySelectorAll(selector).length === 1)
299
+ return selector;
300
+ }
301
+ catch (e) { /* invalid selector */ }
302
+ }
303
+ }
304
+ // 5. Try data-testid and other test attributes
305
+ const dataAttrs = ['data-testid', 'data-test-id', 'data-cy', 'data-id'];
306
+ for (const attr of dataAttrs) {
307
+ const val = el.getAttribute?.(attr);
308
+ if (val) {
309
+ const selector = `${tag}[${attr}="${val}"]`;
310
+ try {
311
+ if (doc.querySelectorAll(selector).length === 1)
312
+ return selector;
313
+ }
314
+ catch (e) { /* invalid selector */ }
315
+ }
316
+ }
317
+ // 6. Text-based selector using :has-text() (cdp-tools extended selector)
318
+ const text = el.textContent?.trim();
319
+ if (text && text.length > 0 && text.length <= 30) {
320
+ const escapedText = text.replace(/"/g, '\\"');
321
+ // :has-text is supported by cdp-tools selector resolver
322
+ return `${tag}:has-text("${escapedText}")`;
323
+ }
324
+ // 7. Try class-based selector (filter out hash/generated classes)
325
+ if (el.className && typeof el.className === 'string') {
326
+ const classes = el.className.split(' ').filter((c) => c.length > 0 && c.length <= 20 && !c.includes('__') && !c.match(/^[a-z]+-[a-f0-9]+$/i));
327
+ if (classes.length > 0) {
328
+ const selector = `${tag}.${classes.slice(0, 2).join('.')}`;
329
+ try {
330
+ if (doc.querySelectorAll(selector).length === 1)
331
+ return selector;
332
+ }
333
+ catch (e) { /* invalid selector */ }
334
+ }
335
+ }
336
+ // 8. No unique selector found - return undefined to fall back to coordinates
337
+ return undefined;
338
+ };
339
+ // Get element info helper
340
+ const getElementInfo = (el) => {
341
+ if (!el)
342
+ return undefined;
343
+ const tag = el.tagName?.toLowerCase();
344
+ const rect = el.getBoundingClientRect?.();
345
+ const isCanvas = tag === 'canvas' || tag === 'svg' || el.closest?.('canvas, svg');
346
+ const isInteractive = ['a', 'button', 'input', 'select', 'textarea'].includes(tag) ||
347
+ el.hasAttribute?.('onclick') ||
348
+ (el.hasAttribute?.('role') && ['button', 'link', 'checkbox', 'radio'].includes(el.getAttribute('role')));
349
+ return {
350
+ tag,
351
+ id: el.id || undefined,
352
+ className: typeof el.className === 'string' ? el.className.trim() : undefined,
353
+ text: el.textContent?.trim().substring(0, 50) || el.getAttribute?.('aria-label') || undefined,
354
+ selector: buildSelector(el),
355
+ isCanvas: isCanvas || undefined,
356
+ isInteractive: isInteractive || undefined,
357
+ boundingBox: rect ? { x: Math.round(rect.x), y: Math.round(rect.y), width: Math.round(rect.width), height: Math.round(rect.height) } : undefined,
358
+ };
359
+ };
360
+ // Check if element is part of our overlay UI
361
+ const isOurUI = (el) => {
362
+ if (!el)
363
+ return false;
364
+ // Check if element or any parent is our overlay
365
+ let current = el;
366
+ while (current) {
367
+ if (current.id === '__cdp-recording-overlay')
368
+ return true;
369
+ if (current.classList?.contains('cdp-panel'))
370
+ return true;
371
+ if (current.classList?.contains('cdp-btn'))
372
+ return true;
373
+ if (current.classList?.contains('cdp-comment-modal'))
374
+ return true;
375
+ current = current.parentElement;
376
+ }
377
+ return false;
378
+ };
379
+ // Capture mouse event
380
+ const captureMouseEvent = (e, type) => {
381
+ if (globalThis.__cdpRecordingPaused)
382
+ return;
383
+ if (globalThis.__cdpCommentModalOpen)
384
+ return;
385
+ if (isOurUI(e.target))
386
+ return;
387
+ const event = {
388
+ type,
389
+ x: e.clientX,
390
+ y: e.clientY,
391
+ timestamp: Date.now(),
392
+ button: e.button,
393
+ buttons: e.buttons,
394
+ };
395
+ if (type === 'wheel') {
396
+ event.deltaX = e.deltaX;
397
+ event.deltaY = e.deltaY;
398
+ }
399
+ if (type === 'click' || type === 'dblclick') {
400
+ event.clickCount = type === 'dblclick' ? 2 : 1;
401
+ }
402
+ event.elementInfo = getElementInfo(e.target);
403
+ globalThis.__cdpRecordingEvents.push(event);
404
+ updateOverlay(event, type);
405
+ };
406
+ // Capture keyboard event
407
+ const captureKeyEvent = (e, type) => {
408
+ if (globalThis.__cdpRecordingPaused)
409
+ return;
410
+ if (globalThis.__cdpCommentModalOpen)
411
+ return;
412
+ // Skip modifier-only keys (they'll be captured with the actual key press)
413
+ if (['Shift', 'Control', 'Alt', 'Meta'].includes(e.key))
414
+ return;
415
+ // Skip the comment shortcut (Ctrl/Cmd + Shift + C)
416
+ if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key.toLowerCase() === 'c')
417
+ return;
418
+ const target = e.target;
419
+ const event = {
420
+ type,
421
+ key: e.key,
422
+ code: e.code,
423
+ timestamp: Date.now(),
424
+ repeat: e.repeat || undefined,
425
+ };
426
+ if (e.ctrlKey || e.altKey || e.shiftKey || e.metaKey) {
427
+ event.modifiers = {
428
+ ctrl: e.ctrlKey || undefined,
429
+ alt: e.altKey || undefined,
430
+ shift: e.shiftKey || undefined,
431
+ meta: e.metaKey || undefined,
432
+ };
433
+ }
434
+ const tag = target?.tagName?.toLowerCase();
435
+ if (target && tag) {
436
+ event.targetInfo = {
437
+ tag,
438
+ id: target.id || undefined,
439
+ isInput: ['input', 'textarea', 'select'].includes(tag) || target.isContentEditable,
440
+ selector: buildSelector(target),
441
+ };
442
+ }
443
+ globalThis.__cdpRecordingEvents.push(event);
444
+ updateOverlay(event, type);
445
+ };
446
+ // Capture paste event
447
+ const capturePasteEvent = (e) => {
448
+ if (globalThis.__cdpRecordingPaused)
449
+ return;
450
+ if (globalThis.__cdpCommentModalOpen)
451
+ return;
452
+ if (isOurUI(e.target))
453
+ return;
454
+ let pastedText = e.clipboardData?.getData('text');
455
+ if (!pastedText)
456
+ return; // Only capture text paste events
457
+ const target = e.target;
458
+ const tag = target?.tagName?.toLowerCase();
459
+ // Normalize text based on target element type
460
+ // Single-line inputs can't accept newlines - the browser strips them
461
+ // So we store what will actually end up in the field
462
+ if (tag === 'input') {
463
+ // Replace newlines with empty string for single-line inputs
464
+ pastedText = pastedText.replace(/[\r\n]/g, '');
465
+ }
466
+ const event = {
467
+ type: 'paste',
468
+ text: pastedText,
469
+ timestamp: Date.now(),
470
+ };
471
+ if (target && tag) {
472
+ event.targetInfo = {
473
+ tag,
474
+ id: target.id || undefined,
475
+ isInput: ['input', 'textarea', 'select'].includes(tag) || target.isContentEditable,
476
+ selector: buildSelector(target),
477
+ };
478
+ }
479
+ // Remove the preceding Cmd/Ctrl+V keydown events since we're capturing the paste content directly
480
+ const events = globalThis.__cdpRecordingEvents;
481
+ // Look backwards for recent keydown events that are part of the paste shortcut
482
+ // (within last 500ms to account for any delay between keydown and paste event)
483
+ const now = Date.now();
484
+ while (events.length > 0) {
485
+ const lastEvent = events[events.length - 1];
486
+ const timeDiff = now - lastEvent.timestamp;
487
+ const isRecentKeydown = lastEvent.type === 'keydown' && timeDiff < 500;
488
+ const isPasteKey = (lastEvent.key === 'v' || lastEvent.key === 'V') &&
489
+ (lastEvent.modifiers?.meta || lastEvent.modifiers?.ctrl);
490
+ if (isRecentKeydown && isPasteKey) {
491
+ events.pop(); // Remove the 'v' keydown
492
+ }
493
+ else {
494
+ break;
495
+ }
496
+ }
497
+ events.push(event);
498
+ updateOverlay(event, 'paste');
499
+ };
500
+ // Overlay
501
+ let overlay = null;
502
+ const updateOverlay = (event, type) => {
503
+ if (!overlay || globalThis.__cdpRecordingState === 'completed')
504
+ return;
505
+ const events = globalThis.__cdpRecordingEvents;
506
+ const duration = ((Date.now() - globalThis.__cdpRecordingStart) / 1000).toFixed(1);
507
+ const isPaused = globalThis.__cdpRecordingPaused;
508
+ // Use stored references (set after DOM creation)
509
+ const statsEl = globalThis.__cdpStatsEl;
510
+ const eventEl = globalThis.__cdpEventEl;
511
+ const statusEl = globalThis.__cdpStatusEl;
512
+ if (statsEl)
513
+ statsEl.textContent = `${events.length} | ${duration}s`;
514
+ if (statusEl) {
515
+ statusEl.textContent = isPaused ? 'PAUSED' : 'REC';
516
+ statusEl.style.background = isPaused ? '#eab308' : '#ef4444';
517
+ }
518
+ if (eventEl) {
519
+ const isKey = type === 'keydown' || type === 'keyup';
520
+ const isComment = type === 'comment';
521
+ const isPaste = type === 'paste';
522
+ eventEl.textContent = isComment ? 'NOTE' : isPaste ? 'PASTE' : isKey ? event.key : type.replace('mouse', '').toUpperCase();
523
+ eventEl.style.background = isComment ? '#f59e0b' :
524
+ isPaste ? '#9333ea' :
525
+ type === 'click' ? '#ef4444' :
526
+ type === 'wheel' ? '#3b82f6' :
527
+ isKey ? '#10b981' :
528
+ type.includes('mouse') ? '#8b5cf6' : '#6b7280';
529
+ }
530
+ };
531
+ if (showOverlayParam) {
532
+ overlay = doc.createElement('div');
533
+ overlay.id = '__cdp-recording-overlay';
534
+ overlay.style.cssText = 'opacity: 1 !important;';
535
+ // Build overlay using DOM methods (Trusted Types compatible)
536
+ const panel = doc.createElement('div');
537
+ panel.className = 'cdp-panel';
538
+ panel.style.cssText = `
539
+ position: fixed !important;
540
+ bottom: 10px !important;
541
+ left: 50% !important;
542
+ transform: translateX(-50%) !important;
543
+ background: rgba(0, 0, 0, 0.9) !important;
544
+ color: white !important;
545
+ padding: 6px 12px !important;
546
+ border-radius: 20px !important;
547
+ font-family: -apple-system, system-ui, sans-serif !important;
548
+ font-size: 12px !important;
549
+ z-index: 2147483647 !important;
550
+ display: flex !important;
551
+ align-items: center !important;
552
+ gap: 10px !important;
553
+ box-shadow: 0 2px 8px rgba(0,0,0,0.3) !important;
554
+ backdrop-filter: blur(4px) !important;
555
+ opacity: 1 !important;
556
+ `;
557
+ const statusEl = doc.createElement('span');
558
+ statusEl.className = 'cdp-status';
559
+ statusEl.textContent = 'REC';
560
+ Object.assign(statusEl.style, {
561
+ padding: '2px 6px',
562
+ borderRadius: '4px',
563
+ fontSize: '10px',
564
+ fontWeight: '600',
565
+ background: '#ef4444',
566
+ letterSpacing: '0.5px'
567
+ });
568
+ const statsEl = doc.createElement('span');
569
+ statsEl.className = 'cdp-stats';
570
+ statsEl.textContent = '0 | 0.0s';
571
+ Object.assign(statsEl.style, { color: '#d1d5db', minWidth: '60px' });
572
+ const eventEl = doc.createElement('span');
573
+ eventEl.className = 'cdp-event';
574
+ eventEl.textContent = '-';
575
+ Object.assign(eventEl.style, {
576
+ padding: '2px 8px',
577
+ borderRadius: '4px',
578
+ fontSize: '10px',
579
+ background: '#6b7280',
580
+ minWidth: '50px',
581
+ textAlign: 'center'
582
+ });
583
+ const btnContainer = doc.createElement('div');
584
+ Object.assign(btnContainer.style, { display: 'flex', gap: '4px', marginLeft: '6px' });
585
+ const btnStyle = {
586
+ background: '#374151',
587
+ border: 'none',
588
+ color: 'white',
589
+ width: '24px',
590
+ height: '24px',
591
+ borderRadius: '4px',
592
+ cursor: 'pointer',
593
+ display: 'flex',
594
+ alignItems: 'center',
595
+ justifyContent: 'center',
596
+ fontSize: '10px'
597
+ };
598
+ const commentBtn = doc.createElement('button');
599
+ commentBtn.className = 'cdp-btn cdp-comment';
600
+ commentBtn.title = 'Add Comment (Cmd+Shift+C) | Bug (Cmd+Shift+B) | Feature (Cmd+Shift+F)';
601
+ commentBtn.textContent = '💬';
602
+ Object.assign(commentBtn.style, btnStyle);
603
+ const pauseBtn = doc.createElement('button');
604
+ pauseBtn.className = 'cdp-btn cdp-pause';
605
+ pauseBtn.title = 'Pause/Resume';
606
+ pauseBtn.textContent = '⏸';
607
+ Object.assign(pauseBtn.style, btnStyle);
608
+ const resetBtn = doc.createElement('button');
609
+ resetBtn.className = 'cdp-btn cdp-reset';
610
+ resetBtn.title = 'Reset';
611
+ resetBtn.textContent = '↺';
612
+ Object.assign(resetBtn.style, btnStyle);
613
+ const doneBtn = doc.createElement('button');
614
+ doneBtn.className = 'cdp-btn cdp-done';
615
+ doneBtn.title = 'Complete';
616
+ doneBtn.textContent = '✓';
617
+ Object.assign(doneBtn.style, { ...btnStyle, background: '#059669' });
618
+ const cancelBtn = doc.createElement('button');
619
+ cancelBtn.className = 'cdp-btn cdp-cancel';
620
+ cancelBtn.title = 'Cancel (discard recording)';
621
+ cancelBtn.textContent = '✕';
622
+ Object.assign(cancelBtn.style, { ...btnStyle, background: '#dc2626' });
623
+ btnContainer.appendChild(commentBtn);
624
+ btnContainer.appendChild(pauseBtn);
625
+ btnContainer.appendChild(resetBtn);
626
+ btnContainer.appendChild(doneBtn);
627
+ btnContainer.appendChild(cancelBtn);
628
+ panel.appendChild(statusEl);
629
+ panel.appendChild(statsEl);
630
+ panel.appendChild(eventEl);
631
+ panel.appendChild(btnContainer);
632
+ // Comment modal
633
+ const commentModal = doc.createElement('div');
634
+ commentModal.className = 'cdp-comment-modal';
635
+ commentModal.style.cssText = `
636
+ display: none !important;
637
+ position: fixed !important;
638
+ top: 0 !important;
639
+ left: 0 !important;
640
+ width: 100% !important;
641
+ height: 100% !important;
642
+ background: rgba(0, 0, 0, 0.9) !important;
643
+ z-index: 2147483648 !important;
644
+ justify-content: center !important;
645
+ align-items: center !important;
646
+ font-family: -apple-system, system-ui, sans-serif !important;
647
+ opacity: 1 !important;
648
+ `;
649
+ const modalCard = doc.createElement('div');
650
+ modalCard.style.cssText = `
651
+ background: #1f2937 !important;
652
+ border-radius: 12px !important;
653
+ padding: 24px !important;
654
+ width: 90% !important;
655
+ max-width: 500px !important;
656
+ box-shadow: 0 25px 50px rgba(0,0,0,0.5) !important;
657
+ opacity: 1 !important;
658
+ `;
659
+ const modalTitle = doc.createElement('h3');
660
+ modalTitle.textContent = 'Add Comment';
661
+ Object.assign(modalTitle.style, { margin: '0 0 16px 0', color: 'white', fontSize: '18px' });
662
+ // Category buttons
663
+ const categoryContainer = doc.createElement('div');
664
+ categoryContainer.className = 'cdp-comment-category';
665
+ Object.assign(categoryContainer.style, { display: 'flex', gap: '0', marginBottom: '16px' });
666
+ const categories = [
667
+ { value: 'narrative', label: 'NARRATIVE', borderRadius: '6px 0 0 6px' },
668
+ { value: 'bug', label: 'BUG', borderRadius: '0' },
669
+ { value: 'feature', label: 'FEATURE', borderRadius: '0 6px 6px 0' }
670
+ ];
671
+ const categoryRadios = [];
672
+ const categoryBtnsArr = [];
673
+ categories.forEach((cat, i) => {
674
+ const label = doc.createElement('label');
675
+ Object.assign(label.style, { flex: '1', cursor: 'pointer' });
676
+ const radio = doc.createElement('input');
677
+ radio.type = 'radio';
678
+ radio.name = 'cdp-category';
679
+ radio.value = cat.value;
680
+ if (i === 0)
681
+ radio.checked = true;
682
+ radio.style.display = 'none';
683
+ categoryRadios.push(radio);
684
+ const span = doc.createElement('span');
685
+ span.className = 'cdp-cat-btn';
686
+ span.dataset.cat = cat.value;
687
+ span.textContent = cat.label;
688
+ Object.assign(span.style, {
689
+ display: 'block',
690
+ textAlign: 'center',
691
+ padding: '8px 12px',
692
+ background: i === 0 ? '#3b82f6' : '#374151',
693
+ color: i === 0 ? 'white' : '#9ca3af',
694
+ fontSize: '13px',
695
+ fontWeight: '500',
696
+ borderRadius: cat.borderRadius,
697
+ border: i === 0 ? '1px solid #3b82f6' : '1px solid #4b5563'
698
+ });
699
+ categoryBtnsArr.push(span);
700
+ label.appendChild(radio);
701
+ label.appendChild(span);
702
+ categoryContainer.appendChild(label);
703
+ });
704
+ const commentInput = doc.createElement('textarea');
705
+ commentInput.className = 'cdp-comment-input';
706
+ commentInput.placeholder = "e.g., 'Expected the form to show a success message'";
707
+ Object.assign(commentInput.style, {
708
+ width: '100%',
709
+ height: '120px',
710
+ background: '#374151',
711
+ border: '1px solid #4b5563',
712
+ borderRadius: '8px',
713
+ color: 'white',
714
+ padding: '12px',
715
+ fontSize: '14px',
716
+ resize: 'vertical',
717
+ boxSizing: 'border-box'
718
+ });
719
+ const modalBtnContainer = doc.createElement('div');
720
+ Object.assign(modalBtnContainer.style, { display: 'flex', gap: '12px', marginTop: '16px', justifyContent: 'flex-end' });
721
+ const commentCancel = doc.createElement('button');
722
+ commentCancel.className = 'cdp-comment-cancel';
723
+ commentCancel.textContent = 'Cancel';
724
+ Object.assign(commentCancel.style, {
725
+ background: '#374151',
726
+ border: 'none',
727
+ color: 'white',
728
+ padding: '8px 16px',
729
+ borderRadius: '6px',
730
+ cursor: 'pointer',
731
+ fontSize: '14px'
732
+ });
733
+ const commentSave = doc.createElement('button');
734
+ commentSave.className = 'cdp-comment-save';
735
+ commentSave.textContent = 'Save Comment';
736
+ Object.assign(commentSave.style, {
737
+ background: '#3b82f6',
738
+ border: 'none',
739
+ color: 'white',
740
+ padding: '8px 16px',
741
+ borderRadius: '6px',
742
+ cursor: 'pointer',
743
+ fontSize: '14px'
744
+ });
745
+ modalBtnContainer.appendChild(commentCancel);
746
+ modalBtnContainer.appendChild(commentSave);
747
+ modalCard.appendChild(modalTitle);
748
+ modalCard.appendChild(categoryContainer);
749
+ modalCard.appendChild(commentInput);
750
+ modalCard.appendChild(modalBtnContainer);
751
+ commentModal.appendChild(modalCard);
752
+ overlay.appendChild(panel);
753
+ overlay.appendChild(commentModal);
754
+ doc.body.appendChild(overlay);
755
+ // Store references for updateOverlay
756
+ globalThis.__cdpStatusEl = statusEl;
757
+ globalThis.__cdpStatsEl = statsEl;
758
+ globalThis.__cdpEventEl = eventEl;
759
+ const categoryBtns = categoryBtnsArr;
760
+ // Category button styling
761
+ const updateCategoryStyles = () => {
762
+ categoryBtns.forEach((btn) => {
763
+ const cat = btn.dataset.cat;
764
+ const radio = overlay.querySelector(`input[value="${cat}"]`);
765
+ if (radio?.checked) {
766
+ btn.style.background = cat === 'bug' ? '#dc2626' : cat === 'feature' ? '#059669' : '#3b82f6';
767
+ btn.style.color = 'white';
768
+ btn.style.borderColor = btn.style.background;
769
+ }
770
+ else {
771
+ btn.style.background = '#374151';
772
+ btn.style.color = '#9ca3af';
773
+ btn.style.borderColor = '#4b5563';
774
+ }
775
+ });
776
+ };
777
+ categoryRadios.forEach((radio) => {
778
+ radio.addEventListener('change', updateCategoryStyles);
779
+ });
780
+ // Comment modal functions
781
+ const showCommentModal = (category = 'narrative') => {
782
+ if (commentModal) {
783
+ globalThis.__cdpCommentModalOpen = true;
784
+ globalThis.__cdpCommentModalOpenedAt = Date.now();
785
+ commentModal.style.display = 'flex';
786
+ // Set the specified category
787
+ const radio = overlay.querySelector(`input[value="${category}"]`);
788
+ if (radio)
789
+ radio.checked = true;
790
+ updateCategoryStyles();
791
+ if (commentInput) {
792
+ commentInput.value = '';
793
+ commentInput.focus();
794
+ }
795
+ }
796
+ };
797
+ const hideCommentModal = () => {
798
+ if (commentModal) {
799
+ // Track pause periods to adjust event timestamps later
800
+ const openedAt = globalThis.__cdpCommentModalOpenedAt;
801
+ if (openedAt) {
802
+ const closedAt = Date.now();
803
+ const pausePeriods = globalThis.__cdpPausePeriods || [];
804
+ pausePeriods.push({ start: openedAt, end: closedAt });
805
+ globalThis.__cdpPausePeriods = pausePeriods;
806
+ delete globalThis.__cdpCommentModalOpenedAt;
807
+ }
808
+ globalThis.__cdpCommentModalOpen = false;
809
+ commentModal.style.display = 'none';
810
+ }
811
+ };
812
+ const saveComment = () => {
813
+ const text = commentInput?.value?.trim();
814
+ if (text) {
815
+ const events = globalThis.__cdpRecordingEvents;
816
+ // Use the timestamp when modal was opened, not current time
817
+ // This way the comment appears at the right point in the timeline
818
+ const modalOpenedAt = globalThis.__cdpCommentModalOpenedAt || Date.now();
819
+ // Get selected category
820
+ const selectedRadio = overlay.querySelector('input[name="cdp-category"]:checked');
821
+ const category = selectedRadio?.value || 'narrative';
822
+ const commentEvent = {
823
+ type: 'comment',
824
+ text,
825
+ timestamp: modalOpenedAt,
826
+ category,
827
+ attachedToEventIndex: events.length > 0 ? events.length - 1 : undefined,
828
+ };
829
+ events.push(commentEvent);
830
+ updateOverlay(commentEvent, 'comment');
831
+ }
832
+ hideCommentModal();
833
+ };
834
+ commentBtn?.addEventListener('click', (e) => {
835
+ e.stopPropagation();
836
+ showCommentModal();
837
+ });
838
+ commentCancel?.addEventListener('click', (e) => {
839
+ e.stopPropagation();
840
+ hideCommentModal();
841
+ });
842
+ commentSave?.addEventListener('click', (e) => {
843
+ e.stopPropagation();
844
+ saveComment();
845
+ });
846
+ // Handle all keyboard events in comment input - stop them from reaching the app
847
+ commentInput?.addEventListener('keydown', (e) => {
848
+ e.stopPropagation();
849
+ if (e.key === 'Escape') {
850
+ e.preventDefault();
851
+ hideCommentModal();
852
+ }
853
+ else if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
854
+ e.preventDefault();
855
+ saveComment();
856
+ }
857
+ });
858
+ commentInput?.addEventListener('keyup', (e) => {
859
+ e.stopPropagation();
860
+ });
861
+ commentInput?.addEventListener('keypress', (e) => {
862
+ e.stopPropagation();
863
+ });
864
+ // Global keyboard shortcuts for comments (Ctrl/Cmd + Shift + C/B/F)
865
+ const commentShortcutHandler = (e) => {
866
+ if (globalThis.__cdpRecordingState === 'completed')
867
+ return;
868
+ if (!(e.ctrlKey || e.metaKey) || !e.shiftKey)
869
+ return;
870
+ const key = e.key.toLowerCase();
871
+ let category = null;
872
+ if (key === 'c')
873
+ category = 'narrative';
874
+ else if (key === 'b')
875
+ category = 'bug';
876
+ else if (key === 'f')
877
+ category = 'feature';
878
+ if (category) {
879
+ e.preventDefault();
880
+ e.stopPropagation();
881
+ showCommentModal(category);
882
+ }
883
+ };
884
+ doc.addEventListener('keydown', commentShortcutHandler, { capture: true });
885
+ globalThis.__cdpCommentShortcutHandler = commentShortcutHandler;
886
+ pauseBtn?.addEventListener('click', (e) => {
887
+ e.stopPropagation();
888
+ const isPaused = !globalThis.__cdpRecordingPaused;
889
+ globalThis.__cdpRecordingPaused = isPaused;
890
+ pauseBtn.textContent = isPaused ? '▶' : '⏸';
891
+ const statusEl = overlay.querySelector('.cdp-status');
892
+ if (statusEl) {
893
+ statusEl.textContent = isPaused ? 'PAUSED' : 'REC';
894
+ statusEl.style.background = isPaused ? '#eab308' : '#ef4444';
895
+ }
896
+ });
897
+ resetBtn?.addEventListener('click', (e) => {
898
+ e.stopPropagation();
899
+ globalThis.__cdpRecordingEvents = [];
900
+ globalThis.__cdpRecordingStart = Date.now();
901
+ const statsEl = overlay.querySelector('.cdp-stats');
902
+ if (statsEl)
903
+ statsEl.textContent = '0 | 0.0s';
904
+ });
905
+ doneBtn?.addEventListener('click', (e) => {
906
+ e.stopPropagation();
907
+ globalThis.__cdpRecordingState = 'completed';
908
+ // Send events to server via CDP binding
909
+ const events = globalThis.__cdpRecordingEvents || [];
910
+ const pausePeriods = globalThis.__cdpPausePeriods || [];
911
+ try {
912
+ globalThis.__cdpRecordingComplete(JSON.stringify({ events, pausePeriods }));
913
+ }
914
+ catch (err) {
915
+ console.error('[cdp-tools] Failed to send recording to server:', err);
916
+ }
917
+ const panel = overlay.querySelector('.cdp-panel');
918
+ if (panel) {
919
+ panel.style.background = 'rgba(5, 150, 105, 0.9)';
920
+ panel.textContent = '';
921
+ const span = doc.createElement('span');
922
+ span.style.padding = '4px 8px';
923
+ span.textContent = 'Saved';
924
+ panel.appendChild(span);
925
+ // Remove overlay after brief delay
926
+ setTimeout(() => {
927
+ overlay.remove();
928
+ }, 800);
929
+ }
930
+ });
931
+ cancelBtn?.addEventListener('click', (e) => {
932
+ e.stopPropagation();
933
+ globalThis.__cdpRecordingState = 'cancelled';
934
+ // Send cancel signal to server via CDP binding
935
+ // Note: CDP bindings require exactly one string argument
936
+ try {
937
+ globalThis.__cdpRecordingCancel('cancel');
938
+ }
939
+ catch (err) {
940
+ console.error('[cdp-tools] Failed to send cancel to server:', err);
941
+ }
942
+ const panel = overlay.querySelector('.cdp-panel');
943
+ if (panel) {
944
+ panel.style.background = 'rgba(220, 38, 38, 0.9)';
945
+ panel.textContent = '';
946
+ const span = doc.createElement('span');
947
+ span.style.padding = '4px 8px';
948
+ span.textContent = 'Cancelled';
949
+ panel.appendChild(span);
950
+ // Remove overlay after brief delay
951
+ setTimeout(() => {
952
+ overlay.remove();
953
+ }, 800);
954
+ }
955
+ });
956
+ globalThis.__cdpOverlay = overlay;
957
+ }
958
+ // Store listener references
959
+ globalThis.__cdpRecordingListeners = {
960
+ mousemove: (e) => captureMouseEvent(e, 'mousemove'),
961
+ mousedown: (e) => captureMouseEvent(e, 'mousedown'),
962
+ mouseup: (e) => captureMouseEvent(e, 'mouseup'),
963
+ wheel: (e) => captureMouseEvent(e, 'wheel'),
964
+ click: (e) => captureMouseEvent(e, 'click'),
965
+ dblclick: (e) => captureMouseEvent(e, 'dblclick'),
966
+ keydown: (e) => captureKeyEvent(e, 'keydown'),
967
+ keyup: (e) => captureKeyEvent(e, 'keyup'),
968
+ paste: (e) => capturePasteEvent(e),
969
+ };
970
+ const listeners = globalThis.__cdpRecordingListeners;
971
+ doc.addEventListener('mousemove', listeners.mousemove, { capture: true, passive: true });
972
+ doc.addEventListener('mousedown', listeners.mousedown, { capture: true, passive: true });
973
+ doc.addEventListener('mouseup', listeners.mouseup, { capture: true, passive: true });
974
+ doc.addEventListener('wheel', listeners.wheel, { capture: true, passive: true });
975
+ doc.addEventListener('click', listeners.click, { capture: true, passive: true });
976
+ doc.addEventListener('dblclick', listeners.dblclick, { capture: true, passive: true });
977
+ doc.addEventListener('keydown', listeners.keydown, { capture: true, passive: true });
978
+ doc.addEventListener('keyup', listeners.keyup, { capture: true, passive: true });
979
+ doc.addEventListener('paste', listeners.paste, { capture: true });
980
+ }, showOverlayOption);
981
+ };
982
+ // Track if we've done initial injection (to distinguish from navigation reloads)
983
+ let initialInjectionDone = false;
984
+ // Re-inject recording overlay and listeners after page load (handles refresh/navigation)
985
+ client.on('Page.loadEventFired', async () => {
986
+ // Skip the initial load event since we inject manually below
987
+ if (!initialInjectionDone) {
988
+ debugLog('recording', 'Skipping initial Page.loadEventFired');
989
+ return;
990
+ }
991
+ debugLog('recording', 'Page.loadEventFired - re-registering bindings and re-injecting script');
992
+ // Small delay to ensure page is ready
993
+ await new Promise(resolve => setTimeout(resolve, 100));
994
+ try {
995
+ // Re-register bindings (they don't persist across navigations)
996
+ await client.send('Runtime.addBinding', { name: '__cdpRecordingComplete' });
997
+ await client.send('Runtime.addBinding', { name: '__cdpRecordingCancel' });
998
+ await client.send('Runtime.addBinding', { name: '__cdpRecordingSaveEvents' });
999
+ debugLog('recording', 'Bindings re-registered');
1000
+ await injectRecordingScript();
1001
+ debugLog('recording', 'Recording script re-injected successfully');
1002
+ }
1003
+ catch (e) {
1004
+ // Page may have been closed or navigated away
1005
+ debugLog('recording', `Failed to re-inject recording script: ${e}`);
1006
+ }
1007
+ });
1008
+ // Initial injection
1009
+ await injectRecordingScript();
1010
+ initialInjectionDone = true;
1011
+ // Store cleanup function
1012
+ cleanupHandles.set(connectionReference, async () => {
1013
+ try {
1014
+ await client.detach();
1015
+ }
1016
+ catch (e) { /* ignore */ }
1017
+ try {
1018
+ await page.evaluate(() => {
1019
+ const listeners = globalThis.__cdpRecordingListeners;
1020
+ const doc = globalThis.document;
1021
+ if (listeners) {
1022
+ doc.removeEventListener('mousemove', listeners.mousemove, { capture: true });
1023
+ doc.removeEventListener('mousedown', listeners.mousedown, { capture: true });
1024
+ doc.removeEventListener('mouseup', listeners.mouseup, { capture: true });
1025
+ doc.removeEventListener('wheel', listeners.wheel, { capture: true });
1026
+ doc.removeEventListener('click', listeners.click, { capture: true });
1027
+ doc.removeEventListener('dblclick', listeners.dblclick, { capture: true });
1028
+ doc.removeEventListener('keydown', listeners.keydown, { capture: true });
1029
+ doc.removeEventListener('keyup', listeners.keyup, { capture: true });
1030
+ doc.removeEventListener('paste', listeners.paste, { capture: true });
1031
+ }
1032
+ const overlay = doc.getElementById('__cdp-recording-overlay');
1033
+ if (overlay)
1034
+ overlay.remove();
1035
+ delete globalThis.__cdpRecordingEvents;
1036
+ delete globalThis.__cdpRecordingListeners;
1037
+ delete globalThis.__cdpRecordingStart;
1038
+ delete globalThis.__cdpRecordingPaused;
1039
+ delete globalThis.__cdpRecordingState;
1040
+ delete globalThis.__cdpOverlay;
1041
+ });
1042
+ }
1043
+ catch (e) { /* page may have navigated */ }
1044
+ });
1045
+ // Block until recording completes (via UI button or stopRecording call)
1046
+ return recordingPromise;
1047
+ }
1048
+ catch (error) {
1049
+ activeSessions.delete(connectionReference);
1050
+ return { success: false, error: error.message || String(error) };
1051
+ }
1052
+ }
1053
+ // =============================================================================
1054
+ // Helper Functions
1055
+ // =============================================================================
1056
+ export function calculateSummary(events) {
1057
+ let clicks = 0;
1058
+ let drags = 0;
1059
+ let scrolls = 0;
1060
+ let keyPresses = 0;
1061
+ let navigations = 0;
1062
+ let comments = 0;
1063
+ let selectorsAvailable = 0;
1064
+ let coordinatesOnly = 0;
1065
+ let canvasInteractions = 0;
1066
+ let interactiveElements = 0;
1067
+ let typedCharacters = 0;
1068
+ let inDrag = false;
1069
+ let lastMouseDown = null;
1070
+ for (const event of events) {
1071
+ if (isCommentEvent(event)) {
1072
+ comments++;
1073
+ }
1074
+ else if (isNavigationEvent(event)) {
1075
+ navigations++;
1076
+ }
1077
+ else if (isKeyboardEvent(event)) {
1078
+ if (event.type === 'keydown') {
1079
+ keyPresses++;
1080
+ // Single-character keys are typed text; the rest are navigation and
1081
+ // modifier presses (Tab, Enter, Shift, arrows).
1082
+ if (event.key.length === 1)
1083
+ typedCharacters++;
1084
+ }
1085
+ }
1086
+ else if (isMouseEvent(event)) {
1087
+ if (event.type === 'mousedown') {
1088
+ inDrag = true;
1089
+ lastMouseDown = event;
1090
+ }
1091
+ else if (event.type === 'mouseup' && inDrag && lastMouseDown) {
1092
+ const distance = Math.sqrt(Math.pow(event.x - lastMouseDown.x, 2) + Math.pow(event.y - lastMouseDown.y, 2));
1093
+ if (distance > 5)
1094
+ drags++;
1095
+ inDrag = false;
1096
+ }
1097
+ else if (event.type === 'click') {
1098
+ clicks++;
1099
+ // Coverage is only meaningful for clicks - a keypress goes to whatever
1100
+ // has focus, so it has no selector to fall back from.
1101
+ if (event.elementInfo?.isCanvas) {
1102
+ // A canvas click has no meaningful element to target, so it is
1103
+ // coordinates by necessity rather than by failure to find a
1104
+ // selector. Counted in both, since it is still coordinate-fragile.
1105
+ canvasInteractions++;
1106
+ coordinatesOnly++;
1107
+ }
1108
+ else if (event.elementInfo?.selector) {
1109
+ selectorsAvailable++;
1110
+ if (event.elementInfo.isInteractive)
1111
+ interactiveElements++;
1112
+ }
1113
+ else {
1114
+ coordinatesOnly++;
1115
+ }
1116
+ }
1117
+ else if (event.type === 'wheel') {
1118
+ scrolls++;
1119
+ }
1120
+ }
1121
+ }
1122
+ return {
1123
+ clicks, drags, scrolls, keyPresses, navigations, comments,
1124
+ selectorsAvailable, coordinatesOnly, canvasInteractions,
1125
+ interactiveElements, typedCharacters,
1126
+ };
1127
+ }
1128
+ // =============================================================================
1129
+ // Event Processing
1130
+ // =============================================================================
1131
+ export function simplifyEvents(events, options = {}) {
1132
+ const { distanceThreshold = 10, timeThreshold = 100, scrollTimeThreshold = 150 } = options;
1133
+ const simplified = [];
1134
+ let lastMove = null;
1135
+ let pendingScroll = null;
1136
+ const flushPending = () => {
1137
+ if (lastMove) {
1138
+ simplified.push(lastMove);
1139
+ lastMove = null;
1140
+ }
1141
+ if (pendingScroll) {
1142
+ simplified.push(pendingScroll);
1143
+ pendingScroll = null;
1144
+ }
1145
+ };
1146
+ for (const event of events) {
1147
+ if (isKeyboardEvent(event) || isNavigationEvent(event) || isCommentEvent(event) || isPasteEvent(event)) {
1148
+ flushPending();
1149
+ simplified.push(event);
1150
+ continue;
1151
+ }
1152
+ if (isMouseEvent(event)) {
1153
+ if (event.type === 'wheel') {
1154
+ // Consolidate consecutive wheel events within time threshold
1155
+ if (lastMove) {
1156
+ simplified.push(lastMove);
1157
+ lastMove = null;
1158
+ }
1159
+ if (!pendingScroll) {
1160
+ pendingScroll = { ...event };
1161
+ }
1162
+ else {
1163
+ const timeDiff = event.timestamp - pendingScroll.timestamp;
1164
+ if (timeDiff <= scrollTimeThreshold) {
1165
+ // Accumulate scroll deltas
1166
+ pendingScroll.deltaX = (pendingScroll.deltaX || 0) + (event.deltaX || 0);
1167
+ pendingScroll.deltaY = (pendingScroll.deltaY || 0) + (event.deltaY || 0);
1168
+ pendingScroll.timestamp = event.timestamp; // Update to latest timestamp
1169
+ }
1170
+ else {
1171
+ // Time gap too large, push previous and start new
1172
+ simplified.push(pendingScroll);
1173
+ pendingScroll = { ...event };
1174
+ }
1175
+ }
1176
+ }
1177
+ else if (event.type === 'mousemove') {
1178
+ if (pendingScroll) {
1179
+ simplified.push(pendingScroll);
1180
+ pendingScroll = null;
1181
+ }
1182
+ if (!lastMove) {
1183
+ lastMove = event;
1184
+ }
1185
+ else {
1186
+ const distance = Math.sqrt(Math.pow(event.x - lastMove.x, 2) + Math.pow(event.y - lastMove.y, 2));
1187
+ const timeDiff = event.timestamp - lastMove.timestamp;
1188
+ if (distance >= distanceThreshold || timeDiff >= timeThreshold) {
1189
+ simplified.push(lastMove);
1190
+ lastMove = event;
1191
+ }
1192
+ else {
1193
+ lastMove = event;
1194
+ }
1195
+ }
1196
+ }
1197
+ else {
1198
+ // click, mousedown, mouseup, dblclick
1199
+ flushPending();
1200
+ simplified.push(event);
1201
+ }
1202
+ }
1203
+ }
1204
+ flushPending();
1205
+ return simplified;
1206
+ }
1207
+ export function eventsToCommands(events, options = {}) {
1208
+ const { simplify = true, includeHovers = false, preferCoordinates = false, preferSelectors = false, includeDelays = false, startTime, maxDelayMs = 0 } = options;
1209
+ const processedEvents = simplify ? simplifyEvents(events) : events;
1210
+ const commands = [];
1211
+ const baseTime = startTime || (processedEvents.length > 0 ? processedEvents[0].timestamp : 0);
1212
+ let lastTimestamp = baseTime;
1213
+ // Track the last selector that received typed text (for append mode)
1214
+ let lastTypedSelector = null;
1215
+ const addCommand = (cmd, eventTimestamp) => {
1216
+ if (includeDelays) {
1217
+ let delay = eventTimestamp - lastTimestamp;
1218
+ // Cap delay at maxDelayMs if configured (0 = no limit)
1219
+ if (maxDelayMs > 0 && delay > maxDelayMs) {
1220
+ delay = maxDelayMs;
1221
+ }
1222
+ if (delay > 0) {
1223
+ commands.push({ ...cmd, delay });
1224
+ }
1225
+ else {
1226
+ commands.push(cmd);
1227
+ }
1228
+ lastTimestamp = eventTimestamp;
1229
+ }
1230
+ else {
1231
+ commands.push(cmd);
1232
+ }
1233
+ };
1234
+ let i = 0;
1235
+ while (i < processedEvents.length) {
1236
+ const event = processedEvents[i];
1237
+ // Comment events - attach to the last meaningful command (skip modifier-only keys)
1238
+ if (isCommentEvent(event)) {
1239
+ // Find the last meaningful command (not a modifier-only keypress)
1240
+ for (let j = commands.length - 1; j >= 0; j--) {
1241
+ const cmd = commands[j];
1242
+ // Skip modifier-only keypresses like Meta+Meta, Shift+Meta+Shift
1243
+ if (cmd.tool === 'input' && cmd.params.action === 'press') {
1244
+ const key = cmd.params.key;
1245
+ if (key && /^(Meta|Shift|Control|Alt)(\+(Meta|Shift|Control|Alt))*$/.test(key)) {
1246
+ continue; // Skip this command, look for previous
1247
+ }
1248
+ }
1249
+ // Found a meaningful command - attach comment
1250
+ if (cmd.comment) {
1251
+ cmd.comment += '\n' + event.text;
1252
+ }
1253
+ else {
1254
+ cmd.comment = event.text;
1255
+ }
1256
+ break;
1257
+ }
1258
+ i++;
1259
+ continue;
1260
+ }
1261
+ // Navigation events
1262
+ if (isNavigationEvent(event)) {
1263
+ if (event.type === 'navigation') {
1264
+ addCommand({ tool: 'navigate', params: { action: 'goto', url: event.url } }, event.timestamp);
1265
+ }
1266
+ else if (event.type === 'reload') {
1267
+ addCommand({ tool: 'navigate', params: { action: 'reload' } }, event.timestamp);
1268
+ }
1269
+ i++;
1270
+ continue;
1271
+ }
1272
+ // Paste events - convert to type command with the pasted text
1273
+ if (isPasteEvent(event)) {
1274
+ // Remove any preceding paste-shortcut command (Meta+v or Ctrl+v)
1275
+ // that was captured before this paste event
1276
+ if (commands.length > 0) {
1277
+ const lastCmd = commands[commands.length - 1];
1278
+ if (lastCmd.tool === 'input' && lastCmd.params.action === 'press') {
1279
+ const key = (lastCmd.params.key || '').toLowerCase();
1280
+ if (key === 'meta+v' || key === 'control+v' || key === 'ctrl+v') {
1281
+ commands.pop(); // Remove the paste shortcut command
1282
+ }
1283
+ }
1284
+ }
1285
+ // Include selector if available from the paste target
1286
+ const selector = event.targetInfo?.selector;
1287
+ const typeParams = { action: 'type', text: event.text };
1288
+ if (selector) {
1289
+ typeParams.selector = selector;
1290
+ // Append if we already typed to this selector
1291
+ if (lastTypedSelector === selector) {
1292
+ typeParams.append = true;
1293
+ }
1294
+ lastTypedSelector = selector;
1295
+ }
1296
+ addCommand({ tool: 'input', params: typeParams }, event.timestamp);
1297
+ i++;
1298
+ continue;
1299
+ }
1300
+ // Keyboard events
1301
+ if (isKeyboardEvent(event)) {
1302
+ if (event.type === 'keydown') {
1303
+ if (event.key.length === 1 && !event.modifiers?.ctrl && !event.modifiers?.alt && !event.modifiers?.meta) {
1304
+ let typedText = event.key;
1305
+ // Use selector from first event in the sequence
1306
+ const selector = event.targetInfo?.selector;
1307
+ let j = i + 1;
1308
+ while (j < processedEvents.length) {
1309
+ const next = processedEvents[j];
1310
+ if (isKeyboardEvent(next) && next.type === 'keydown' &&
1311
+ next.key.length === 1 && !next.modifiers?.ctrl && !next.modifiers?.alt && !next.modifiers?.meta) {
1312
+ typedText += next.key;
1313
+ j++;
1314
+ }
1315
+ else if (isKeyboardEvent(next) && next.type === 'keyup') {
1316
+ j++;
1317
+ }
1318
+ else {
1319
+ break;
1320
+ }
1321
+ }
1322
+ if (typedText.length > 1) {
1323
+ const typeParams = { action: 'type', text: typedText };
1324
+ if (selector) {
1325
+ typeParams.selector = selector;
1326
+ // Append if we already typed to this selector
1327
+ if (lastTypedSelector === selector) {
1328
+ typeParams.append = true;
1329
+ }
1330
+ lastTypedSelector = selector;
1331
+ }
1332
+ addCommand({ tool: 'input', params: typeParams }, event.timestamp);
1333
+ i = j;
1334
+ continue;
1335
+ }
1336
+ }
1337
+ const key = event.key;
1338
+ const modifiers = event.modifiers;
1339
+ let keyCombo = key;
1340
+ if (modifiers) {
1341
+ const parts = [];
1342
+ if (modifiers.ctrl)
1343
+ parts.push('Control');
1344
+ if (modifiers.alt)
1345
+ parts.push('Alt');
1346
+ if (modifiers.shift && key.length > 1)
1347
+ parts.push('Shift');
1348
+ if (modifiers.meta)
1349
+ parts.push('Meta');
1350
+ if (parts.length > 0)
1351
+ keyCombo = parts.join('+') + '+' + key;
1352
+ }
1353
+ addCommand({ tool: 'input', params: { action: 'press', key: keyCombo } }, event.timestamp);
1354
+ }
1355
+ i++;
1356
+ continue;
1357
+ }
1358
+ // Mouse events
1359
+ if (!isMouseEvent(event)) {
1360
+ i++;
1361
+ continue;
1362
+ }
1363
+ // Drag detection
1364
+ if (event.type === 'mousedown') {
1365
+ const dragStart = { x: Math.max(0, event.x), y: Math.max(0, event.y) };
1366
+ let dragEnd = dragStart;
1367
+ let j = i + 1;
1368
+ while (j < processedEvents.length && processedEvents[j].type !== 'mouseup') {
1369
+ const nextEvent = processedEvents[j];
1370
+ if (isMouseEvent(nextEvent) && nextEvent.type === 'mousemove') {
1371
+ // Clamp coordinates to valid viewport bounds (negative coords are outside viewport)
1372
+ dragEnd = { x: Math.max(0, nextEvent.x), y: Math.max(0, nextEvent.y) };
1373
+ }
1374
+ j++;
1375
+ }
1376
+ if (j < processedEvents.length && processedEvents[j].type === 'mouseup') {
1377
+ const mouseup = processedEvents[j];
1378
+ if (isMouseEvent(mouseup)) {
1379
+ // Clamp coordinates to valid viewport bounds
1380
+ dragEnd = { x: Math.max(0, mouseup.x), y: Math.max(0, mouseup.y) };
1381
+ }
1382
+ const distance = Math.sqrt(Math.pow(dragEnd.x - dragStart.x, 2) + Math.pow(dragEnd.y - dragStart.y, 2));
1383
+ if (distance > 5) {
1384
+ addCommand({ tool: 'input', params: { action: 'drag', from: dragStart, to: dragEnd } }, event.timestamp);
1385
+ }
1386
+ i = j + 1;
1387
+ continue;
1388
+ }
1389
+ }
1390
+ // Scroll
1391
+ if (event.type === 'wheel' && (event.deltaX || event.deltaY)) {
1392
+ addCommand({
1393
+ tool: 'input',
1394
+ params: { action: 'scroll', x: event.x, y: event.y, deltaX: event.deltaX || 0, deltaY: event.deltaY || 0 },
1395
+ }, event.timestamp);
1396
+ i++;
1397
+ continue;
1398
+ }
1399
+ // Click
1400
+ if (event.type === 'click') {
1401
+ const elementInfo = event.elementInfo;
1402
+ const selector = elementInfo?.selector;
1403
+ const isCanvas = elementInfo?.isCanvas;
1404
+ // Use selector when available, unless preferCoordinates is set or element is canvas.
1405
+ // Selector-based clicks are more reliable as they survive layout changes.
1406
+ // preferSelectors forces the selector whenever one exists (canvas included)
1407
+ // and wins over preferCoordinates when both flags are set.
1408
+ const useSelector = selector && (preferSelectors || (!preferCoordinates && !isCanvas));
1409
+ if (useSelector) {
1410
+ addCommand({ tool: 'input', params: { action: 'click', selector } }, event.timestamp);
1411
+ }
1412
+ else {
1413
+ // Coordinate-based click for canvas/3D elements or when no selector available
1414
+ addCommand({ tool: 'input', params: { action: 'click', x: event.x, y: event.y } }, event.timestamp);
1415
+ }
1416
+ // Reset lastTypedSelector on click - typing after a click should replace, not append
1417
+ lastTypedSelector = null;
1418
+ i++;
1419
+ continue;
1420
+ }
1421
+ // Mousemove (hover)
1422
+ if (event.type === 'mousemove' && includeHovers) {
1423
+ addCommand({ tool: 'input', params: { action: 'mousemove', x: event.x, y: event.y } }, event.timestamp);
1424
+ }
1425
+ i++;
1426
+ }
1427
+ return commands;
1428
+ }
1429
+ /**
1430
+ * Generate a condensed timeline of events, grouping consecutive actions of the same type
1431
+ * but showing actual comment text inline.
1432
+ * Example: "3 clicks → 5 keys → 'testing this feature' → 2 clicks → 'done testing'"
1433
+ */
1434
+ export function generateCondensedTimeline(events) {
1435
+ const simplified = simplifyEvents(events);
1436
+ const parts = [];
1437
+ let currentType = null;
1438
+ let currentCount = 0;
1439
+ const flushCurrent = () => {
1440
+ if (currentType && currentCount > 0) {
1441
+ if (currentCount === 1) {
1442
+ parts.push(`1 ${currentType}`);
1443
+ }
1444
+ else {
1445
+ parts.push(`${currentCount} ${currentType}s`);
1446
+ }
1447
+ }
1448
+ currentType = null;
1449
+ currentCount = 0;
1450
+ };
1451
+ for (const event of simplified) {
1452
+ if (isCommentEvent(event)) {
1453
+ flushCurrent();
1454
+ // Truncate long comments
1455
+ const text = event.text.length > 50 ? event.text.substring(0, 47) + '...' : event.text;
1456
+ const category = event.category || 'narrative';
1457
+ if (category === 'bug') {
1458
+ parts.push(`BUG: "${text}"`);
1459
+ }
1460
+ else if (category === 'feature') {
1461
+ parts.push(`FEATURE: "${text}"`);
1462
+ }
1463
+ else {
1464
+ parts.push(`"${text}"`);
1465
+ }
1466
+ continue;
1467
+ }
1468
+ if (isNavigationEvent(event)) {
1469
+ flushCurrent();
1470
+ parts.push(`nav`);
1471
+ continue;
1472
+ }
1473
+ if (isPasteEvent(event)) {
1474
+ flushCurrent();
1475
+ // Show truncated paste content
1476
+ const text = event.text.length > 30 ? event.text.substring(0, 27) + '...' : event.text;
1477
+ parts.push(`paste: "${text}"`);
1478
+ continue;
1479
+ }
1480
+ let eventType;
1481
+ if (isKeyboardEvent(event)) {
1482
+ if (event.type === 'keyup')
1483
+ continue;
1484
+ eventType = 'key';
1485
+ }
1486
+ else if (isMouseEvent(event)) {
1487
+ if (event.type === 'click') {
1488
+ eventType = 'click';
1489
+ }
1490
+ else if (event.type === 'wheel') {
1491
+ eventType = 'scroll';
1492
+ }
1493
+ else if (event.type === 'mousemove') {
1494
+ continue; // Skip mousemove
1495
+ }
1496
+ else {
1497
+ continue; // Skip mousedown/mouseup
1498
+ }
1499
+ }
1500
+ else {
1501
+ continue;
1502
+ }
1503
+ if (eventType === currentType) {
1504
+ currentCount++;
1505
+ }
1506
+ else {
1507
+ flushCurrent();
1508
+ currentType = eventType;
1509
+ currentCount = 1;
1510
+ }
1511
+ }
1512
+ flushCurrent();
1513
+ return parts.join(' → ');
1514
+ }
1515
+ /**
1516
+ * Show a verification overlay on the page asking the user to confirm
1517
+ * whether an issue is resolved.
1518
+ */
1519
+ export async function showVerificationOverlay(page, issueType, issueTitle, issueId) {
1520
+ const typeLabel = issueType === 'bug' ? 'Bug' : 'Feature';
1521
+ const questionText = issueType === 'bug'
1522
+ ? 'Is this bug fixed to your satisfaction?'
1523
+ : 'Is this feature implemented to your satisfaction?';
1524
+ return await page.evaluate((params) => {
1525
+ return new Promise((resolve) => {
1526
+ const doc = globalThis.document;
1527
+ // Remove any existing overlays (including replay overlay that may still be present)
1528
+ const existing = doc.getElementById('__cdp-verification-overlay');
1529
+ if (existing)
1530
+ existing.remove();
1531
+ const replayOverlay = doc.getElementById('__cdp-replay-overlay');
1532
+ if (replayOverlay)
1533
+ replayOverlay.remove();
1534
+ // Also clean up replay overlay event listeners if present
1535
+ const blocker = globalThis.__cdpReplayBlocker;
1536
+ if (blocker) {
1537
+ blocker.feedbackEvents?.forEach((evt) => doc.removeEventListener(evt, blocker.blockWithFeedback, true));
1538
+ blocker.silentBlockEvents?.forEach((evt) => doc.removeEventListener(evt, blocker.blockSilently, true));
1539
+ delete globalThis.__cdpReplayBlocker;
1540
+ }
1541
+ const style = globalThis.__cdpReplayStyle;
1542
+ if (style) {
1543
+ style.remove();
1544
+ delete globalThis.__cdpReplayStyle;
1545
+ }
1546
+ // Create elements using DOM methods (Trusted Types compatible)
1547
+ const overlay = doc.createElement('div');
1548
+ overlay.id = '__cdp-verification-overlay';
1549
+ overlay.style.cssText = 'opacity: 1 !important;';
1550
+ const backdrop = doc.createElement('div');
1551
+ backdrop.style.cssText = `
1552
+ position: fixed !important;
1553
+ top: 0 !important;
1554
+ left: 0 !important;
1555
+ right: 0 !important;
1556
+ bottom: 0 !important;
1557
+ background: #424242 !important;
1558
+ z-index: 2147483646 !important;
1559
+ display: flex !important;
1560
+ align-items: center !important;
1561
+ justify-content: center !important;
1562
+ font-family: Roboto, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif !important;
1563
+ opacity: 1 !important;
1564
+ `;
1565
+ const card = doc.createElement('div');
1566
+ card.style.cssText = `
1567
+ background: #ffffff !important;
1568
+ border-radius: 4px !important;
1569
+ padding: 24px !important;
1570
+ max-width: 480px !important;
1571
+ width: 90% !important;
1572
+ box-shadow: 0 11px 15px -7px rgba(0,0,0,0.2), 0 24px 38px 3px rgba(0,0,0,0.14), 0 9px 46px 8px rgba(0,0,0,0.12) !important;
1573
+ color: #212121 !important;
1574
+ opacity: 1 !important;
1575
+ `;
1576
+ // Header
1577
+ const header = doc.createElement('div');
1578
+ header.style.cssText = 'margin-bottom: 16px !important; text-align: center !important; opacity: 1 !important;';
1579
+ const typeLabel = doc.createElement('div');
1580
+ typeLabel.textContent = `${params.typeLabel} #${params.issueId}`;
1581
+ typeLabel.style.cssText = `
1582
+ font-size: 12px !important;
1583
+ color: #757575 !important;
1584
+ text-transform: uppercase !important;
1585
+ letter-spacing: 0.5px !important;
1586
+ margin-bottom: 8px !important;
1587
+ opacity: 1 !important;
1588
+ `;
1589
+ const description = doc.createElement('div');
1590
+ description.textContent = params.issueTitle;
1591
+ description.style.cssText = `
1592
+ font-size: 20px !important;
1593
+ font-weight: 500 !important;
1594
+ color: #212121 !important;
1595
+ line-height: 1.4 !important;
1596
+ text-align: center !important;
1597
+ opacity: 1 !important;
1598
+ `;
1599
+ header.appendChild(typeLabel);
1600
+ header.appendChild(description);
1601
+ // Question text (subtle styling)
1602
+ const questionBox = doc.createElement('div');
1603
+ questionBox.textContent = params.questionText;
1604
+ questionBox.style.cssText = `
1605
+ font-size: 14px !important;
1606
+ margin-bottom: 20px !important;
1607
+ padding: 12px 16px !important;
1608
+ background: #fafafa !important;
1609
+ border-radius: 4px !important;
1610
+ color: #757575 !important;
1611
+ opacity: 1 !important;
1612
+ `;
1613
+ // Comment textarea
1614
+ const commentInput = doc.createElement('textarea');
1615
+ commentInput.id = '__cdp-verify-comment';
1616
+ commentInput.placeholder = 'Add a comment (optional)';
1617
+ commentInput.style.cssText = `
1618
+ width: 100% !important;
1619
+ height: 80px !important;
1620
+ padding: 12px !important;
1621
+ border: 1px solid #e0e0e0 !important;
1622
+ border-radius: 4px !important;
1623
+ background: #fafafa !important;
1624
+ color: #212121 !important;
1625
+ font-size: 14px !important;
1626
+ resize: none !important;
1627
+ margin-bottom: 24px !important;
1628
+ box-sizing: border-box !important;
1629
+ outline: none !important;
1630
+ opacity: 1 !important;
1631
+ `;
1632
+ // Button container
1633
+ const buttonContainer = doc.createElement('div');
1634
+ buttonContainer.style.cssText = `
1635
+ display: flex !important;
1636
+ gap: 8px !important;
1637
+ justify-content: flex-end !important;
1638
+ opacity: 1 !important;
1639
+ `;
1640
+ // No button
1641
+ const noBtn = doc.createElement('button');
1642
+ noBtn.id = '__cdp-verify-no';
1643
+ noBtn.textContent = 'Not Fixed';
1644
+ noBtn.style.cssText = `
1645
+ min-width: 120px !important;
1646
+ height: 36px !important;
1647
+ padding: 0 16px !important;
1648
+ border-radius: 4px !important;
1649
+ border: none !important;
1650
+ font-size: 14px !important;
1651
+ font-weight: 500 !important;
1652
+ cursor: pointer !important;
1653
+ text-transform: uppercase !important;
1654
+ letter-spacing: 0.5px !important;
1655
+ background: #f5f5f5 !important;
1656
+ color: #616161 !important;
1657
+ opacity: 1 !important;
1658
+ `;
1659
+ // Yes button
1660
+ const yesBtn = doc.createElement('button');
1661
+ yesBtn.id = '__cdp-verify-yes';
1662
+ yesBtn.textContent = 'Fixed';
1663
+ yesBtn.style.cssText = `
1664
+ min-width: 120px !important;
1665
+ height: 36px !important;
1666
+ padding: 0 16px !important;
1667
+ border-radius: 4px !important;
1668
+ border: none !important;
1669
+ font-size: 14px !important;
1670
+ font-weight: 500 !important;
1671
+ cursor: pointer !important;
1672
+ text-transform: uppercase !important;
1673
+ letter-spacing: 0.5px !important;
1674
+ background: #1976d2 !important;
1675
+ color: #ffffff !important;
1676
+ opacity: 1 !important;
1677
+ `;
1678
+ buttonContainer.appendChild(noBtn);
1679
+ buttonContainer.appendChild(yesBtn);
1680
+ card.appendChild(header);
1681
+ card.appendChild(questionBox);
1682
+ card.appendChild(commentInput);
1683
+ card.appendChild(buttonContainer);
1684
+ backdrop.appendChild(card);
1685
+ overlay.appendChild(backdrop);
1686
+ doc.body.appendChild(overlay);
1687
+ // Block keyboard events from reaching the app
1688
+ const blockKeyboard = (e) => {
1689
+ e.stopPropagation();
1690
+ };
1691
+ doc.addEventListener('keydown', blockKeyboard, true);
1692
+ doc.addEventListener('keyup', blockKeyboard, true);
1693
+ doc.addEventListener('keypress', blockKeyboard, true);
1694
+ const cleanup = () => {
1695
+ doc.removeEventListener('keydown', blockKeyboard, true);
1696
+ doc.removeEventListener('keyup', blockKeyboard, true);
1697
+ doc.removeEventListener('keypress', blockKeyboard, true);
1698
+ overlay.remove();
1699
+ };
1700
+ yesBtn?.addEventListener('click', () => {
1701
+ const comment = commentInput?.value?.trim() || undefined;
1702
+ cleanup();
1703
+ resolve({ resolved: true, comment });
1704
+ });
1705
+ noBtn?.addEventListener('click', () => {
1706
+ const comment = commentInput?.value?.trim() || undefined;
1707
+ cleanup();
1708
+ resolve({ resolved: false, comment });
1709
+ });
1710
+ });
1711
+ }, { typeLabel, issueTitle, issueId, questionText });
1712
+ }
1713
+ /**
1714
+ * Show a "Ready to begin?" overlay before starting a test replay
1715
+ * Uses DOM methods instead of innerHTML to support Trusted Types policies
1716
+ * Returns: 'cancel', 'begin', or 'rerecord'
1717
+ */
1718
+ export async function showTestReadyOverlay(page, issueType, issueTitle, issueId, hasSequence = true) {
1719
+ const typeLabel = issueType === 'bug' ? 'Bug' : 'Feature';
1720
+ return await page.evaluate((params) => {
1721
+ return new Promise((resolve) => {
1722
+ const doc = globalThis.document;
1723
+ // Remove any existing overlay
1724
+ const existing = doc.getElementById('__cdp-test-ready-overlay');
1725
+ if (existing)
1726
+ existing.remove();
1727
+ // Create elements using DOM methods (Trusted Types compatible)
1728
+ const overlay = doc.createElement('div');
1729
+ overlay.id = '__cdp-test-ready-overlay';
1730
+ overlay.style.cssText = 'opacity: 1 !important;';
1731
+ const backdrop = doc.createElement('div');
1732
+ backdrop.style.cssText = `
1733
+ position: fixed !important;
1734
+ top: 0 !important;
1735
+ left: 0 !important;
1736
+ right: 0 !important;
1737
+ bottom: 0 !important;
1738
+ background: #424242 !important;
1739
+ z-index: 2147483646 !important;
1740
+ display: flex !important;
1741
+ align-items: center !important;
1742
+ justify-content: center !important;
1743
+ font-family: Roboto, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif !important;
1744
+ opacity: 1 !important;
1745
+ `;
1746
+ const card = doc.createElement('div');
1747
+ card.style.cssText = `
1748
+ background: #ffffff !important;
1749
+ border-radius: 4px !important;
1750
+ padding: 24px !important;
1751
+ max-width: 480px !important;
1752
+ width: 90% !important;
1753
+ box-shadow: 0 11px 15px -7px rgba(0,0,0,0.2), 0 24px 38px 3px rgba(0,0,0,0.14), 0 9px 46px 8px rgba(0,0,0,0.12) !important;
1754
+ color: #212121 !important;
1755
+ text-align: center !important;
1756
+ opacity: 1 !important;
1757
+ `;
1758
+ const testingLabel = doc.createElement('div');
1759
+ testingLabel.textContent = `Testing ${params.typeLabel} #${params.issueId}`;
1760
+ testingLabel.style.cssText = `
1761
+ font-size: 12px !important;
1762
+ color: #757575 !important;
1763
+ text-transform: uppercase !important;
1764
+ letter-spacing: 0.5px !important;
1765
+ margin-bottom: 16px !important;
1766
+ opacity: 1 !important;
1767
+ `;
1768
+ const description = doc.createElement('div');
1769
+ description.textContent = params.issueTitle;
1770
+ description.style.cssText = `
1771
+ font-size: 18px !important;
1772
+ font-weight: 500 !important;
1773
+ margin-bottom: 20px !important;
1774
+ padding: 16px !important;
1775
+ background: #f5f5f5 !important;
1776
+ border-radius: 4px !important;
1777
+ color: #212121 !important;
1778
+ text-align: center !important;
1779
+ opacity: 1 !important;
1780
+ `;
1781
+ const instructions = doc.createElement('div');
1782
+ instructions.textContent = params.hasSequence
1783
+ ? 'The recorded sequence will replay. Watch for the issue.'
1784
+ : 'Recording will start. Reproduce the issue to verify.';
1785
+ instructions.style.cssText = `
1786
+ font-size: 14px !important;
1787
+ color: #616161 !important;
1788
+ margin-bottom: 24px !important;
1789
+ opacity: 1 !important;
1790
+ `;
1791
+ // Button container
1792
+ const buttonContainer = doc.createElement('div');
1793
+ buttonContainer.style.cssText = `
1794
+ display: flex !important;
1795
+ gap: 8px !important;
1796
+ justify-content: center !important;
1797
+ opacity: 1 !important;
1798
+ `;
1799
+ const buttonBaseCss = `
1800
+ min-width: 100px !important;
1801
+ height: 36px !important;
1802
+ padding: 0 16px !important;
1803
+ border-radius: 4px !important;
1804
+ border: none !important;
1805
+ font-size: 14px !important;
1806
+ font-weight: 500 !important;
1807
+ cursor: pointer !important;
1808
+ text-transform: uppercase !important;
1809
+ letter-spacing: 0.5px !important;
1810
+ opacity: 1 !important;
1811
+ `;
1812
+ const cancelBtn = doc.createElement('button');
1813
+ cancelBtn.textContent = 'CANCEL';
1814
+ cancelBtn.style.cssText = buttonBaseCss + 'background: #f5f5f5 !important; color: #616161 !important;';
1815
+ // Only show re-record button if there's already a sequence
1816
+ let rerecordBtn = null;
1817
+ if (params.hasSequence) {
1818
+ rerecordBtn = doc.createElement('button');
1819
+ rerecordBtn.textContent = 'RE-RECORD';
1820
+ rerecordBtn.style.cssText = buttonBaseCss + 'background: #ff9800 !important; color: #ffffff !important;';
1821
+ }
1822
+ const beginBtn = doc.createElement('button');
1823
+ beginBtn.textContent = 'BEGIN TEST';
1824
+ beginBtn.id = '__cdp-test-begin';
1825
+ beginBtn.style.cssText = buttonBaseCss + 'background: #1976d2 !important; color: #ffffff !important;';
1826
+ buttonContainer.appendChild(cancelBtn);
1827
+ if (rerecordBtn)
1828
+ buttonContainer.appendChild(rerecordBtn);
1829
+ buttonContainer.appendChild(beginBtn);
1830
+ card.appendChild(testingLabel);
1831
+ card.appendChild(description);
1832
+ card.appendChild(instructions);
1833
+ card.appendChild(buttonContainer);
1834
+ backdrop.appendChild(card);
1835
+ overlay.appendChild(backdrop);
1836
+ doc.body.appendChild(overlay);
1837
+ // Block keyboard events from reaching the app
1838
+ const blockKeyboard = (e) => {
1839
+ e.stopPropagation();
1840
+ };
1841
+ doc.addEventListener('keydown', blockKeyboard, true);
1842
+ doc.addEventListener('keyup', blockKeyboard, true);
1843
+ doc.addEventListener('keypress', blockKeyboard, true);
1844
+ const cleanup = () => {
1845
+ doc.removeEventListener('keydown', blockKeyboard, true);
1846
+ doc.removeEventListener('keyup', blockKeyboard, true);
1847
+ doc.removeEventListener('keypress', blockKeyboard, true);
1848
+ overlay.remove();
1849
+ };
1850
+ cancelBtn.addEventListener('click', () => {
1851
+ cleanup();
1852
+ resolve('cancel');
1853
+ });
1854
+ if (rerecordBtn) {
1855
+ rerecordBtn.addEventListener('click', () => {
1856
+ cleanup();
1857
+ resolve('rerecord');
1858
+ });
1859
+ }
1860
+ beginBtn.addEventListener('click', () => {
1861
+ cleanup();
1862
+ resolve('begin');
1863
+ });
1864
+ });
1865
+ }, { typeLabel, issueTitle, issueId, hasSequence });
1866
+ }
1867
+ /**
1868
+ * Show a "Replay in progress" overlay that blocks user interaction
1869
+ * Returns a cleanup function to remove the overlay
1870
+ */
1871
+ export async function showReplayOverlay(page, issueType, issueTitle, issueId) {
1872
+ const typeLabel = issueType === 'bug' ? 'Bug' : 'Feature';
1873
+ await page.evaluate((params) => {
1874
+ const doc = globalThis.document;
1875
+ // Create overlay - transparent with banner at top
1876
+ // pointerEvents: 'none' allows automated clicks to pass through while
1877
+ // event listeners still capture and block manual user interactions
1878
+ const overlay = doc.createElement('div');
1879
+ overlay.id = '__cdp-replay-overlay';
1880
+ Object.assign(overlay.style, {
1881
+ position: 'fixed',
1882
+ top: '0',
1883
+ left: '0',
1884
+ width: '100%',
1885
+ height: '100%',
1886
+ background: 'transparent',
1887
+ zIndex: '2147483647',
1888
+ pointerEvents: 'none',
1889
+ fontFamily: '"Roboto", -apple-system, system-ui, sans-serif'
1890
+ });
1891
+ // Banner at top (not centered)
1892
+ const card = doc.createElement('div');
1893
+ Object.assign(card.style, {
1894
+ position: 'absolute',
1895
+ top: '16px',
1896
+ left: '50%',
1897
+ transform: 'translateX(-50%)',
1898
+ background: 'rgba(33, 33, 33, 0.9)',
1899
+ borderRadius: '8px',
1900
+ padding: '12px 24px',
1901
+ maxWidth: '400px',
1902
+ boxShadow: '0 4px 12px rgba(0,0,0,0.3)',
1903
+ textAlign: 'center',
1904
+ display: 'flex',
1905
+ alignItems: 'center',
1906
+ gap: '12px'
1907
+ });
1908
+ // Spinner (small, inline)
1909
+ const spinner = doc.createElement('div');
1910
+ Object.assign(spinner.style, {
1911
+ width: '20px',
1912
+ height: '20px',
1913
+ border: '2px solid rgba(255,255,255,0.3)',
1914
+ borderTop: '2px solid #4fc3f7',
1915
+ borderRadius: '50%',
1916
+ animation: 'cdp-spin 1s linear infinite',
1917
+ flexShrink: '0'
1918
+ });
1919
+ // Add keyframe animations (spin for spinner, shake for feedback)
1920
+ const style = doc.createElement('style');
1921
+ style.textContent = `
1922
+ @keyframes cdp-spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
1923
+ @keyframes cdp-shake { 0%, 100% { transform: translateX(0); } 20% { transform: translateX(-4px); } 40% { transform: translateX(4px); } 60% { transform: translateX(-4px); } 80% { transform: translateX(4px); } }
1924
+ `;
1925
+ doc.head.appendChild(style);
1926
+ globalThis.__cdpReplayStyle = style;
1927
+ // Instructions/status text
1928
+ const instructions = doc.createElement('div');
1929
+ instructions.textContent = `Replaying ${params.typeLabel} #${params.issueId}...`;
1930
+ Object.assign(instructions.style, {
1931
+ fontSize: '14px',
1932
+ color: '#ffffff',
1933
+ whiteSpace: 'nowrap'
1934
+ });
1935
+ card.appendChild(spinner);
1936
+ card.appendChild(instructions);
1937
+ overlay.appendChild(card);
1938
+ doc.body.appendChild(overlay);
1939
+ // Block all user interactions with escalating feedback
1940
+ let attemptCount = 0;
1941
+ const attitudeMessages = [
1942
+ "I said WAIT.",
1943
+ "Seriously?",
1944
+ "The spinner means BUSY.",
1945
+ "Do you want bugs? Because this is how you get bugs.",
1946
+ "...",
1947
+ ];
1948
+ // Events that trigger shake/attitude (intentional user actions)
1949
+ const feedbackEvents = ['click', 'keydown', 'touchend'];
1950
+ // Events to block silently (prevent default but no feedback)
1951
+ const silentBlockEvents = ['mousedown', 'mouseup', 'keyup', 'keypress', 'touchstart'];
1952
+ const blockWithFeedback = (e) => {
1953
+ // Don't block if CDP replay is in progress (flag set by replay executor)
1954
+ if (globalThis.__cdpReplayClickInProgress)
1955
+ return;
1956
+ e.preventDefault();
1957
+ e.stopPropagation();
1958
+ attemptCount++;
1959
+ console.log('[cdp-tools] Blocked user interaction during replay:', e.type, 'target:', e.target?.tagName);
1960
+ // Shake the instructions text
1961
+ instructions.style.animation = 'none';
1962
+ void instructions.offsetWidth; // Force reflow
1963
+ instructions.style.animation = 'cdp-shake 0.3s ease';
1964
+ // After 3 attempts, show attitude
1965
+ if (attemptCount > 3) {
1966
+ const msgIndex = Math.min(attemptCount - 4, attitudeMessages.length - 1);
1967
+ instructions.textContent = attitudeMessages[msgIndex];
1968
+ instructions.style.color = '#d32f2f';
1969
+ }
1970
+ };
1971
+ const blockSilently = (e) => {
1972
+ // Don't block if CDP replay is in progress (flag set by replay executor)
1973
+ if (globalThis.__cdpReplayClickInProgress)
1974
+ return;
1975
+ e.preventDefault();
1976
+ e.stopPropagation();
1977
+ };
1978
+ feedbackEvents.forEach(evt => doc.addEventListener(evt, blockWithFeedback, true));
1979
+ silentBlockEvents.forEach(evt => doc.addEventListener(evt, blockSilently, true));
1980
+ globalThis.__cdpReplayBlocker = {
1981
+ blockWithFeedback,
1982
+ blockSilently,
1983
+ feedbackEvents,
1984
+ silentBlockEvents
1985
+ };
1986
+ }, { typeLabel, issueTitle, issueId });
1987
+ // Return cleanup function
1988
+ return async () => {
1989
+ await page.evaluate(() => {
1990
+ const doc = globalThis.document;
1991
+ const overlay = doc.getElementById('__cdp-replay-overlay');
1992
+ if (overlay)
1993
+ overlay.remove();
1994
+ const style = globalThis.__cdpReplayStyle;
1995
+ if (style)
1996
+ style.remove();
1997
+ const blocker = globalThis.__cdpReplayBlocker;
1998
+ if (blocker) {
1999
+ blocker.feedbackEvents.forEach((evt) => doc.removeEventListener(evt, blocker.blockWithFeedback, true));
2000
+ blocker.silentBlockEvents.forEach((evt) => doc.removeEventListener(evt, blocker.blockSilently, true));
2001
+ delete globalThis.__cdpReplayBlocker;
2002
+ }
2003
+ delete globalThis.__cdpReplayStyle;
2004
+ }).catch(() => { });
2005
+ };
2006
+ }
2007
+ //# sourceMappingURL=interaction-recorder.js.map