tabwright 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/bin.js +4 -0
- package/dist/a11y-client.js +204 -0
- package/dist/agent-skills/tabwright/SKILL.md +119 -0
- package/dist/aria-snapshot.d.ts +262 -0
- package/dist/aria-snapshot.d.ts.map +1 -0
- package/dist/aria-snapshot.js +1256 -0
- package/dist/aria-snapshot.js.map +1 -0
- package/dist/aria-snapshot.test.d.ts +2 -0
- package/dist/aria-snapshot.test.d.ts.map +1 -0
- package/dist/aria-snapshot.test.js +270 -0
- package/dist/aria-snapshot.test.js.map +1 -0
- package/dist/aria-snapshot.unit.test.d.ts +2 -0
- package/dist/aria-snapshot.unit.test.d.ts.map +1 -0
- package/dist/aria-snapshot.unit.test.js +542 -0
- package/dist/aria-snapshot.unit.test.js.map +1 -0
- package/dist/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.d.ts +5 -0
- package/dist/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.d.ts.map +1 -0
- package/dist/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.js +5 -0
- package/dist/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.js.map +1 -0
- package/dist/bippy.js +894 -0
- package/dist/browser-config.d.ts +13 -0
- package/dist/browser-config.d.ts.map +1 -0
- package/dist/browser-config.js +126 -0
- package/dist/browser-config.js.map +1 -0
- package/dist/browser-install.d.ts +16 -0
- package/dist/browser-install.d.ts.map +1 -0
- package/dist/browser-install.js +238 -0
- package/dist/browser-install.js.map +1 -0
- package/dist/browser-launch.d.ts +17 -0
- package/dist/browser-launch.d.ts.map +1 -0
- package/dist/browser-launch.js +44 -0
- package/dist/browser-launch.js.map +1 -0
- package/dist/builtin-capabilities.d.ts +28 -0
- package/dist/builtin-capabilities.d.ts.map +1 -0
- package/dist/builtin-capabilities.js +2673 -0
- package/dist/builtin-capabilities.js.map +1 -0
- package/dist/capability-agent-skill.d.ts +60 -0
- package/dist/capability-agent-skill.d.ts.map +1 -0
- package/dist/capability-agent-skill.js +225 -0
- package/dist/capability-agent-skill.js.map +1 -0
- package/dist/capability-auth-state.d.ts +34 -0
- package/dist/capability-auth-state.d.ts.map +1 -0
- package/dist/capability-auth-state.js +157 -0
- package/dist/capability-auth-state.js.map +1 -0
- package/dist/capability-auth.d.ts +36 -0
- package/dist/capability-auth.d.ts.map +1 -0
- package/dist/capability-auth.js +198 -0
- package/dist/capability-auth.js.map +1 -0
- package/dist/capability-cli-confirmation.test.d.ts +2 -0
- package/dist/capability-cli-confirmation.test.d.ts.map +1 -0
- package/dist/capability-cli-confirmation.test.js +79 -0
- package/dist/capability-cli-confirmation.test.js.map +1 -0
- package/dist/capability-options.d.ts +20 -0
- package/dist/capability-options.d.ts.map +1 -0
- package/dist/capability-options.js +31 -0
- package/dist/capability-options.js.map +1 -0
- package/dist/capability-options.test.d.ts +2 -0
- package/dist/capability-options.test.d.ts.map +1 -0
- package/dist/capability-options.test.js +81 -0
- package/dist/capability-options.test.js.map +1 -0
- package/dist/capability-package.d.ts +27 -0
- package/dist/capability-package.d.ts.map +1 -0
- package/dist/capability-package.js +444 -0
- package/dist/capability-package.js.map +1 -0
- package/dist/capability-package.test.d.ts +2 -0
- package/dist/capability-package.test.d.ts.map +1 -0
- package/dist/capability-package.test.js +179 -0
- package/dist/capability-package.test.js.map +1 -0
- package/dist/capability-registry.d.ts +262 -0
- package/dist/capability-registry.d.ts.map +1 -0
- package/dist/capability-registry.js +931 -0
- package/dist/capability-registry.js.map +1 -0
- package/dist/capability-registry.test.d.ts +2 -0
- package/dist/capability-registry.test.d.ts.map +1 -0
- package/dist/capability-registry.test.js +1683 -0
- package/dist/capability-registry.test.js.map +1 -0
- package/dist/capability-runner.d.ts +99 -0
- package/dist/capability-runner.d.ts.map +1 -0
- package/dist/capability-runner.js +608 -0
- package/dist/capability-runner.js.map +1 -0
- package/dist/capability-search-relevance.test.d.ts +2 -0
- package/dist/capability-search-relevance.test.d.ts.map +1 -0
- package/dist/capability-search-relevance.test.js +67 -0
- package/dist/capability-search-relevance.test.js.map +1 -0
- package/dist/capability-studio.d.ts +11 -0
- package/dist/capability-studio.d.ts.map +1 -0
- package/dist/capability-studio.js +593 -0
- package/dist/capability-studio.js.map +1 -0
- package/dist/capability-studio.test.d.ts +2 -0
- package/dist/capability-studio.test.d.ts.map +1 -0
- package/dist/capability-studio.test.js +41 -0
- package/dist/capability-studio.test.js.map +1 -0
- package/dist/cdp-log.d.ts +19 -0
- package/dist/cdp-log.d.ts.map +1 -0
- package/dist/cdp-log.js +83 -0
- package/dist/cdp-log.js.map +1 -0
- package/dist/cdp-log.test.d.ts +2 -0
- package/dist/cdp-log.test.d.ts.map +1 -0
- package/dist/cdp-log.test.js +109 -0
- package/dist/cdp-log.test.js.map +1 -0
- package/dist/cdp-relay.d.ts +19 -0
- package/dist/cdp-relay.d.ts.map +1 -0
- package/dist/cdp-relay.js +2375 -0
- package/dist/cdp-relay.js.map +1 -0
- package/dist/cdp-session.d.ts +37 -0
- package/dist/cdp-session.d.ts.map +1 -0
- package/dist/cdp-session.js +36 -0
- package/dist/cdp-session.js.map +1 -0
- package/dist/cdp-types.d.ts +65 -0
- package/dist/cdp-types.d.ts.map +1 -0
- package/dist/cdp-types.js +91 -0
- package/dist/cdp-types.js.map +1 -0
- package/dist/channel-owner-inspect.test.d.ts +2 -0
- package/dist/channel-owner-inspect.test.d.ts.map +1 -0
- package/dist/channel-owner-inspect.test.js +75 -0
- package/dist/channel-owner-inspect.test.js.map +1 -0
- package/dist/chrome-discovery.d.ts +65 -0
- package/dist/chrome-discovery.d.ts.map +1 -0
- package/dist/chrome-discovery.js +173 -0
- package/dist/chrome-discovery.js.map +1 -0
- package/dist/chrome-discovery.test.d.ts +2 -0
- package/dist/chrome-discovery.test.d.ts.map +1 -0
- package/dist/chrome-discovery.test.js +41 -0
- package/dist/chrome-discovery.test.js.map +1 -0
- package/dist/clean-html.d.ts +11 -0
- package/dist/clean-html.d.ts.map +1 -0
- package/dist/clean-html.js +106 -0
- package/dist/clean-html.js.map +1 -0
- package/dist/cli-help.test.d.ts +2 -0
- package/dist/cli-help.test.d.ts.map +1 -0
- package/dist/cli-help.test.js +98 -0
- package/dist/cli-help.test.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +2709 -0
- package/dist/cli.js.map +1 -0
- package/dist/cloud-client.d.ts +56 -0
- package/dist/cloud-client.d.ts.map +1 -0
- package/dist/cloud-client.js +127 -0
- package/dist/cloud-client.js.map +1 -0
- package/dist/create-logger.d.ts +9 -0
- package/dist/create-logger.d.ts.map +1 -0
- package/dist/create-logger.js +27 -0
- package/dist/create-logger.js.map +1 -0
- package/dist/debugger-api.md +458 -0
- package/dist/debugger-examples-types.d.ts +24 -0
- package/dist/debugger-examples-types.d.ts.map +1 -0
- package/dist/debugger-examples-types.js +2 -0
- package/dist/debugger-examples-types.js.map +1 -0
- package/dist/debugger-examples.d.ts +6 -0
- package/dist/debugger-examples.d.ts.map +1 -0
- package/dist/debugger-examples.js +53 -0
- package/dist/debugger-examples.js.map +1 -0
- package/dist/debugger.d.ts +381 -0
- package/dist/debugger.d.ts.map +1 -0
- package/dist/debugger.js +630 -0
- package/dist/debugger.js.map +1 -0
- package/dist/diff-utils.d.ts +20 -0
- package/dist/diff-utils.d.ts.map +1 -0
- package/dist/diff-utils.js +50 -0
- package/dist/diff-utils.js.map +1 -0
- package/dist/diff-utils.test.d.ts +2 -0
- package/dist/diff-utils.test.d.ts.map +1 -0
- package/dist/diff-utils.test.js +137 -0
- package/dist/diff-utils.test.js.map +1 -0
- package/dist/doctor.d.ts +44 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/doctor.js +246 -0
- package/dist/doctor.js.map +1 -0
- package/dist/doctor.test.d.ts +2 -0
- package/dist/doctor.test.d.ts.map +1 -0
- package/dist/doctor.test.js +208 -0
- package/dist/doctor.test.js.map +1 -0
- package/dist/editor-api.md +374 -0
- package/dist/editor-examples.d.ts +11 -0
- package/dist/editor-examples.d.ts.map +1 -0
- package/dist/editor-examples.js +124 -0
- package/dist/editor-examples.js.map +1 -0
- package/dist/editor.d.ts +203 -0
- package/dist/editor.d.ts.map +1 -0
- package/dist/editor.js +363 -0
- package/dist/editor.js.map +1 -0
- package/dist/env-compat.d.ts +2 -0
- package/dist/env-compat.d.ts.map +1 -0
- package/dist/env-compat.js +27 -0
- package/dist/env-compat.js.map +1 -0
- package/dist/executor.d.ts +229 -0
- package/dist/executor.d.ts.map +1 -0
- package/dist/executor.js +1465 -0
- package/dist/executor.js.map +1 -0
- package/dist/executor.unit.test.d.ts +2 -0
- package/dist/executor.unit.test.d.ts.map +1 -0
- package/dist/executor.unit.test.js +143 -0
- package/dist/executor.unit.test.js.map +1 -0
- package/dist/extension/_locales/en/messages.json +278 -0
- package/dist/extension/_locales/zh_CN/messages.json +256 -0
- package/dist/extension/assets/options-CYH96Lcx.css +1349 -0
- package/dist/extension/background.js +3994 -0
- package/dist/extension/icons/SolarCursorSquareBold.png +0 -0
- package/dist/extension/icons/icon-black-128.png +0 -0
- package/dist/extension/icons/icon-black-16.png +0 -0
- package/dist/extension/icons/icon-black-32.png +0 -0
- package/dist/extension/icons/icon-black-48.png +0 -0
- package/dist/extension/icons/icon-gray-128.png +0 -0
- package/dist/extension/icons/icon-gray-16.png +0 -0
- package/dist/extension/icons/icon-gray-32.png +0 -0
- package/dist/extension/icons/icon-gray-48.png +0 -0
- package/dist/extension/icons/icon-green-128.png +0 -0
- package/dist/extension/icons/icon-green-16.png +0 -0
- package/dist/extension/icons/icon-green-32.png +0 -0
- package/dist/extension/icons/icon-green-48.png +0 -0
- package/dist/extension/icons/tabwright-icon-black.png +0 -0
- package/dist/extension/icons/tabwright-icon-gray-disabled.png +0 -0
- package/dist/extension/icons/tabwright-icon-gray-disabled.svg +1 -0
- package/dist/extension/manifest.json +54 -0
- package/dist/extension/options.js +16145 -0
- package/dist/extension/rrweb-recorder.js +12952 -0
- package/dist/extension/src/options.html +165 -0
- package/dist/extension/src/prism-bash.min.js +1 -0
- package/dist/extension/src/prism.min.js +1 -0
- package/dist/extension/src/welcome.html +396 -0
- package/dist/extension-connection.test.d.ts +2 -0
- package/dist/extension-connection.test.d.ts.map +1 -0
- package/dist/extension-connection.test.js +708 -0
- package/dist/extension-connection.test.js.map +1 -0
- package/dist/extension-icon-warning.test.d.ts +2 -0
- package/dist/extension-icon-warning.test.d.ts.map +1 -0
- package/dist/extension-icon-warning.test.js +33 -0
- package/dist/extension-icon-warning.test.js.map +1 -0
- package/dist/ghost-browser.d.ts +62 -0
- package/dist/ghost-browser.d.ts.map +1 -0
- package/dist/ghost-browser.js +107 -0
- package/dist/ghost-browser.js.map +1 -0
- package/dist/ghost-cursor-client.js +374 -0
- package/dist/ghost-cursor-controller.d.ts +46 -0
- package/dist/ghost-cursor-controller.d.ts.map +1 -0
- package/dist/ghost-cursor-controller.js +98 -0
- package/dist/ghost-cursor-controller.js.map +1 -0
- package/dist/ghost-cursor.d.ts +27 -0
- package/dist/ghost-cursor.d.ts.map +1 -0
- package/dist/ghost-cursor.js +79 -0
- package/dist/ghost-cursor.js.map +1 -0
- package/dist/htmlrewrite.d.ts +8 -0
- package/dist/htmlrewrite.d.ts.map +1 -0
- package/dist/htmlrewrite.js +339 -0
- package/dist/htmlrewrite.js.map +1 -0
- package/dist/htmlrewrite.test.d.ts +2 -0
- package/dist/htmlrewrite.test.d.ts.map +1 -0
- package/dist/htmlrewrite.test.js +7975 -0
- package/dist/htmlrewrite.test.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/kill-port.d.ts +29 -0
- package/dist/kill-port.d.ts.map +1 -0
- package/dist/kill-port.js +190 -0
- package/dist/kill-port.js.map +1 -0
- package/dist/kitty-graphics.d.ts +22 -0
- package/dist/kitty-graphics.d.ts.map +1 -0
- package/dist/kitty-graphics.js +71 -0
- package/dist/kitty-graphics.js.map +1 -0
- package/dist/kitty-graphics.test.d.ts +2 -0
- package/dist/kitty-graphics.test.d.ts.map +1 -0
- package/dist/kitty-graphics.test.js +125 -0
- package/dist/kitty-graphics.test.js.map +1 -0
- package/dist/locator-selector.test.d.ts +2 -0
- package/dist/locator-selector.test.d.ts.map +1 -0
- package/dist/locator-selector.test.js +98 -0
- package/dist/locator-selector.test.js.map +1 -0
- package/dist/mcp-client.d.ts +20 -0
- package/dist/mcp-client.d.ts.map +1 -0
- package/dist/mcp-client.js +56 -0
- package/dist/mcp-client.js.map +1 -0
- package/dist/mcp.d.ts +6 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +511 -0
- package/dist/mcp.js.map +1 -0
- package/dist/on-mouse-action.test.d.ts +2 -0
- package/dist/on-mouse-action.test.d.ts.map +1 -0
- package/dist/on-mouse-action.test.js +180 -0
- package/dist/on-mouse-action.test.js.map +1 -0
- package/dist/options-page.test.d.ts +2 -0
- package/dist/options-page.test.d.ts.map +1 -0
- package/dist/options-page.test.js +773 -0
- package/dist/options-page.test.js.map +1 -0
- package/dist/package-paths.d.ts +4 -0
- package/dist/package-paths.d.ts.map +1 -0
- package/dist/package-paths.js +30 -0
- package/dist/package-paths.js.map +1 -0
- package/dist/page-markdown.d.ts +40 -0
- package/dist/page-markdown.d.ts.map +1 -0
- package/dist/page-markdown.js +181 -0
- package/dist/page-markdown.js.map +1 -0
- package/dist/performance-examples.d.ts +5 -0
- package/dist/performance-examples.d.ts.map +1 -0
- package/dist/performance-examples.js +112 -0
- package/dist/performance-examples.js.map +1 -0
- package/dist/performance-profiling.md +417 -0
- package/dist/playwright-import.d.ts +19 -0
- package/dist/playwright-import.d.ts.map +1 -0
- package/dist/playwright-import.js +40 -0
- package/dist/playwright-import.js.map +1 -0
- package/dist/popup-relocation.test.d.ts +7 -0
- package/dist/popup-relocation.test.d.ts.map +1 -0
- package/dist/popup-relocation.test.js +139 -0
- package/dist/popup-relocation.test.js.map +1 -0
- package/dist/product-paths.d.ts +9 -0
- package/dist/product-paths.d.ts.map +1 -0
- package/dist/product-paths.js +24 -0
- package/dist/product-paths.js.map +1 -0
- package/dist/product-paths.test.d.ts +2 -0
- package/dist/product-paths.test.d.ts.map +1 -0
- package/dist/product-paths.test.js +39 -0
- package/dist/product-paths.test.js.map +1 -0
- package/dist/prompt.md +1004 -0
- package/dist/protocol.d.ts +248 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +59 -0
- package/dist/protocol.js.map +1 -0
- package/dist/protocol.test.d.ts +2 -0
- package/dist/protocol.test.d.ts.map +1 -0
- package/dist/protocol.test.js +31 -0
- package/dist/protocol.test.js.map +1 -0
- package/dist/react-source.d.ts +57 -0
- package/dist/react-source.d.ts.map +1 -0
- package/dist/react-source.js +254 -0
- package/dist/react-source.js.map +1 -0
- package/dist/readability.js +1674 -0
- package/dist/relay-client.d.ts +104 -0
- package/dist/relay-client.d.ts.map +1 -0
- package/dist/relay-client.js +426 -0
- package/dist/relay-client.js.map +1 -0
- package/dist/relay-client.test.d.ts +2 -0
- package/dist/relay-client.test.d.ts.map +1 -0
- package/dist/relay-client.test.js +305 -0
- package/dist/relay-client.test.js.map +1 -0
- package/dist/relay-core.test.d.ts +2 -0
- package/dist/relay-core.test.d.ts.map +1 -0
- package/dist/relay-core.test.js +1427 -0
- package/dist/relay-core.test.js.map +1 -0
- package/dist/relay-navigation.test.d.ts +2 -0
- package/dist/relay-navigation.test.d.ts.map +1 -0
- package/dist/relay-navigation.test.js +661 -0
- package/dist/relay-navigation.test.js.map +1 -0
- package/dist/relay-session.test.d.ts +2 -0
- package/dist/relay-session.test.d.ts.map +1 -0
- package/dist/relay-session.test.js +1121 -0
- package/dist/relay-session.test.js.map +1 -0
- package/dist/relay-state.d.ts +178 -0
- package/dist/relay-state.d.ts.map +1 -0
- package/dist/relay-state.js +346 -0
- package/dist/relay-state.js.map +1 -0
- package/dist/relay-state.test.d.ts +2 -0
- package/dist/relay-state.test.d.ts.map +1 -0
- package/dist/relay-state.test.js +594 -0
- package/dist/relay-state.test.js.map +1 -0
- package/dist/replay-ai-index.d.ts +99 -0
- package/dist/replay-ai-index.d.ts.map +1 -0
- package/dist/replay-ai-index.js +677 -0
- package/dist/replay-ai-index.js.map +1 -0
- package/dist/replay-ai-index.test.d.ts +2 -0
- package/dist/replay-ai-index.test.d.ts.map +1 -0
- package/dist/replay-ai-index.test.js +255 -0
- package/dist/replay-ai-index.test.js.map +1 -0
- package/dist/replay-cli.test.d.ts +2 -0
- package/dist/replay-cli.test.d.ts.map +1 -0
- package/dist/replay-cli.test.js +275 -0
- package/dist/replay-cli.test.js.map +1 -0
- package/dist/replay-eval.d.ts +44 -0
- package/dist/replay-eval.d.ts.map +1 -0
- package/dist/replay-eval.js +902 -0
- package/dist/replay-eval.js.map +1 -0
- package/dist/replay-handoff.d.ts +29 -0
- package/dist/replay-handoff.d.ts.map +1 -0
- package/dist/replay-handoff.js +83 -0
- package/dist/replay-handoff.js.map +1 -0
- package/dist/replay-handoff.test.d.ts +2 -0
- package/dist/replay-handoff.test.d.ts.map +1 -0
- package/dist/replay-handoff.test.js +123 -0
- package/dist/replay-handoff.test.js.map +1 -0
- package/dist/replay-workflow-compiler.d.ts +44 -0
- package/dist/replay-workflow-compiler.d.ts.map +1 -0
- package/dist/replay-workflow-compiler.js +601 -0
- package/dist/replay-workflow-compiler.js.map +1 -0
- package/dist/replay-workflow-compiler.test.d.ts +2 -0
- package/dist/replay-workflow-compiler.test.d.ts.map +1 -0
- package/dist/replay-workflow-compiler.test.js +223 -0
- package/dist/replay-workflow-compiler.test.js.map +1 -0
- package/dist/rrweb-recording-relay.d.ts +56 -0
- package/dist/rrweb-recording-relay.d.ts.map +1 -0
- package/dist/rrweb-recording-relay.js +299 -0
- package/dist/rrweb-recording-relay.js.map +1 -0
- package/dist/rrweb-recording-relay.test.d.ts +2 -0
- package/dist/rrweb-recording-relay.test.d.ts.map +1 -0
- package/dist/rrweb-recording-relay.test.js +85 -0
- package/dist/rrweb-recording-relay.test.js.map +1 -0
- package/dist/rrweb-recording.d.ts +104 -0
- package/dist/rrweb-recording.d.ts.map +1 -0
- package/dist/rrweb-recording.js +180 -0
- package/dist/rrweb-recording.js.map +1 -0
- package/dist/scoped-fs.d.ts +95 -0
- package/dist/scoped-fs.d.ts.map +1 -0
- package/dist/scoped-fs.js +358 -0
- package/dist/scoped-fs.js.map +1 -0
- package/dist/scoped-fs.test.d.ts +5 -0
- package/dist/scoped-fs.test.d.ts.map +1 -0
- package/dist/scoped-fs.test.js +73 -0
- package/dist/scoped-fs.test.js.map +1 -0
- package/dist/selector-generator.js +7378 -0
- package/dist/skill-stub.test.d.ts +2 -0
- package/dist/skill-stub.test.d.ts.map +1 -0
- package/dist/skill-stub.test.js +22 -0
- package/dist/skill-stub.test.js.map +1 -0
- package/dist/snapshot-tools.test.d.ts +2 -0
- package/dist/snapshot-tools.test.d.ts.map +1 -0
- package/dist/snapshot-tools.test.js +856 -0
- package/dist/snapshot-tools.test.js.map +1 -0
- package/dist/start-relay-server.d.ts +6 -0
- package/dist/start-relay-server.d.ts.map +1 -0
- package/dist/start-relay-server.js +57 -0
- package/dist/start-relay-server.js.map +1 -0
- package/dist/styles-api.md +124 -0
- package/dist/styles-examples.d.ts +8 -0
- package/dist/styles-examples.d.ts.map +1 -0
- package/dist/styles-examples.js +64 -0
- package/dist/styles-examples.js.map +1 -0
- package/dist/styles.d.ts +27 -0
- package/dist/styles.d.ts.map +1 -0
- package/dist/styles.js +231 -0
- package/dist/styles.js.map +1 -0
- package/dist/tabwright-agent-skill.d.ts +30 -0
- package/dist/tabwright-agent-skill.d.ts.map +1 -0
- package/dist/tabwright-agent-skill.js +74 -0
- package/dist/tabwright-agent-skill.js.map +1 -0
- package/dist/tabwright-agent-skill.test.d.ts +2 -0
- package/dist/tabwright-agent-skill.test.d.ts.map +1 -0
- package/dist/tabwright-agent-skill.test.js +51 -0
- package/dist/tabwright-agent-skill.test.js.map +1 -0
- package/dist/test-declarations.d.ts +13 -0
- package/dist/test-declarations.d.ts.map +1 -0
- package/dist/test-declarations.js +2 -0
- package/dist/test-declarations.js.map +1 -0
- package/dist/test-utils.d.ts +69 -0
- package/dist/test-utils.d.ts.map +1 -0
- package/dist/test-utils.js +448 -0
- package/dist/test-utils.js.map +1 -0
- package/dist/test-utils.test.d.ts +2 -0
- package/dist/test-utils.test.d.ts.map +1 -0
- package/dist/test-utils.test.js +55 -0
- package/dist/test-utils.test.js.map +1 -0
- package/dist/utils.d.ts +26 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +60 -0
- package/dist/utils.js.map +1 -0
- package/dist/wait-for-page-load.d.ts +16 -0
- package/dist/wait-for-page-load.d.ts.map +1 -0
- package/dist/wait-for-page-load.js +146 -0
- package/dist/wait-for-page-load.js.map +1 -0
- package/dist/workflow-capability.d.ts +72 -0
- package/dist/workflow-capability.d.ts.map +1 -0
- package/dist/workflow-capability.js +316 -0
- package/dist/workflow-capability.js.map +1 -0
- package/dist/workflow-script-flow.test.d.ts +2 -0
- package/dist/workflow-script-flow.test.d.ts.map +1 -0
- package/dist/workflow-script-flow.test.js +285 -0
- package/dist/workflow-script-flow.test.js.map +1 -0
- package/package.json +80 -0
- package/src/__snapshots__/x.com.processed.html +1003 -0
- package/src/__snapshots__/x.com.processed.withStyles.html +2720 -0
- package/src/a11y-client.ts +241 -0
- package/src/agent-skills/conan-config/SKILL.md +141 -0
- package/src/agent-skills/conan-config/agents/openai.yaml +4 -0
- package/src/aria-snapshot.test.ts +311 -0
- package/src/aria-snapshot.ts +1681 -0
- package/src/aria-snapshot.unit.test.ts +615 -0
- package/src/aria-snapshots/github-interactive.txt +193 -0
- package/src/aria-snapshots/github-raw.txt +309 -0
- package/src/aria-snapshots/hackernews-interactive.txt +477 -0
- package/src/aria-snapshots/hackernews-raw.txt +428 -0
- package/src/aria-snapshots/prosemirror-interactive.txt +66 -0
- package/src/aria-snapshots/prosemirror-raw.txt +145 -0
- package/src/assets/aria-labels-hacker-news.png +0 -0
- package/src/assets/aria-labels-old-reddit.png +0 -0
- package/src/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.ts +5 -0
- package/src/assets/cursors/screen-studio/pointer-macos-tahoe.svg +18 -0
- package/src/assets/x.com.html +32946 -0
- package/src/browser-config.ts +179 -0
- package/src/browser-install.ts +284 -0
- package/src/browser-launch.ts +75 -0
- package/src/builtin-capabilities.ts +2765 -0
- package/src/capability-agent-skill.ts +305 -0
- package/src/capability-auth-state.ts +197 -0
- package/src/capability-auth.ts +249 -0
- package/src/capability-cli-confirmation.test.ts +90 -0
- package/src/capability-options.test.ts +89 -0
- package/src/capability-options.ts +57 -0
- package/src/capability-package.test.ts +204 -0
- package/src/capability-package.ts +555 -0
- package/src/capability-registry.test.ts +1905 -0
- package/src/capability-registry.ts +1234 -0
- package/src/capability-runner.ts +801 -0
- package/src/capability-search-relevance.test.ts +73 -0
- package/src/capability-studio.test.ts +45 -0
- package/src/capability-studio.ts +641 -0
- package/src/cdp-log.test.ts +131 -0
- package/src/cdp-log.ts +112 -0
- package/src/cdp-relay.ts +2944 -0
- package/src/cdp-session.ts +77 -0
- package/src/cdp-types.ts +158 -0
- package/src/channel-owner-inspect.test.ts +94 -0
- package/src/chrome-discovery.test.ts +50 -0
- package/src/chrome-discovery.ts +220 -0
- package/src/clean-html.ts +141 -0
- package/src/cli-help.test.ts +123 -0
- package/src/cli.ts +3181 -0
- package/src/cloud-client.ts +179 -0
- package/src/create-logger.ts +38 -0
- package/src/debugger-examples-types.ts +16 -0
- package/src/debugger-examples.ts +66 -0
- package/src/debugger.ts +708 -0
- package/src/diff-utils.test.ts +152 -0
- package/src/diff-utils.ts +70 -0
- package/src/doctor.test.ts +244 -0
- package/src/doctor.ts +321 -0
- package/src/editor-examples.ts +158 -0
- package/src/editor.ts +426 -0
- package/src/env-compat.ts +28 -0
- package/src/executor.ts +1824 -0
- package/src/executor.unit.test.ts +168 -0
- package/src/extension-connection.test.ts +855 -0
- package/src/extension-icon-warning.test.ts +45 -0
- package/src/ghost-browser.ts +149 -0
- package/src/ghost-cursor-client.ts +494 -0
- package/src/ghost-cursor-controller.ts +129 -0
- package/src/ghost-cursor.ts +123 -0
- package/src/htmlrewrite.test.ts +8008 -0
- package/src/htmlrewrite.ts +390 -0
- package/src/index.ts +53 -0
- package/src/kill-port.ts +219 -0
- package/src/kitty-graphics.test.ts +139 -0
- package/src/kitty-graphics.ts +79 -0
- package/src/locator-selector.test.ts +119 -0
- package/src/mcp-client.ts +78 -0
- package/src/mcp.ts +629 -0
- package/src/on-mouse-action.test.ts +226 -0
- package/src/options-page.test.ts +839 -0
- package/src/package-paths.ts +38 -0
- package/src/page-markdown.ts +240 -0
- package/src/performance-examples.ts +186 -0
- package/src/playwright-import.ts +59 -0
- package/src/popup-relocation.test.ts +163 -0
- package/src/product-paths.test.ts +47 -0
- package/src/product-paths.ts +28 -0
- package/src/protocol.test.ts +52 -0
- package/src/protocol.ts +351 -0
- package/src/react-source.ts +379 -0
- package/src/relay-client.test.ts +375 -0
- package/src/relay-client.ts +572 -0
- package/src/relay-core.test.ts +1603 -0
- package/src/relay-navigation.test.ts +790 -0
- package/src/relay-session.test.ts +1375 -0
- package/src/relay-state.test.ts +729 -0
- package/src/relay-state.ts +561 -0
- package/src/replay-ai-index.test.ts +278 -0
- package/src/replay-ai-index.ts +872 -0
- package/src/replay-cli.test.ts +325 -0
- package/src/replay-eval.ts +1039 -0
- package/src/replay-handoff.test.ts +163 -0
- package/src/replay-handoff.ts +109 -0
- package/src/replay-workflow-compiler.test.ts +250 -0
- package/src/replay-workflow-compiler.ts +700 -0
- package/src/resource.md +409 -0
- package/src/rrweb-recording-relay.test.ts +99 -0
- package/src/rrweb-recording-relay.ts +400 -0
- package/src/rrweb-recording.ts +322 -0
- package/src/scoped-fs.test.ts +80 -0
- package/src/scoped-fs.ts +420 -0
- package/src/skill-stub.test.ts +24 -0
- package/src/skill.md +1413 -0
- package/src/snapshot-tools.test.ts +1017 -0
- package/src/snapshots/hacker-news-accessibility-full.md +81 -0
- package/src/snapshots/hacker-news-accessibility-interactive.md +43 -0
- package/src/snapshots/page-markdown-output.txt +16 -0
- package/src/snapshots/shadcn-ui-accessibility-full.md +201 -0
- package/src/snapshots/shadcn-ui-accessibility-interactive.md +109 -0
- package/src/start-relay-server.ts +69 -0
- package/src/styles-examples.ts +84 -0
- package/src/styles.ts +343 -0
- package/src/tabwright-agent-skill.test.ts +61 -0
- package/src/tabwright-agent-skill.ts +105 -0
- package/src/test-declarations.ts +13 -0
- package/src/test-utils.test.ts +70 -0
- package/src/test-utils.ts +566 -0
- package/src/utils.ts +78 -0
- package/src/wait-for-page-load.ts +198 -0
- package/src/workflow-capability.ts +417 -0
- package/src/workflow-script-flow.test.ts +388 -0
|
@@ -0,0 +1,3994 @@
|
|
|
1
|
+
//#region ../node_modules/.pnpm/string-dedent@3.0.2/node_modules/string-dedent/dist/dedent.mjs
|
|
2
|
+
var cache = /* @__PURE__ */ new WeakMap();
|
|
3
|
+
var newline = /(\n|\r\n?|\u2028|\u2029)/g;
|
|
4
|
+
var leadingWhitespace = /^\s*/;
|
|
5
|
+
var nonWhitespace = /\S/;
|
|
6
|
+
var slice = Array.prototype.slice;
|
|
7
|
+
var zero = "0".charCodeAt(0);
|
|
8
|
+
var nine = "9".charCodeAt(0);
|
|
9
|
+
var lowerA = "a".charCodeAt(0);
|
|
10
|
+
var lowerF = "f".charCodeAt(0);
|
|
11
|
+
var upperA = "A".charCodeAt(0);
|
|
12
|
+
var upperF = "F".charCodeAt(0);
|
|
13
|
+
function dedent(arg) {
|
|
14
|
+
if (typeof arg === "string") return process([arg])[0];
|
|
15
|
+
if (typeof arg === "function") return function() {
|
|
16
|
+
const args = slice.call(arguments);
|
|
17
|
+
args[0] = processTemplateStringsArray(args[0]);
|
|
18
|
+
return arg.apply(this, args);
|
|
19
|
+
};
|
|
20
|
+
const strings = processTemplateStringsArray(arg);
|
|
21
|
+
let s = getCooked(strings, 0);
|
|
22
|
+
for (let i = 1; i < strings.length; i++) s += arguments[i] + getCooked(strings, i);
|
|
23
|
+
return s;
|
|
24
|
+
}
|
|
25
|
+
function getCooked(strings, index) {
|
|
26
|
+
const str = strings[index];
|
|
27
|
+
if (str === void 0) throw new TypeError(`invalid cooked string at index ${index}`);
|
|
28
|
+
return str;
|
|
29
|
+
}
|
|
30
|
+
function processTemplateStringsArray(strings) {
|
|
31
|
+
const cached = cache.get(strings);
|
|
32
|
+
if (cached) return cached;
|
|
33
|
+
const raw = process(strings.raw);
|
|
34
|
+
const cooked = raw.map(cook);
|
|
35
|
+
Object.defineProperty(cooked, "raw", { value: Object.freeze(raw) });
|
|
36
|
+
Object.freeze(cooked);
|
|
37
|
+
cache.set(strings, cooked);
|
|
38
|
+
return cooked;
|
|
39
|
+
}
|
|
40
|
+
function process(strings) {
|
|
41
|
+
const splitQuasis = strings.map((quasi) => quasi.split(newline));
|
|
42
|
+
let common;
|
|
43
|
+
for (let i = 0; i < splitQuasis.length; i++) {
|
|
44
|
+
const lines = splitQuasis[i];
|
|
45
|
+
const firstSplit = i === 0;
|
|
46
|
+
const lastSplit = i + 1 === splitQuasis.length;
|
|
47
|
+
if (firstSplit) {
|
|
48
|
+
if (lines.length === 1 || lines[0].length > 0) throw new Error("invalid content on opening line");
|
|
49
|
+
lines[1] = "";
|
|
50
|
+
}
|
|
51
|
+
if (lastSplit) {
|
|
52
|
+
if (lines.length === 1 || nonWhitespace.test(lines[lines.length - 1])) throw new Error("invalid content on closing line");
|
|
53
|
+
lines[lines.length - 2] = "";
|
|
54
|
+
lines[lines.length - 1] = "";
|
|
55
|
+
}
|
|
56
|
+
for (let j = 2; j < lines.length; j += 2) {
|
|
57
|
+
const text = lines[j];
|
|
58
|
+
const lineContainsTemplateExpression = j + 1 === lines.length && !lastSplit;
|
|
59
|
+
const leading = leadingWhitespace.exec(text)[0];
|
|
60
|
+
if (!lineContainsTemplateExpression && leading.length === text.length) {
|
|
61
|
+
lines[j] = "";
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
common = commonStart(leading, common);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const min = common ? common.length : 0;
|
|
68
|
+
return splitQuasis.map((lines) => {
|
|
69
|
+
let quasi = lines[0];
|
|
70
|
+
for (let i = 1; i < lines.length; i += 2) {
|
|
71
|
+
const newline = lines[i];
|
|
72
|
+
const text = lines[i + 1];
|
|
73
|
+
quasi += newline + text.slice(min);
|
|
74
|
+
}
|
|
75
|
+
return quasi;
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
function commonStart(a, b) {
|
|
79
|
+
if (b === void 0 || a === b) return a;
|
|
80
|
+
let i = 0;
|
|
81
|
+
for (const len = Math.min(a.length, b.length); i < len; i++) if (a[i] !== b[i]) break;
|
|
82
|
+
return a.slice(0, i);
|
|
83
|
+
}
|
|
84
|
+
function cook(raw) {
|
|
85
|
+
let out = "";
|
|
86
|
+
let start = 0;
|
|
87
|
+
let i = 0;
|
|
88
|
+
while ((i = raw.indexOf("\\", i)) > -1) {
|
|
89
|
+
out += raw.slice(start, i);
|
|
90
|
+
if (++i === raw.length) return void 0;
|
|
91
|
+
const next = raw[i++];
|
|
92
|
+
switch (next) {
|
|
93
|
+
case "b":
|
|
94
|
+
out += "\b";
|
|
95
|
+
break;
|
|
96
|
+
case "t":
|
|
97
|
+
out += " ";
|
|
98
|
+
break;
|
|
99
|
+
case "n":
|
|
100
|
+
out += "\n";
|
|
101
|
+
break;
|
|
102
|
+
case "v":
|
|
103
|
+
out += "\v";
|
|
104
|
+
break;
|
|
105
|
+
case "f":
|
|
106
|
+
out += "\f";
|
|
107
|
+
break;
|
|
108
|
+
case "r":
|
|
109
|
+
out += "\r";
|
|
110
|
+
break;
|
|
111
|
+
case "\r": if (i < raw.length && raw[i] === "\n") ++i;
|
|
112
|
+
case "\n":
|
|
113
|
+
case "\u2028":
|
|
114
|
+
case "\u2029": break;
|
|
115
|
+
case "0":
|
|
116
|
+
if (isDigit(raw, i)) return void 0;
|
|
117
|
+
out += "\0";
|
|
118
|
+
break;
|
|
119
|
+
case "x": {
|
|
120
|
+
const n = parseHex(raw, i, i + 2);
|
|
121
|
+
if (n === -1) return void 0;
|
|
122
|
+
i += 2;
|
|
123
|
+
out += String.fromCharCode(n);
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
case "u": {
|
|
127
|
+
let n;
|
|
128
|
+
if (i < raw.length && raw[i] === "{") {
|
|
129
|
+
const end = raw.indexOf("}", ++i);
|
|
130
|
+
if (end === -1) return void 0;
|
|
131
|
+
n = parseHex(raw, i, end);
|
|
132
|
+
i = end + 1;
|
|
133
|
+
} else {
|
|
134
|
+
n = parseHex(raw, i, i + 4);
|
|
135
|
+
i += 4;
|
|
136
|
+
}
|
|
137
|
+
if (n === -1 || n > 1114111) return void 0;
|
|
138
|
+
out += String.fromCodePoint(n);
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
default:
|
|
142
|
+
if (isDigit(next, 0)) return void 0;
|
|
143
|
+
out += next;
|
|
144
|
+
}
|
|
145
|
+
start = i;
|
|
146
|
+
}
|
|
147
|
+
return out + raw.slice(start);
|
|
148
|
+
}
|
|
149
|
+
function isDigit(str, index) {
|
|
150
|
+
const c = str.charCodeAt(index);
|
|
151
|
+
return c >= zero && c <= nine;
|
|
152
|
+
}
|
|
153
|
+
function parseHex(str, index, end) {
|
|
154
|
+
if (end >= str.length) return -1;
|
|
155
|
+
let n = 0;
|
|
156
|
+
for (; index < end; index++) {
|
|
157
|
+
const c = hexToInt(str.charCodeAt(index));
|
|
158
|
+
if (c === -1) return -1;
|
|
159
|
+
n = n * 16 + c;
|
|
160
|
+
}
|
|
161
|
+
return n;
|
|
162
|
+
}
|
|
163
|
+
function hexToInt(c) {
|
|
164
|
+
if (c >= zero && c <= nine) return c - zero;
|
|
165
|
+
if (c >= lowerA && c <= lowerF) return c - lowerA + 10;
|
|
166
|
+
if (c >= upperA && c <= upperF) return c - upperA + 10;
|
|
167
|
+
return -1;
|
|
168
|
+
}
|
|
169
|
+
//#endregion
|
|
170
|
+
//#region ../node_modules/.pnpm/zustand@5.0.14_@types+react@19.2.17_react@19.2.7/node_modules/zustand/esm/vanilla.mjs
|
|
171
|
+
var createStoreImpl = (createState) => {
|
|
172
|
+
let state;
|
|
173
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
174
|
+
const setState = (partial, replace) => {
|
|
175
|
+
const nextState = typeof partial === "function" ? partial(state) : partial;
|
|
176
|
+
if (!Object.is(nextState, state)) {
|
|
177
|
+
const previousState = state;
|
|
178
|
+
state = (replace != null ? replace : typeof nextState !== "object" || nextState === null) ? nextState : Object.assign({}, state, nextState);
|
|
179
|
+
listeners.forEach((listener) => listener(state, previousState));
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
const getState = () => state;
|
|
183
|
+
const getInitialState = () => initialState;
|
|
184
|
+
const subscribe = (listener) => {
|
|
185
|
+
listeners.add(listener);
|
|
186
|
+
return () => listeners.delete(listener);
|
|
187
|
+
};
|
|
188
|
+
const api = {
|
|
189
|
+
setState,
|
|
190
|
+
getState,
|
|
191
|
+
getInitialState,
|
|
192
|
+
subscribe
|
|
193
|
+
};
|
|
194
|
+
const initialState = state = createState(setState, getState, api);
|
|
195
|
+
return api;
|
|
196
|
+
};
|
|
197
|
+
var createStore = ((createState) => createState ? createStoreImpl(createState) : createStoreImpl);
|
|
198
|
+
//#endregion
|
|
199
|
+
//#region src/toolbar/toolbar.ts
|
|
200
|
+
function initTabwrightToolbarBridge() {
|
|
201
|
+
if (window.__playwriterToolbarBridgeInstalled) return;
|
|
202
|
+
window.__playwriterToolbarBridgeInstalled = true;
|
|
203
|
+
function isToolbarRequest(value) {
|
|
204
|
+
if (!value || typeof value !== "object") return false;
|
|
205
|
+
const candidate = value;
|
|
206
|
+
return candidate.source === "playwriter-toolbar" && (candidate.type === "recording-status" || candidate.type === "recording-toggle") && typeof candidate.requestId === "string";
|
|
207
|
+
}
|
|
208
|
+
function isToolbarRuntimeResponse(value, requestId) {
|
|
209
|
+
if (!value || typeof value !== "object") return false;
|
|
210
|
+
return value.requestId === requestId;
|
|
211
|
+
}
|
|
212
|
+
function getErrorMessage(error) {
|
|
213
|
+
return error instanceof Error ? error.message : String(error);
|
|
214
|
+
}
|
|
215
|
+
function isExtensionContextInvalidated(error) {
|
|
216
|
+
return getErrorMessage(error).includes("Extension context invalidated");
|
|
217
|
+
}
|
|
218
|
+
window.addEventListener("message", (event) => {
|
|
219
|
+
if (!isToolbarRequest(event.data)) return;
|
|
220
|
+
const data = event.data;
|
|
221
|
+
const action = data.type === "recording-toggle" ? "playwriterToolbarToggleRecording" : "playwriterToolbarRecordingStatus";
|
|
222
|
+
const requestId = data.requestId;
|
|
223
|
+
const responseType = `${data.type}-result`;
|
|
224
|
+
const connectedPort = (() => {
|
|
225
|
+
try {
|
|
226
|
+
return chrome.runtime.connect({ name: "playwriter-toolbar-recording" });
|
|
227
|
+
} catch (error) {
|
|
228
|
+
if (isExtensionContextInvalidated(error)) return null;
|
|
229
|
+
const message = getErrorMessage(error);
|
|
230
|
+
console.warn("[Tabwright toolbar bridge] port connect failed", data.type, requestId, message);
|
|
231
|
+
window.postMessage({
|
|
232
|
+
source: "playwriter-toolbar-bridge",
|
|
233
|
+
type: responseType,
|
|
234
|
+
requestId,
|
|
235
|
+
result: {
|
|
236
|
+
success: false,
|
|
237
|
+
error: message
|
|
238
|
+
}
|
|
239
|
+
}, "*");
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
})();
|
|
243
|
+
if (!connectedPort) return;
|
|
244
|
+
const port = connectedPort;
|
|
245
|
+
const timeoutId = window.setTimeout(() => {
|
|
246
|
+
failWithMessage({
|
|
247
|
+
message: `Tabwright toolbar background did not respond (${data.type}, ${requestId})`,
|
|
248
|
+
disconnect: true
|
|
249
|
+
});
|
|
250
|
+
}, 85e3);
|
|
251
|
+
let settled = false;
|
|
252
|
+
function cleanup(options) {
|
|
253
|
+
window.clearTimeout(timeoutId);
|
|
254
|
+
port.onMessage.removeListener(onPortMessage);
|
|
255
|
+
port.onDisconnect.removeListener(onDisconnect);
|
|
256
|
+
if (options.disconnect) port.disconnect();
|
|
257
|
+
}
|
|
258
|
+
function postResult(result) {
|
|
259
|
+
window.postMessage({
|
|
260
|
+
source: "playwriter-toolbar-bridge",
|
|
261
|
+
type: responseType,
|
|
262
|
+
requestId,
|
|
263
|
+
result
|
|
264
|
+
}, "*");
|
|
265
|
+
}
|
|
266
|
+
function abandonRequest() {
|
|
267
|
+
if (settled) return;
|
|
268
|
+
settled = true;
|
|
269
|
+
cleanup({ disconnect: false });
|
|
270
|
+
}
|
|
271
|
+
function failWithMessage(options) {
|
|
272
|
+
if (settled) return;
|
|
273
|
+
settled = true;
|
|
274
|
+
cleanup({ disconnect: options.disconnect });
|
|
275
|
+
console.warn("[Tabwright toolbar bridge] port request failed", data.type, requestId, options.message);
|
|
276
|
+
postResult({
|
|
277
|
+
success: false,
|
|
278
|
+
error: options.message
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
function onPortMessage(response) {
|
|
282
|
+
if (!isToolbarRuntimeResponse(response, requestId)) return;
|
|
283
|
+
if (settled) return;
|
|
284
|
+
settled = true;
|
|
285
|
+
cleanup({ disconnect: true });
|
|
286
|
+
postResult(response.result);
|
|
287
|
+
}
|
|
288
|
+
function onDisconnect() {
|
|
289
|
+
const message = (() => {
|
|
290
|
+
try {
|
|
291
|
+
return chrome.runtime.lastError?.message || "Tabwright toolbar background port disconnected";
|
|
292
|
+
} catch (error) {
|
|
293
|
+
return getErrorMessage(error);
|
|
294
|
+
}
|
|
295
|
+
})();
|
|
296
|
+
if (message.includes("Extension context invalidated")) {
|
|
297
|
+
abandonRequest();
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
failWithMessage({
|
|
301
|
+
message,
|
|
302
|
+
disconnect: false
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
port.onMessage.addListener(onPortMessage);
|
|
306
|
+
port.onDisconnect.addListener(onDisconnect);
|
|
307
|
+
try {
|
|
308
|
+
port.postMessage({
|
|
309
|
+
action,
|
|
310
|
+
requestId
|
|
311
|
+
});
|
|
312
|
+
} catch (error) {
|
|
313
|
+
if (isExtensionContextInvalidated(error)) {
|
|
314
|
+
abandonRequest();
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
failWithMessage({
|
|
318
|
+
message: getErrorMessage(error),
|
|
319
|
+
disconnect: true
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
function initTabwrightToolbar() {
|
|
325
|
+
if (window.__playwriterToolbarInstalled) return;
|
|
326
|
+
window.__playwriterToolbarInstalled = true;
|
|
327
|
+
try {
|
|
328
|
+
if (window !== window.top) return;
|
|
329
|
+
} catch {
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
let pinModeActive = false;
|
|
333
|
+
let pinCount = 0;
|
|
334
|
+
let recordingActive = false;
|
|
335
|
+
let recordingBusy = false;
|
|
336
|
+
let nextBridgeRequestId = 1;
|
|
337
|
+
let toastTimer = null;
|
|
338
|
+
let overlayEl = null;
|
|
339
|
+
let annotationLayerEl = null;
|
|
340
|
+
let annotationEditorEl = null;
|
|
341
|
+
let annotationPreview = null;
|
|
342
|
+
let annotationCount = 0;
|
|
343
|
+
let annotationLayoutListening = false;
|
|
344
|
+
const annotationMarkers = [];
|
|
345
|
+
let pinBtn;
|
|
346
|
+
let recordBtn;
|
|
347
|
+
const host = document.createElement("div");
|
|
348
|
+
host.setAttribute("data-playwriter-toolbar", "1");
|
|
349
|
+
host.style.cssText = "position:fixed;top:12px;right:12px;z-index:2147483647;pointer-events:none;font-size:0;line-height:0;";
|
|
350
|
+
const shadow = host.attachShadow({ mode: "closed" });
|
|
351
|
+
const styleEl = document.createElement("style");
|
|
352
|
+
styleEl.textContent = `
|
|
353
|
+
*,*::before,*::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
354
|
+
.toolbar {
|
|
355
|
+
display: flex;
|
|
356
|
+
align-items: center;
|
|
357
|
+
gap: 2px;
|
|
358
|
+
padding: 3px;
|
|
359
|
+
background: #fff;
|
|
360
|
+
border-radius: 10px;
|
|
361
|
+
pointer-events: all;
|
|
362
|
+
user-select: none;
|
|
363
|
+
box-shadow: 0px 0px 0.5px rgba(0,0,0,0.18), 0px 3px 8px rgba(0,0,0,0.1), 0px 1px 3px rgba(0,0,0,0.1);
|
|
364
|
+
}
|
|
365
|
+
.divider {
|
|
366
|
+
width: 1px;
|
|
367
|
+
height: 12px;
|
|
368
|
+
background: rgba(0, 0, 0, 0.08);
|
|
369
|
+
margin: 0 1px;
|
|
370
|
+
flex-shrink: 0;
|
|
371
|
+
}
|
|
372
|
+
.btn {
|
|
373
|
+
display: flex;
|
|
374
|
+
align-items: center;
|
|
375
|
+
justify-content: center;
|
|
376
|
+
width: 26px;
|
|
377
|
+
height: 26px;
|
|
378
|
+
border: none;
|
|
379
|
+
border-radius: 7px;
|
|
380
|
+
background: transparent;
|
|
381
|
+
color: #000;
|
|
382
|
+
cursor: pointer;
|
|
383
|
+
transition: background 0.1s;
|
|
384
|
+
padding: 0;
|
|
385
|
+
flex-shrink: 0;
|
|
386
|
+
outline: none;
|
|
387
|
+
}
|
|
388
|
+
.btn:hover {
|
|
389
|
+
background: rgba(0, 0, 0, 0.04);
|
|
390
|
+
}
|
|
391
|
+
.btn.active {
|
|
392
|
+
background: #0d99ff;
|
|
393
|
+
color: #fff;
|
|
394
|
+
}
|
|
395
|
+
.btn.active:hover {
|
|
396
|
+
background: #0d99ff;
|
|
397
|
+
filter: brightness(1.05);
|
|
398
|
+
}
|
|
399
|
+
.btn.recording {
|
|
400
|
+
color: #ef4444;
|
|
401
|
+
}
|
|
402
|
+
.btn.recording.active {
|
|
403
|
+
background: #ef4444;
|
|
404
|
+
color: #fff;
|
|
405
|
+
}
|
|
406
|
+
.btn.recording.active:hover {
|
|
407
|
+
background: #ef4444;
|
|
408
|
+
filter: brightness(1.05);
|
|
409
|
+
}
|
|
410
|
+
.btn.busy {
|
|
411
|
+
opacity: 0.55;
|
|
412
|
+
cursor: wait;
|
|
413
|
+
}
|
|
414
|
+
/* When active, the logo inner cursor path needs to match the blue bg
|
|
415
|
+
so it appears as a "cutout" through the white outer shape */
|
|
416
|
+
.btn.active .logo-inner { fill: #0d99ff; }
|
|
417
|
+
.toast {
|
|
418
|
+
position: fixed;
|
|
419
|
+
background: #0f172a;
|
|
420
|
+
border-radius: 8px;
|
|
421
|
+
padding: 9px 18px;
|
|
422
|
+
color: rgba(255, 255, 255, 0.85);
|
|
423
|
+
font-size: 11px;
|
|
424
|
+
font-family: ui-monospace, 'SF Mono', Menlo, monospace;
|
|
425
|
+
pointer-events: none;
|
|
426
|
+
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.35);
|
|
427
|
+
white-space: nowrap;
|
|
428
|
+
z-index: 1;
|
|
429
|
+
--toast-transform: translateX(-50%);
|
|
430
|
+
animation: toast-in 0.15s ease;
|
|
431
|
+
}
|
|
432
|
+
@keyframes toast-in {
|
|
433
|
+
from { opacity: 0; transform: var(--toast-transform) translateY(4px); }
|
|
434
|
+
to { opacity: 1; transform: var(--toast-transform); }
|
|
435
|
+
}
|
|
436
|
+
.annotation-popover {
|
|
437
|
+
position: fixed;
|
|
438
|
+
width: min(360px, calc(100vw - 24px));
|
|
439
|
+
padding: 12px;
|
|
440
|
+
border: 1px solid rgba(15, 23, 42, 0.12);
|
|
441
|
+
border-radius: 12px;
|
|
442
|
+
background: rgba(255, 255, 255, 0.98);
|
|
443
|
+
color: #0f172a;
|
|
444
|
+
font-family: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
445
|
+
box-shadow: 0 18px 50px rgba(15, 23, 42, 0.18), 0 3px 12px rgba(15, 23, 42, 0.1);
|
|
446
|
+
z-index: 2;
|
|
447
|
+
pointer-events: all;
|
|
448
|
+
animation: annotation-in 0.14s ease;
|
|
449
|
+
backdrop-filter: blur(10px);
|
|
450
|
+
}
|
|
451
|
+
@keyframes annotation-in {
|
|
452
|
+
from { opacity: 0; transform: translateY(4px) scale(0.98); }
|
|
453
|
+
to { opacity: 1; transform: translateY(0) scale(1); }
|
|
454
|
+
}
|
|
455
|
+
.annotation-title {
|
|
456
|
+
display: flex;
|
|
457
|
+
align-items: center;
|
|
458
|
+
justify-content: space-between;
|
|
459
|
+
gap: 8px;
|
|
460
|
+
margin-bottom: 8px;
|
|
461
|
+
font-size: 12px;
|
|
462
|
+
font-weight: 650;
|
|
463
|
+
letter-spacing: 0;
|
|
464
|
+
}
|
|
465
|
+
.annotation-target {
|
|
466
|
+
max-width: 210px;
|
|
467
|
+
overflow: hidden;
|
|
468
|
+
text-overflow: ellipsis;
|
|
469
|
+
white-space: nowrap;
|
|
470
|
+
color: #64748b;
|
|
471
|
+
font-size: 11px;
|
|
472
|
+
font-weight: 500;
|
|
473
|
+
}
|
|
474
|
+
.annotation-input {
|
|
475
|
+
display: block;
|
|
476
|
+
width: 100%;
|
|
477
|
+
min-height: 76px;
|
|
478
|
+
max-height: 150px;
|
|
479
|
+
resize: vertical;
|
|
480
|
+
border: 1px solid #d8dee8;
|
|
481
|
+
border-radius: 9px;
|
|
482
|
+
padding: 9px 10px;
|
|
483
|
+
color: #0f172a;
|
|
484
|
+
background: #fff;
|
|
485
|
+
font: 12px/1.45 ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
486
|
+
outline: none;
|
|
487
|
+
}
|
|
488
|
+
.annotation-input:focus {
|
|
489
|
+
border-color: #0d99ff;
|
|
490
|
+
box-shadow: 0 0 0 3px rgba(13, 153, 255, 0.14);
|
|
491
|
+
}
|
|
492
|
+
.annotation-actions {
|
|
493
|
+
display: flex;
|
|
494
|
+
justify-content: flex-end;
|
|
495
|
+
gap: 8px;
|
|
496
|
+
margin-top: 10px;
|
|
497
|
+
}
|
|
498
|
+
.annotation-action {
|
|
499
|
+
height: 28px;
|
|
500
|
+
padding: 0 11px;
|
|
501
|
+
border-radius: 8px;
|
|
502
|
+
border: 1px solid transparent;
|
|
503
|
+
font-size: 12px;
|
|
504
|
+
font-weight: 600;
|
|
505
|
+
cursor: pointer;
|
|
506
|
+
background: transparent;
|
|
507
|
+
color: #334155;
|
|
508
|
+
}
|
|
509
|
+
.annotation-action:hover {
|
|
510
|
+
background: #f1f5f9;
|
|
511
|
+
}
|
|
512
|
+
.annotation-action.primary {
|
|
513
|
+
color: #fff;
|
|
514
|
+
background: #0d99ff;
|
|
515
|
+
border-color: #0d99ff;
|
|
516
|
+
}
|
|
517
|
+
.annotation-action.primary:hover {
|
|
518
|
+
background: #087ad1;
|
|
519
|
+
border-color: #087ad1;
|
|
520
|
+
}
|
|
521
|
+
`;
|
|
522
|
+
const toolbarEl = document.createElement("div");
|
|
523
|
+
toolbarEl.className = "toolbar";
|
|
524
|
+
toolbarEl.setAttribute("role", "toolbar");
|
|
525
|
+
toolbarEl.setAttribute("aria-label", "Tabwright tools");
|
|
526
|
+
shadow.appendChild(styleEl);
|
|
527
|
+
shadow.appendChild(toolbarEl);
|
|
528
|
+
function showToast(msg, anchorRect) {
|
|
529
|
+
shadow.querySelectorAll(".toast").forEach((el) => {
|
|
530
|
+
el.remove();
|
|
531
|
+
});
|
|
532
|
+
if (toastTimer !== null) clearTimeout(toastTimer);
|
|
533
|
+
const toastEl = document.createElement("div");
|
|
534
|
+
toastEl.className = "toast";
|
|
535
|
+
toastEl.textContent = msg;
|
|
536
|
+
if (anchorRect) {
|
|
537
|
+
const GAP = 8;
|
|
538
|
+
const centerX = anchorRect.left + anchorRect.width / 2;
|
|
539
|
+
const belowY = anchorRect.bottom + GAP;
|
|
540
|
+
const fitsBelow = belowY + 36 < window.innerHeight;
|
|
541
|
+
const top = fitsBelow ? belowY : anchorRect.top - GAP;
|
|
542
|
+
const transformOrigin = fitsBelow ? "top center" : "bottom center";
|
|
543
|
+
toastEl.style.left = Math.max(8, Math.min(centerX, window.innerWidth - 8)) + "px";
|
|
544
|
+
toastEl.style.top = top + "px";
|
|
545
|
+
const baseTransform = fitsBelow ? "translateX(-50%)" : "translateX(-50%) translateY(-100%)";
|
|
546
|
+
toastEl.style.setProperty("--toast-transform", baseTransform);
|
|
547
|
+
toastEl.style.transform = baseTransform;
|
|
548
|
+
toastEl.style.transformOrigin = transformOrigin;
|
|
549
|
+
} else {
|
|
550
|
+
toastEl.style.bottom = "20px";
|
|
551
|
+
toastEl.style.left = "50%";
|
|
552
|
+
toastEl.style.transform = "translateX(-50%)";
|
|
553
|
+
}
|
|
554
|
+
shadow.appendChild(toastEl);
|
|
555
|
+
toastTimer = window.setTimeout(() => {
|
|
556
|
+
toastEl.remove();
|
|
557
|
+
}, 1900);
|
|
558
|
+
}
|
|
559
|
+
function isToolbarBridgeResult(value) {
|
|
560
|
+
if (!value || typeof value !== "object") return false;
|
|
561
|
+
const candidate = value;
|
|
562
|
+
return (candidate.success === void 0 || typeof candidate.success === "boolean") && (candidate.isRecording === void 0 || typeof candidate.isRecording === "boolean") && (candidate.startedAt === void 0 || typeof candidate.startedAt === "number") && (candidate.tabId === void 0 || typeof candidate.tabId === "number") && (candidate.id === void 0 || typeof candidate.id === "string") && (candidate.path === void 0 || typeof candidate.path === "string") && (candidate.duration === void 0 || typeof candidate.duration === "number") && (candidate.size === void 0 || typeof candidate.size === "number") && (candidate.replayId === void 0 || typeof candidate.replayId === "string") && (candidate.replayPath === void 0 || typeof candidate.replayPath === "string") && (candidate.replayDuration === void 0 || typeof candidate.replayDuration === "number") && (candidate.replaySize === void 0 || typeof candidate.replaySize === "number") && (candidate.replayEventCount === void 0 || typeof candidate.replayEventCount === "number") && (candidate.warning === void 0 || typeof candidate.warning === "string") && (candidate.error === void 0 || typeof candidate.error === "string");
|
|
563
|
+
}
|
|
564
|
+
function sendToolbarBridgeRequest(type) {
|
|
565
|
+
const requestId = `playwriter-${Date.now()}-${nextBridgeRequestId++}`;
|
|
566
|
+
return new Promise((resolve, reject) => {
|
|
567
|
+
const timeout = window.setTimeout(() => {
|
|
568
|
+
window.removeEventListener("message", onResponse);
|
|
569
|
+
console.warn("[Tabwright toolbar] bridge timeout", type, requestId);
|
|
570
|
+
reject(/* @__PURE__ */ new Error(`Tabwright toolbar bridge did not respond (${type}, ${requestId})`));
|
|
571
|
+
}, 9e4);
|
|
572
|
+
function onResponse(event) {
|
|
573
|
+
const data = event.data;
|
|
574
|
+
if (!data || typeof data !== "object") return;
|
|
575
|
+
const candidate = data;
|
|
576
|
+
if (candidate.source !== "playwriter-toolbar-bridge" || candidate.type !== `${type}-result` || candidate.requestId !== requestId) return;
|
|
577
|
+
window.clearTimeout(timeout);
|
|
578
|
+
window.removeEventListener("message", onResponse);
|
|
579
|
+
if (!isToolbarBridgeResult(candidate.result)) {
|
|
580
|
+
reject(/* @__PURE__ */ new Error("Invalid Tabwright toolbar bridge response"));
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
resolve(candidate.result);
|
|
584
|
+
}
|
|
585
|
+
window.addEventListener("message", onResponse);
|
|
586
|
+
window.postMessage({
|
|
587
|
+
source: "playwriter-toolbar",
|
|
588
|
+
type,
|
|
589
|
+
requestId
|
|
590
|
+
}, "*");
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
function updateRecordButton() {
|
|
594
|
+
recordBtn.classList.toggle("active", recordingActive);
|
|
595
|
+
recordBtn.classList.toggle("busy", recordingBusy);
|
|
596
|
+
recordBtn.setAttribute("aria-pressed", String(recordingActive));
|
|
597
|
+
recordBtn.setAttribute("aria-busy", String(recordingBusy));
|
|
598
|
+
recordBtn.setAttribute("title", recordingActive ? "Stop demonstration recording" : "Start demonstration recording");
|
|
599
|
+
recordBtn.setAttribute("aria-label", recordingActive ? "Stop Tabwright demonstration recording" : "Start Tabwright demonstration recording");
|
|
600
|
+
recordBtn.innerHTML = recordingActive ? STOP_RECORDING_SVG : RECORD_SVG;
|
|
601
|
+
updatePinButton();
|
|
602
|
+
}
|
|
603
|
+
function updatePinButton() {
|
|
604
|
+
if (!pinBtn) return;
|
|
605
|
+
pinBtn.setAttribute("aria-label", recordingActive ? "Annotate an element in this recording" : "Pin element — click any element to copy inspection code for a tabwright -e call");
|
|
606
|
+
pinBtn.setAttribute("title", recordingActive ? "Annotate element in recording" : "Pin element (click to copy inspection code)");
|
|
607
|
+
}
|
|
608
|
+
function applyRecordingResult(result) {
|
|
609
|
+
if (result.success === false) throw new Error(result.error || "Recording request failed");
|
|
610
|
+
if (typeof result.isRecording === "boolean") {
|
|
611
|
+
const wasRecording = recordingActive;
|
|
612
|
+
recordingActive = result.isRecording;
|
|
613
|
+
if (recordingActive && !wasRecording) clearAnnotationBadges();
|
|
614
|
+
if (!recordingActive && wasRecording) {
|
|
615
|
+
closeAnnotationEditor();
|
|
616
|
+
clearAnnotationBadges();
|
|
617
|
+
setPinMode(false);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
function getOverlay() {
|
|
622
|
+
if (!overlayEl) {
|
|
623
|
+
const EDGE = "color-mix(in oklch, oklch(0.62 0.18 255) 80%, transparent)";
|
|
624
|
+
const FILL = "color-mix(in oklch, oklch(0.62 0.18 255) 8%, transparent)";
|
|
625
|
+
const container = document.createElement("div");
|
|
626
|
+
container.setAttribute("data-playwriter-overlay", "1");
|
|
627
|
+
container.style.cssText = [
|
|
628
|
+
"position:fixed",
|
|
629
|
+
"pointer-events:none",
|
|
630
|
+
"z-index:2147483646",
|
|
631
|
+
`background:${FILL}`,
|
|
632
|
+
"display:none"
|
|
633
|
+
].join(";");
|
|
634
|
+
const edgeTop = document.createElement("div");
|
|
635
|
+
edgeTop.style.cssText = `position:absolute;top:0;left:0;width:100%;height:1px;background:${EDGE};`;
|
|
636
|
+
const edgeRight = document.createElement("div");
|
|
637
|
+
edgeRight.style.cssText = `position:absolute;top:0;right:0;width:1px;height:100%;background:${EDGE};`;
|
|
638
|
+
const edgeBottom = document.createElement("div");
|
|
639
|
+
edgeBottom.style.cssText = `position:absolute;bottom:0;left:0;width:100%;height:1px;background:${EDGE};`;
|
|
640
|
+
const edgeLeft = document.createElement("div");
|
|
641
|
+
edgeLeft.style.cssText = `position:absolute;top:0;left:0;width:1px;height:100%;background:${EDGE};`;
|
|
642
|
+
container.appendChild(edgeTop);
|
|
643
|
+
container.appendChild(edgeRight);
|
|
644
|
+
container.appendChild(edgeBottom);
|
|
645
|
+
container.appendChild(edgeLeft);
|
|
646
|
+
document.documentElement.appendChild(container);
|
|
647
|
+
overlayEl = container;
|
|
648
|
+
}
|
|
649
|
+
return overlayEl;
|
|
650
|
+
}
|
|
651
|
+
function positionOverlay(target) {
|
|
652
|
+
const rect = target.getBoundingClientRect();
|
|
653
|
+
if (!rect.width && !rect.height) return;
|
|
654
|
+
const overlay = getOverlay();
|
|
655
|
+
overlay.style.display = "block";
|
|
656
|
+
overlay.style.top = rect.top + "px";
|
|
657
|
+
overlay.style.left = rect.left + "px";
|
|
658
|
+
overlay.style.width = rect.width + "px";
|
|
659
|
+
overlay.style.height = rect.height + "px";
|
|
660
|
+
}
|
|
661
|
+
function hideOverlay() {
|
|
662
|
+
if (overlayEl) overlayEl.style.display = "none";
|
|
663
|
+
}
|
|
664
|
+
function removeOverlay() {
|
|
665
|
+
if (overlayEl) {
|
|
666
|
+
overlayEl.remove();
|
|
667
|
+
overlayEl = null;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
function getTargetAt(x, y) {
|
|
671
|
+
return document.elementsFromPoint(x, y).find((el) => !el.hasAttribute("data-playwriter-overlay") && !el.hasAttribute("data-playwriter-toolbar") && el !== document.documentElement && el !== document.body) ?? null;
|
|
672
|
+
}
|
|
673
|
+
function isOverToolbar(e) {
|
|
674
|
+
return e.composedPath().some((node) => node === host);
|
|
675
|
+
}
|
|
676
|
+
function flashElement(el) {
|
|
677
|
+
const s = el.style;
|
|
678
|
+
if (!s) return;
|
|
679
|
+
const prevOutline = s.outline;
|
|
680
|
+
const prevOffset = s.outlineOffset;
|
|
681
|
+
s.outline = "1px solid #22c55e";
|
|
682
|
+
s.outlineOffset = "2px";
|
|
683
|
+
window.setTimeout(() => {
|
|
684
|
+
s.outline = prevOutline;
|
|
685
|
+
s.outlineOffset = prevOffset;
|
|
686
|
+
}, 350);
|
|
687
|
+
}
|
|
688
|
+
function copyText(text) {
|
|
689
|
+
navigator.clipboard.writeText(text).catch(() => {
|
|
690
|
+
try {
|
|
691
|
+
const ta = document.createElement("textarea");
|
|
692
|
+
ta.value = text;
|
|
693
|
+
ta.style.cssText = "position:fixed;top:0;left:0;opacity:0;pointer-events:none;";
|
|
694
|
+
document.body.appendChild(ta);
|
|
695
|
+
ta.focus();
|
|
696
|
+
ta.select();
|
|
697
|
+
document.execCommand("copy");
|
|
698
|
+
ta.remove();
|
|
699
|
+
} catch {}
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
function shellQuote(value) {
|
|
703
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
704
|
+
}
|
|
705
|
+
function replayCapabilityId(replayId) {
|
|
706
|
+
return `replay-${replayId.replace(/[^a-zA-Z0-9_-]+/g, "-").replace(/^-+|-+$/g, "").slice(-28) || "workflow"}`;
|
|
707
|
+
}
|
|
708
|
+
function replayMakeCommand(replayId) {
|
|
709
|
+
const capabilityId = replayCapabilityId(replayId);
|
|
710
|
+
return [
|
|
711
|
+
"tabwright replay make",
|
|
712
|
+
shellQuote(replayId),
|
|
713
|
+
shellQuote(capabilityId),
|
|
714
|
+
"--force",
|
|
715
|
+
"--goal",
|
|
716
|
+
shellQuote("<describe the batch task goal>")
|
|
717
|
+
].join(" ");
|
|
718
|
+
}
|
|
719
|
+
function replayRunCommand(replayId) {
|
|
720
|
+
const capabilityId = replayCapabilityId(replayId);
|
|
721
|
+
return [
|
|
722
|
+
"tabwright capability run",
|
|
723
|
+
shellQuote(capabilityId),
|
|
724
|
+
"--browser user",
|
|
725
|
+
"--force",
|
|
726
|
+
"--confirm",
|
|
727
|
+
shellQuote(capabilityId),
|
|
728
|
+
"--input-json",
|
|
729
|
+
shellQuote("{\"value\":\"...\"}"),
|
|
730
|
+
"--json"
|
|
731
|
+
].join(" ");
|
|
732
|
+
}
|
|
733
|
+
function replayAiHandoffText(result) {
|
|
734
|
+
const replayId = result.replayId || result.id;
|
|
735
|
+
if (!replayId) return null;
|
|
736
|
+
const path = result.replayPath || result.path;
|
|
737
|
+
return [
|
|
738
|
+
`这是 Tabwright replay id:${replayId}`,
|
|
739
|
+
`Recorded URL: ${window.location.href}`,
|
|
740
|
+
path ? `Recording path: ${path}` : "",
|
|
741
|
+
result.replayEventCount !== void 0 ? `Events: ${result.replayEventCount}` : "",
|
|
742
|
+
"",
|
|
743
|
+
"我的目标是:<描述要 AI 基于这次录制重复/批量完成什么>",
|
|
744
|
+
"",
|
|
745
|
+
"请先编译成 capability:",
|
|
746
|
+
replayMakeCommand(replayId),
|
|
747
|
+
"",
|
|
748
|
+
"这是写操作。先暂停并取得用户明确确认,确认后才可执行:",
|
|
749
|
+
replayRunCommand(replayId),
|
|
750
|
+
"",
|
|
751
|
+
"如果返回 needs_ai,请根据上下文修改脚本后继续跑。"
|
|
752
|
+
].filter((line) => {
|
|
753
|
+
return line.length > 0;
|
|
754
|
+
}).join("\n");
|
|
755
|
+
}
|
|
756
|
+
function normalizeText(value) {
|
|
757
|
+
return value.replace(/\s+/g, " ").trim();
|
|
758
|
+
}
|
|
759
|
+
function uniqueStrings(values) {
|
|
760
|
+
return Array.from(new Set(values.filter((value) => {
|
|
761
|
+
return value.length > 0;
|
|
762
|
+
})));
|
|
763
|
+
}
|
|
764
|
+
function attrSelector(name, value) {
|
|
765
|
+
return `[${name}=${JSON.stringify(value)}]`;
|
|
766
|
+
}
|
|
767
|
+
function isSimpleCssIdentifier(value) {
|
|
768
|
+
return /^[A-Za-z_-][A-Za-z0-9_-]*$/.test(value);
|
|
769
|
+
}
|
|
770
|
+
function elementText(target) {
|
|
771
|
+
const text = target instanceof HTMLElement ? normalizeText(target.innerText || target.textContent || "") : normalizeText(target.textContent || "");
|
|
772
|
+
if (!text || text.length > 180) return;
|
|
773
|
+
return text;
|
|
774
|
+
}
|
|
775
|
+
function labelForTarget(target) {
|
|
776
|
+
const direct = target.getAttribute("aria-label") || target.getAttribute("title") || target.getAttribute("placeholder") || target.getAttribute("name") || "";
|
|
777
|
+
if (direct.trim()) return normalizeText(direct);
|
|
778
|
+
const id = target.getAttribute("id");
|
|
779
|
+
if (id) {
|
|
780
|
+
const labelText = normalizeText(document.querySelector(`label[for=${JSON.stringify(id)}]`)?.textContent || "");
|
|
781
|
+
if (labelText) return labelText;
|
|
782
|
+
}
|
|
783
|
+
const closestLabelText = normalizeText(target.closest("label")?.textContent || "");
|
|
784
|
+
if (closestLabelText) return closestLabelText;
|
|
785
|
+
return elementText(target);
|
|
786
|
+
}
|
|
787
|
+
function selectorHintsForTarget(target) {
|
|
788
|
+
const tagName = target.tagName.toLowerCase();
|
|
789
|
+
const classHints = Array.from(target.classList).filter((className) => {
|
|
790
|
+
return isSimpleCssIdentifier(className) && className.length <= 80;
|
|
791
|
+
}).slice(0, 3).map((className) => {
|
|
792
|
+
return `${tagName}.${className}`;
|
|
793
|
+
});
|
|
794
|
+
const id = target.getAttribute("id") || "";
|
|
795
|
+
return uniqueStrings([
|
|
796
|
+
id ? isSimpleCssIdentifier(id) ? `#${id}` : attrSelector("id", id) : "",
|
|
797
|
+
target.getAttribute("data-testid") ? attrSelector("data-testid", target.getAttribute("data-testid") || "") : "",
|
|
798
|
+
target.getAttribute("data-test") ? attrSelector("data-test", target.getAttribute("data-test") || "") : "",
|
|
799
|
+
target.getAttribute("name") ? `${tagName}${attrSelector("name", target.getAttribute("name") || "")}` : "",
|
|
800
|
+
target.getAttribute("placeholder") ? `${tagName}${attrSelector("placeholder", target.getAttribute("placeholder") || "")}` : "",
|
|
801
|
+
target.getAttribute("aria-label") ? `${tagName}${attrSelector("aria-label", target.getAttribute("aria-label") || "")}` : "",
|
|
802
|
+
...classHints
|
|
803
|
+
]);
|
|
804
|
+
}
|
|
805
|
+
function targetLabelPreview(target) {
|
|
806
|
+
const label = labelForTarget(target);
|
|
807
|
+
if (label) return label.length > 44 ? `${label.slice(0, 43)}…` : label;
|
|
808
|
+
const selector = selectorHintsForTarget(target)[0];
|
|
809
|
+
if (selector) return selector;
|
|
810
|
+
return target.tagName.toLowerCase();
|
|
811
|
+
}
|
|
812
|
+
function summarizeTarget(target) {
|
|
813
|
+
const rect = target.getBoundingClientRect();
|
|
814
|
+
return {
|
|
815
|
+
tagName: target.tagName.toLowerCase(),
|
|
816
|
+
label: labelForTarget(target),
|
|
817
|
+
text: elementText(target),
|
|
818
|
+
role: target.getAttribute("role") || void 0,
|
|
819
|
+
name: target.getAttribute("name") || void 0,
|
|
820
|
+
placeholder: target.getAttribute("placeholder") || void 0,
|
|
821
|
+
selectorHints: selectorHintsForTarget(target),
|
|
822
|
+
rect: {
|
|
823
|
+
x: Math.round(rect.left),
|
|
824
|
+
y: Math.round(rect.top),
|
|
825
|
+
width: Math.round(rect.width),
|
|
826
|
+
height: Math.round(rect.height)
|
|
827
|
+
}
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
function getAnnotationLayer() {
|
|
831
|
+
if (!annotationLayerEl) {
|
|
832
|
+
const layer = document.createElement("div");
|
|
833
|
+
layer.setAttribute("data-playwriter-annotation-layer", "1");
|
|
834
|
+
layer.style.cssText = [
|
|
835
|
+
"position:fixed",
|
|
836
|
+
"inset:0",
|
|
837
|
+
"z-index:2147483645",
|
|
838
|
+
"pointer-events:none",
|
|
839
|
+
"font-family:ui-sans-serif,-apple-system,BlinkMacSystemFont,\"Segoe UI\",sans-serif"
|
|
840
|
+
].join(";");
|
|
841
|
+
document.documentElement.appendChild(layer);
|
|
842
|
+
annotationLayerEl = layer;
|
|
843
|
+
}
|
|
844
|
+
return annotationLayerEl;
|
|
845
|
+
}
|
|
846
|
+
function createAnnotationRegion(options) {
|
|
847
|
+
const region = document.createElement("div");
|
|
848
|
+
if (options.preview) region.setAttribute("data-playwriter-annotation-preview", "1");
|
|
849
|
+
else if (typeof options.index === "number") region.setAttribute("data-playwriter-annotation-region", String(options.index));
|
|
850
|
+
region.style.cssText = [
|
|
851
|
+
"position:fixed",
|
|
852
|
+
`border:1px solid ${options.preview ? "rgba(99,102,241,0.46)" : "rgba(99,102,241,0.32)"}`,
|
|
853
|
+
`background:${options.preview ? "rgba(99,102,241,0.075)" : "rgba(99,102,241,0.045)"}`,
|
|
854
|
+
`box-shadow:inset 0 0 0 1px rgba(255,255,255,0.45),0 0 0 2px ${options.preview ? "rgba(99,102,241,0.095)" : "rgba(99,102,241,0.055)"}`,
|
|
855
|
+
"border-radius:6px",
|
|
856
|
+
"pointer-events:none"
|
|
857
|
+
].join(";");
|
|
858
|
+
return region;
|
|
859
|
+
}
|
|
860
|
+
function positionAnnotationRegion(options) {
|
|
861
|
+
const rect = options.target.getBoundingClientRect();
|
|
862
|
+
const visible = rect.width > 0 && rect.height > 0 && rect.bottom >= 0 && rect.right >= 0;
|
|
863
|
+
options.region.style.display = visible ? "block" : "none";
|
|
864
|
+
if (!visible) return false;
|
|
865
|
+
const regionLeft = Math.max(0, rect.left);
|
|
866
|
+
const regionTop = Math.max(0, rect.top);
|
|
867
|
+
options.region.style.left = `${regionLeft}px`;
|
|
868
|
+
options.region.style.top = `${regionTop}px`;
|
|
869
|
+
options.region.style.width = `${Math.max(1, Math.min(rect.width, window.innerWidth - regionLeft))}px`;
|
|
870
|
+
options.region.style.height = `${Math.max(1, Math.min(rect.height, window.innerHeight - regionTop))}px`;
|
|
871
|
+
return true;
|
|
872
|
+
}
|
|
873
|
+
function updateAnnotationPreview() {
|
|
874
|
+
if (!annotationPreview) return;
|
|
875
|
+
positionAnnotationRegion(annotationPreview);
|
|
876
|
+
}
|
|
877
|
+
function removeAnnotationPreview() {
|
|
878
|
+
if (!annotationPreview) return;
|
|
879
|
+
annotationPreview.region.remove();
|
|
880
|
+
annotationPreview = null;
|
|
881
|
+
if (annotationMarkers.length === 0) removeAnnotationBadgeLayoutListeners();
|
|
882
|
+
}
|
|
883
|
+
function showAnnotationPreview(target) {
|
|
884
|
+
removeAnnotationPreview();
|
|
885
|
+
const region = createAnnotationRegion({ preview: true });
|
|
886
|
+
getAnnotationLayer().appendChild(region);
|
|
887
|
+
annotationPreview = {
|
|
888
|
+
target,
|
|
889
|
+
region
|
|
890
|
+
};
|
|
891
|
+
ensureAnnotationBadgeLayoutListeners();
|
|
892
|
+
updateAnnotationPreview();
|
|
893
|
+
}
|
|
894
|
+
function clampAnnotationPosition(options) {
|
|
895
|
+
return Math.max(options.min, Math.min(options.value, options.max));
|
|
896
|
+
}
|
|
897
|
+
function chooseAnnotationPlacement(options) {
|
|
898
|
+
const placements = [
|
|
899
|
+
{
|
|
900
|
+
placement: "right",
|
|
901
|
+
fits: window.innerWidth - options.rect.right - options.gap >= options.bubbleWidth,
|
|
902
|
+
score: window.innerWidth - options.rect.right
|
|
903
|
+
},
|
|
904
|
+
{
|
|
905
|
+
placement: "left",
|
|
906
|
+
fits: options.rect.left - options.gap >= options.bubbleWidth,
|
|
907
|
+
score: options.rect.left
|
|
908
|
+
},
|
|
909
|
+
{
|
|
910
|
+
placement: "bottom",
|
|
911
|
+
fits: window.innerHeight - options.rect.bottom - options.gap >= options.bubbleHeight,
|
|
912
|
+
score: window.innerHeight - options.rect.bottom
|
|
913
|
+
},
|
|
914
|
+
{
|
|
915
|
+
placement: "top",
|
|
916
|
+
fits: options.rect.top - options.gap >= options.bubbleHeight,
|
|
917
|
+
score: options.rect.top
|
|
918
|
+
}
|
|
919
|
+
];
|
|
920
|
+
const firstFit = placements.find((placement) => {
|
|
921
|
+
return placement.fits;
|
|
922
|
+
});
|
|
923
|
+
if (firstFit) return firstFit.placement;
|
|
924
|
+
return placements.reduce((best, placement) => {
|
|
925
|
+
return placement.score > best.score ? placement : best;
|
|
926
|
+
}).placement;
|
|
927
|
+
}
|
|
928
|
+
function updateAnnotationBadges() {
|
|
929
|
+
updateAnnotationPreview();
|
|
930
|
+
annotationMarkers.forEach(({ target, region, label }) => {
|
|
931
|
+
const rect = target.getBoundingClientRect();
|
|
932
|
+
const visible = positionAnnotationRegion({
|
|
933
|
+
target,
|
|
934
|
+
region
|
|
935
|
+
});
|
|
936
|
+
label.style.display = visible ? "flex" : "none";
|
|
937
|
+
if (!visible) return;
|
|
938
|
+
const padding = 8;
|
|
939
|
+
const gap = 12;
|
|
940
|
+
const bubbleWidth = label.offsetWidth || 240;
|
|
941
|
+
const bubbleHeight = label.offsetHeight || 34;
|
|
942
|
+
const placement = chooseAnnotationPlacement({
|
|
943
|
+
rect,
|
|
944
|
+
bubbleWidth,
|
|
945
|
+
bubbleHeight,
|
|
946
|
+
gap
|
|
947
|
+
});
|
|
948
|
+
const targetCenterX = rect.left + rect.width / 2;
|
|
949
|
+
const targetCenterY = rect.top + rect.height / 2;
|
|
950
|
+
const maxLeft = Math.max(padding, window.innerWidth - bubbleWidth - padding);
|
|
951
|
+
const maxTop = Math.max(padding, window.innerHeight - bubbleHeight - padding);
|
|
952
|
+
const position = (() => {
|
|
953
|
+
if (placement === "right") return {
|
|
954
|
+
left: clampAnnotationPosition({
|
|
955
|
+
value: rect.right + gap,
|
|
956
|
+
min: padding,
|
|
957
|
+
max: maxLeft
|
|
958
|
+
}),
|
|
959
|
+
top: clampAnnotationPosition({
|
|
960
|
+
value: targetCenterY - bubbleHeight / 2,
|
|
961
|
+
min: padding,
|
|
962
|
+
max: maxTop
|
|
963
|
+
})
|
|
964
|
+
};
|
|
965
|
+
if (placement === "left") return {
|
|
966
|
+
left: clampAnnotationPosition({
|
|
967
|
+
value: rect.left - gap - bubbleWidth,
|
|
968
|
+
min: padding,
|
|
969
|
+
max: maxLeft
|
|
970
|
+
}),
|
|
971
|
+
top: clampAnnotationPosition({
|
|
972
|
+
value: targetCenterY - bubbleHeight / 2,
|
|
973
|
+
min: padding,
|
|
974
|
+
max: maxTop
|
|
975
|
+
})
|
|
976
|
+
};
|
|
977
|
+
if (placement === "bottom") return {
|
|
978
|
+
left: clampAnnotationPosition({
|
|
979
|
+
value: targetCenterX - bubbleWidth / 2,
|
|
980
|
+
min: padding,
|
|
981
|
+
max: maxLeft
|
|
982
|
+
}),
|
|
983
|
+
top: clampAnnotationPosition({
|
|
984
|
+
value: rect.bottom + gap,
|
|
985
|
+
min: padding,
|
|
986
|
+
max: maxTop
|
|
987
|
+
})
|
|
988
|
+
};
|
|
989
|
+
return {
|
|
990
|
+
left: clampAnnotationPosition({
|
|
991
|
+
value: targetCenterX - bubbleWidth / 2,
|
|
992
|
+
min: padding,
|
|
993
|
+
max: maxLeft
|
|
994
|
+
}),
|
|
995
|
+
top: clampAnnotationPosition({
|
|
996
|
+
value: rect.top - gap - bubbleHeight,
|
|
997
|
+
min: padding,
|
|
998
|
+
max: maxTop
|
|
999
|
+
})
|
|
1000
|
+
};
|
|
1001
|
+
})();
|
|
1002
|
+
label.style.left = `${position.left}px`;
|
|
1003
|
+
label.style.top = `${position.top}px`;
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
function ensureAnnotationBadgeLayoutListeners() {
|
|
1007
|
+
if (annotationLayoutListening) return;
|
|
1008
|
+
annotationLayoutListening = true;
|
|
1009
|
+
window.addEventListener("resize", updateAnnotationBadges, true);
|
|
1010
|
+
document.addEventListener("scroll", updateAnnotationBadges, true);
|
|
1011
|
+
}
|
|
1012
|
+
function removeAnnotationBadgeLayoutListeners() {
|
|
1013
|
+
if (!annotationLayoutListening) return;
|
|
1014
|
+
annotationLayoutListening = false;
|
|
1015
|
+
window.removeEventListener("resize", updateAnnotationBadges, true);
|
|
1016
|
+
document.removeEventListener("scroll", updateAnnotationBadges, true);
|
|
1017
|
+
}
|
|
1018
|
+
function removeAnnotationMarker(options) {
|
|
1019
|
+
const markerIndex = annotationMarkers.findIndex((marker) => {
|
|
1020
|
+
return marker.id === options.id;
|
|
1021
|
+
});
|
|
1022
|
+
if (markerIndex < 0) return;
|
|
1023
|
+
const marker = annotationMarkers[markerIndex];
|
|
1024
|
+
if (!marker) return;
|
|
1025
|
+
annotationMarkers.splice(markerIndex, 1);
|
|
1026
|
+
marker.region.remove();
|
|
1027
|
+
marker.label.remove();
|
|
1028
|
+
if (options.emitDelete) postAnnotationDeleteEvent({ id: options.id });
|
|
1029
|
+
if (annotationMarkers.length === 0 && !annotationPreview) {
|
|
1030
|
+
removeAnnotationBadgeLayoutListeners();
|
|
1031
|
+
annotationLayerEl?.remove();
|
|
1032
|
+
annotationLayerEl = null;
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
function createAnnotationBadge(options) {
|
|
1036
|
+
const region = createAnnotationRegion({
|
|
1037
|
+
index: options.index,
|
|
1038
|
+
preview: false
|
|
1039
|
+
});
|
|
1040
|
+
const label = document.createElement("div");
|
|
1041
|
+
label.setAttribute("data-playwriter-annotation-badge", String(options.index));
|
|
1042
|
+
label.setAttribute("data-playwriter-annotation-id", options.id);
|
|
1043
|
+
label.setAttribute("data-playwriter-annotation-text", options.text);
|
|
1044
|
+
label.title = options.text;
|
|
1045
|
+
label.style.cssText = [
|
|
1046
|
+
"position:fixed",
|
|
1047
|
+
"align-items:center",
|
|
1048
|
+
"gap:6px",
|
|
1049
|
+
"max-width:min(300px,calc(100vw - 16px))",
|
|
1050
|
+
"min-height:28px",
|
|
1051
|
+
"padding:5px 10px 5px 6px",
|
|
1052
|
+
"border:1px solid rgba(99,102,241,0.2)",
|
|
1053
|
+
"border-radius:10px",
|
|
1054
|
+
"background:rgba(255,255,255,0.9)",
|
|
1055
|
+
"color:#312e81",
|
|
1056
|
+
"font-size:12px",
|
|
1057
|
+
"font-weight:560",
|
|
1058
|
+
"line-height:1.25",
|
|
1059
|
+
"box-shadow:0 10px 28px rgba(15,23,42,0.13),0 1px 3px rgba(49,46,129,0.1)",
|
|
1060
|
+
"backdrop-filter:blur(10px)",
|
|
1061
|
+
"opacity:0.92",
|
|
1062
|
+
"pointer-events:auto",
|
|
1063
|
+
"cursor:default"
|
|
1064
|
+
].join(";");
|
|
1065
|
+
const number = document.createElement("span");
|
|
1066
|
+
number.textContent = String(options.index);
|
|
1067
|
+
number.style.cssText = [
|
|
1068
|
+
"display:inline-flex",
|
|
1069
|
+
"align-items:center",
|
|
1070
|
+
"justify-content:center",
|
|
1071
|
+
"width:17px",
|
|
1072
|
+
"height:17px",
|
|
1073
|
+
"border-radius:5px",
|
|
1074
|
+
"background:rgba(79,70,229,0.9)",
|
|
1075
|
+
"color:#fff",
|
|
1076
|
+
"font-size:10px",
|
|
1077
|
+
"font-weight:750",
|
|
1078
|
+
"line-height:1",
|
|
1079
|
+
"flex:0 0 auto"
|
|
1080
|
+
].join(";");
|
|
1081
|
+
const text = document.createElement("span");
|
|
1082
|
+
text.textContent = options.text;
|
|
1083
|
+
text.style.cssText = [
|
|
1084
|
+
"display:block",
|
|
1085
|
+
"overflow:hidden",
|
|
1086
|
+
"text-overflow:ellipsis",
|
|
1087
|
+
"white-space:nowrap",
|
|
1088
|
+
"letter-spacing:0"
|
|
1089
|
+
].join(";");
|
|
1090
|
+
const deleteButton = document.createElement("button");
|
|
1091
|
+
deleteButton.type = "button";
|
|
1092
|
+
deleteButton.textContent = "×";
|
|
1093
|
+
deleteButton.setAttribute("aria-label", `Delete annotation ${options.index}`);
|
|
1094
|
+
deleteButton.title = "Delete annotation";
|
|
1095
|
+
deleteButton.style.cssText = [
|
|
1096
|
+
"display:inline-flex",
|
|
1097
|
+
"align-items:center",
|
|
1098
|
+
"justify-content:center",
|
|
1099
|
+
"width:18px",
|
|
1100
|
+
"height:18px",
|
|
1101
|
+
"margin-left:2px",
|
|
1102
|
+
"border:0",
|
|
1103
|
+
"border-radius:6px",
|
|
1104
|
+
"background:rgba(49,46,129,0.08)",
|
|
1105
|
+
"color:#4338ca",
|
|
1106
|
+
"font-size:14px",
|
|
1107
|
+
"font-weight:700",
|
|
1108
|
+
"line-height:1",
|
|
1109
|
+
"opacity:0",
|
|
1110
|
+
"cursor:pointer",
|
|
1111
|
+
"transition:opacity 0.12s ease,background 0.12s ease,color 0.12s ease",
|
|
1112
|
+
"pointer-events:auto",
|
|
1113
|
+
"flex:0 0 auto"
|
|
1114
|
+
].join(";");
|
|
1115
|
+
label.addEventListener("mouseenter", () => {
|
|
1116
|
+
deleteButton.style.opacity = "1";
|
|
1117
|
+
});
|
|
1118
|
+
label.addEventListener("mouseleave", () => {
|
|
1119
|
+
deleteButton.style.opacity = "0";
|
|
1120
|
+
});
|
|
1121
|
+
label.addEventListener("mousedown", (event) => {
|
|
1122
|
+
event.preventDefault();
|
|
1123
|
+
event.stopPropagation();
|
|
1124
|
+
});
|
|
1125
|
+
label.addEventListener("click", (event) => {
|
|
1126
|
+
event.preventDefault();
|
|
1127
|
+
event.stopPropagation();
|
|
1128
|
+
});
|
|
1129
|
+
deleteButton.addEventListener("mouseenter", () => {
|
|
1130
|
+
deleteButton.style.background = "rgba(239,68,68,0.12)";
|
|
1131
|
+
deleteButton.style.color = "#dc2626";
|
|
1132
|
+
});
|
|
1133
|
+
deleteButton.addEventListener("mouseleave", () => {
|
|
1134
|
+
deleteButton.style.background = "rgba(49,46,129,0.08)";
|
|
1135
|
+
deleteButton.style.color = "#4338ca";
|
|
1136
|
+
});
|
|
1137
|
+
deleteButton.addEventListener("mousedown", (event) => {
|
|
1138
|
+
event.preventDefault();
|
|
1139
|
+
event.stopPropagation();
|
|
1140
|
+
});
|
|
1141
|
+
deleteButton.addEventListener("click", (event) => {
|
|
1142
|
+
event.preventDefault();
|
|
1143
|
+
event.stopPropagation();
|
|
1144
|
+
removeAnnotationMarker({
|
|
1145
|
+
id: options.id,
|
|
1146
|
+
emitDelete: true
|
|
1147
|
+
});
|
|
1148
|
+
showToast("Annotation deleted", label.getBoundingClientRect());
|
|
1149
|
+
});
|
|
1150
|
+
label.appendChild(number);
|
|
1151
|
+
label.appendChild(text);
|
|
1152
|
+
label.appendChild(deleteButton);
|
|
1153
|
+
const layer = getAnnotationLayer();
|
|
1154
|
+
layer.appendChild(region);
|
|
1155
|
+
layer.appendChild(label);
|
|
1156
|
+
annotationMarkers.push({
|
|
1157
|
+
id: options.id,
|
|
1158
|
+
target: options.target,
|
|
1159
|
+
region,
|
|
1160
|
+
label
|
|
1161
|
+
});
|
|
1162
|
+
ensureAnnotationBadgeLayoutListeners();
|
|
1163
|
+
updateAnnotationBadges();
|
|
1164
|
+
}
|
|
1165
|
+
function clearAnnotationBadges() {
|
|
1166
|
+
removeAnnotationPreview();
|
|
1167
|
+
annotationMarkers.splice(0).forEach(({ region, label }) => {
|
|
1168
|
+
region.remove();
|
|
1169
|
+
label.remove();
|
|
1170
|
+
});
|
|
1171
|
+
removeAnnotationBadgeLayoutListeners();
|
|
1172
|
+
annotationLayerEl?.remove();
|
|
1173
|
+
annotationLayerEl = null;
|
|
1174
|
+
annotationCount = 0;
|
|
1175
|
+
}
|
|
1176
|
+
function closeAnnotationEditor() {
|
|
1177
|
+
if (annotationEditorEl) {
|
|
1178
|
+
annotationEditorEl.remove();
|
|
1179
|
+
annotationEditorEl = null;
|
|
1180
|
+
}
|
|
1181
|
+
removeAnnotationPreview();
|
|
1182
|
+
}
|
|
1183
|
+
function postAnnotationEvent(options) {
|
|
1184
|
+
const annotation = {
|
|
1185
|
+
schemaVersion: 1,
|
|
1186
|
+
id: options.id,
|
|
1187
|
+
text: options.text,
|
|
1188
|
+
url: location.href,
|
|
1189
|
+
timestamp: Date.now(),
|
|
1190
|
+
target: summarizeTarget(options.target)
|
|
1191
|
+
};
|
|
1192
|
+
window.postMessage({
|
|
1193
|
+
source: "playwriter-toolbar",
|
|
1194
|
+
type: "recording-annotation",
|
|
1195
|
+
annotation
|
|
1196
|
+
}, "*");
|
|
1197
|
+
}
|
|
1198
|
+
function postAnnotationDeleteEvent(options) {
|
|
1199
|
+
window.postMessage({
|
|
1200
|
+
source: "playwriter-toolbar",
|
|
1201
|
+
type: "recording-annotation-delete",
|
|
1202
|
+
annotation: {
|
|
1203
|
+
schemaVersion: 1,
|
|
1204
|
+
id: options.id,
|
|
1205
|
+
url: location.href,
|
|
1206
|
+
timestamp: Date.now()
|
|
1207
|
+
}
|
|
1208
|
+
}, "*");
|
|
1209
|
+
}
|
|
1210
|
+
function showAnnotationEditor(target, anchorRect) {
|
|
1211
|
+
closeAnnotationEditor();
|
|
1212
|
+
showAnnotationPreview(target);
|
|
1213
|
+
const popover = document.createElement("div");
|
|
1214
|
+
popover.className = "annotation-popover";
|
|
1215
|
+
popover.addEventListener("click", (event) => {
|
|
1216
|
+
event.stopPropagation();
|
|
1217
|
+
});
|
|
1218
|
+
popover.addEventListener("mousedown", (event) => {
|
|
1219
|
+
event.stopPropagation();
|
|
1220
|
+
});
|
|
1221
|
+
const titleRow = document.createElement("div");
|
|
1222
|
+
titleRow.className = "annotation-title";
|
|
1223
|
+
const title = document.createElement("span");
|
|
1224
|
+
title.textContent = "Add annotation";
|
|
1225
|
+
const targetText = document.createElement("span");
|
|
1226
|
+
targetText.className = "annotation-target";
|
|
1227
|
+
targetText.textContent = targetLabelPreview(target);
|
|
1228
|
+
const input = document.createElement("textarea");
|
|
1229
|
+
input.className = "annotation-input";
|
|
1230
|
+
input.placeholder = "What should AI remember here?";
|
|
1231
|
+
const actions = document.createElement("div");
|
|
1232
|
+
actions.className = "annotation-actions";
|
|
1233
|
+
const cancel = document.createElement("button");
|
|
1234
|
+
cancel.type = "button";
|
|
1235
|
+
cancel.className = "annotation-action";
|
|
1236
|
+
cancel.textContent = "Cancel";
|
|
1237
|
+
const save = document.createElement("button");
|
|
1238
|
+
save.type = "button";
|
|
1239
|
+
save.className = "annotation-action primary";
|
|
1240
|
+
save.textContent = "Save";
|
|
1241
|
+
function saveAnnotation() {
|
|
1242
|
+
const text = normalizeText(input.value);
|
|
1243
|
+
if (!text) {
|
|
1244
|
+
input.focus();
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
annotationCount += 1;
|
|
1248
|
+
const id = `ann-${Date.now()}-${annotationCount}`;
|
|
1249
|
+
createAnnotationBadge({
|
|
1250
|
+
id,
|
|
1251
|
+
target,
|
|
1252
|
+
text,
|
|
1253
|
+
index: annotationCount
|
|
1254
|
+
});
|
|
1255
|
+
postAnnotationEvent({
|
|
1256
|
+
id,
|
|
1257
|
+
target,
|
|
1258
|
+
text,
|
|
1259
|
+
index: annotationCount
|
|
1260
|
+
});
|
|
1261
|
+
closeAnnotationEditor();
|
|
1262
|
+
showToast("Annotation saved", anchorRect);
|
|
1263
|
+
}
|
|
1264
|
+
cancel.addEventListener("click", () => {
|
|
1265
|
+
closeAnnotationEditor();
|
|
1266
|
+
});
|
|
1267
|
+
save.addEventListener("click", () => {
|
|
1268
|
+
saveAnnotation();
|
|
1269
|
+
});
|
|
1270
|
+
input.addEventListener("keydown", (event) => {
|
|
1271
|
+
if (event.key === "Escape") {
|
|
1272
|
+
event.preventDefault();
|
|
1273
|
+
closeAnnotationEditor();
|
|
1274
|
+
return;
|
|
1275
|
+
}
|
|
1276
|
+
if (event.key === "Enter" && (event.metaKey || event.ctrlKey)) {
|
|
1277
|
+
event.preventDefault();
|
|
1278
|
+
saveAnnotation();
|
|
1279
|
+
}
|
|
1280
|
+
});
|
|
1281
|
+
titleRow.appendChild(title);
|
|
1282
|
+
titleRow.appendChild(targetText);
|
|
1283
|
+
actions.appendChild(cancel);
|
|
1284
|
+
actions.appendChild(save);
|
|
1285
|
+
popover.appendChild(titleRow);
|
|
1286
|
+
popover.appendChild(input);
|
|
1287
|
+
popover.appendChild(actions);
|
|
1288
|
+
const GAP = 10;
|
|
1289
|
+
const preferredLeft = anchorRect.left;
|
|
1290
|
+
const preferredTop = anchorRect.bottom + GAP;
|
|
1291
|
+
const width = Math.min(360, window.innerWidth - 24);
|
|
1292
|
+
const left = Math.max(12, Math.min(preferredLeft, window.innerWidth - width - 12));
|
|
1293
|
+
const top = preferredTop + 160 < window.innerHeight ? preferredTop : Math.max(12, anchorRect.top - 170);
|
|
1294
|
+
popover.style.left = `${left}px`;
|
|
1295
|
+
popover.style.top = `${top}px`;
|
|
1296
|
+
shadow.appendChild(popover);
|
|
1297
|
+
annotationEditorEl = popover;
|
|
1298
|
+
window.setTimeout(() => {
|
|
1299
|
+
input.focus();
|
|
1300
|
+
}, 0);
|
|
1301
|
+
}
|
|
1302
|
+
function allocatePinName() {
|
|
1303
|
+
const shared = window.__playwriterPinCount;
|
|
1304
|
+
if (typeof shared === "number" && shared > pinCount) pinCount = shared;
|
|
1305
|
+
pinCount++;
|
|
1306
|
+
window.__playwriterPinCount = pinCount;
|
|
1307
|
+
return `playwriterPinnedElem${pinCount}`;
|
|
1308
|
+
}
|
|
1309
|
+
function onMouseMove(e) {
|
|
1310
|
+
if (isOverToolbar(e)) {
|
|
1311
|
+
hideOverlay();
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1314
|
+
const target = getTargetAt(e.clientX, e.clientY);
|
|
1315
|
+
if (target) positionOverlay(target);
|
|
1316
|
+
else hideOverlay();
|
|
1317
|
+
}
|
|
1318
|
+
function buildInspectionCode(n, url) {
|
|
1319
|
+
return `inspectPinnedElement(${JSON.stringify(url).replace(/'/g, "\\u0027")},"globalThis.playwriterPinnedElem${n}")`;
|
|
1320
|
+
}
|
|
1321
|
+
function onClick(e) {
|
|
1322
|
+
if (isOverToolbar(e)) return;
|
|
1323
|
+
e.preventDefault();
|
|
1324
|
+
e.stopImmediatePropagation();
|
|
1325
|
+
const target = getTargetAt(e.clientX, e.clientY);
|
|
1326
|
+
if (!target) return;
|
|
1327
|
+
if (recordingActive) {
|
|
1328
|
+
const rect = target.getBoundingClientRect();
|
|
1329
|
+
setPinMode(false);
|
|
1330
|
+
showAnnotationEditor(target, rect);
|
|
1331
|
+
return;
|
|
1332
|
+
}
|
|
1333
|
+
const name = allocatePinName();
|
|
1334
|
+
const n = pinCount;
|
|
1335
|
+
window[name] = target;
|
|
1336
|
+
flashElement(target);
|
|
1337
|
+
const url = location.href;
|
|
1338
|
+
copyText("tabwright -e '" + buildInspectionCode(n, url) + "'");
|
|
1339
|
+
showToast("Copied Tabwright element reference, use it in your agent prompt", target.getBoundingClientRect());
|
|
1340
|
+
setPinMode(false);
|
|
1341
|
+
}
|
|
1342
|
+
function onKeyDown(e) {
|
|
1343
|
+
if (e.key === "Escape") setPinMode(false);
|
|
1344
|
+
}
|
|
1345
|
+
function setPinMode(on) {
|
|
1346
|
+
pinModeActive = on;
|
|
1347
|
+
pinBtn.classList.toggle("active", on);
|
|
1348
|
+
if (on) {
|
|
1349
|
+
document.documentElement.style.cursor = "crosshair";
|
|
1350
|
+
getOverlay();
|
|
1351
|
+
document.addEventListener("mousemove", onMouseMove, {
|
|
1352
|
+
capture: true,
|
|
1353
|
+
passive: true
|
|
1354
|
+
});
|
|
1355
|
+
document.addEventListener("click", onClick, true);
|
|
1356
|
+
document.addEventListener("keydown", onKeyDown, true);
|
|
1357
|
+
} else {
|
|
1358
|
+
document.documentElement.style.cursor = "";
|
|
1359
|
+
hideOverlay();
|
|
1360
|
+
document.removeEventListener("mousemove", onMouseMove, true);
|
|
1361
|
+
document.removeEventListener("click", onClick, true);
|
|
1362
|
+
document.removeEventListener("keydown", onKeyDown, true);
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
const CLIPBOARD_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" viewBox="0 0 424 424" aria-hidden="true"><path d="M 0 212 C 0 112.063 0 62.095 31.037 31.037 C 62.116 0 112.063 0 212 0 C 311.937 0 361.905 0 392.942 31.037 C 424 62.116 424 112.063 424 212 C 424 311.937 424 361.905 392.942 392.942 C 361.926 424 311.937 424 212 424 C 112.063 424 62.095 424 31.037 392.942 C 0 361.926 0 311.937 0 212" fill="currentColor"/><path class="logo-inner" d="M 225.732 260.521 L 277.905 312.673 C 283.311 318.1 286.003 320.793 289.014 322.043 C 293.042 323.718 297.557 323.718 301.585 322.043 C 304.596 320.793 307.309 318.1 312.694 312.694 C 318.1 307.288 320.793 304.596 322.043 301.585 C 323.722 297.563 323.722 293.036 322.043 289.014 C 320.793 286.003 318.1 283.29 312.694 277.905 L 260.521 225.732 L 276.442 209.789 C 292.766 193.465 300.907 185.325 298.999 176.548 C 297.07 167.792 286.237 163.785 264.591 155.814 L 192.384 129.208 C 149.2 113.308 127.618 105.358 116.488 116.488 C 105.358 127.618 113.308 149.2 129.208 192.384 L 155.814 264.591 C 163.785 286.237 167.792 297.07 176.548 298.999 C 185.303 300.928 193.465 292.766 209.789 276.442 Z" fill="white"/></svg>`;
|
|
1366
|
+
const RECORD_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><circle cx="12" cy="12" r="7"/></svg>`;
|
|
1367
|
+
const STOP_RECORDING_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><rect x="7" y="7" width="10" height="10" rx="1.5"/></svg>`;
|
|
1368
|
+
const CLOSE_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>`;
|
|
1369
|
+
pinBtn = document.createElement("button");
|
|
1370
|
+
pinBtn.className = "btn";
|
|
1371
|
+
pinBtn.setAttribute("aria-label", "Pin element — click any element to copy inspection code for a tabwright -e call");
|
|
1372
|
+
pinBtn.setAttribute("title", "Pin element (click to copy inspection code)");
|
|
1373
|
+
pinBtn.innerHTML = CLIPBOARD_SVG;
|
|
1374
|
+
pinBtn.addEventListener("click", (e) => {
|
|
1375
|
+
e.stopPropagation();
|
|
1376
|
+
setPinMode(!pinModeActive);
|
|
1377
|
+
});
|
|
1378
|
+
recordBtn = document.createElement("button");
|
|
1379
|
+
recordBtn.className = "btn recording";
|
|
1380
|
+
recordBtn.setAttribute("aria-pressed", "false");
|
|
1381
|
+
recordBtn.innerHTML = RECORD_SVG;
|
|
1382
|
+
recordBtn.addEventListener("click", (e) => {
|
|
1383
|
+
e.stopPropagation();
|
|
1384
|
+
if (recordingBusy) return;
|
|
1385
|
+
recordingBusy = true;
|
|
1386
|
+
updateRecordButton();
|
|
1387
|
+
sendToolbarBridgeRequest("recording-toggle").then((result) => {
|
|
1388
|
+
applyRecordingResult(result);
|
|
1389
|
+
if (recordingActive) {
|
|
1390
|
+
showToast("Recording started", recordBtn.getBoundingClientRect());
|
|
1391
|
+
return;
|
|
1392
|
+
}
|
|
1393
|
+
const handoffText = replayAiHandoffText(result);
|
|
1394
|
+
if (handoffText) {
|
|
1395
|
+
copyText(handoffText);
|
|
1396
|
+
showToast("Recording saved · copied for AI", recordBtn.getBoundingClientRect());
|
|
1397
|
+
return;
|
|
1398
|
+
}
|
|
1399
|
+
showToast(`Recording saved${result.path ? `: ${result.path}` : ""}`, recordBtn.getBoundingClientRect());
|
|
1400
|
+
}).catch((error) => {
|
|
1401
|
+
showToast(error instanceof Error ? error.message : String(error), recordBtn.getBoundingClientRect());
|
|
1402
|
+
}).finally(() => {
|
|
1403
|
+
recordingBusy = false;
|
|
1404
|
+
updateRecordButton();
|
|
1405
|
+
});
|
|
1406
|
+
});
|
|
1407
|
+
updateRecordButton();
|
|
1408
|
+
const dividerEl = document.createElement("div");
|
|
1409
|
+
dividerEl.className = "divider";
|
|
1410
|
+
dividerEl.setAttribute("aria-hidden", "true");
|
|
1411
|
+
const closeBtn = document.createElement("button");
|
|
1412
|
+
closeBtn.className = "btn";
|
|
1413
|
+
closeBtn.setAttribute("aria-label", "Close Tabwright toolbar");
|
|
1414
|
+
closeBtn.setAttribute("title", "Close toolbar");
|
|
1415
|
+
closeBtn.innerHTML = CLOSE_SVG;
|
|
1416
|
+
closeBtn.addEventListener("click", (e) => {
|
|
1417
|
+
e.stopPropagation();
|
|
1418
|
+
setPinMode(false);
|
|
1419
|
+
closeAnnotationEditor();
|
|
1420
|
+
clearAnnotationBadges();
|
|
1421
|
+
host.style.display = "none";
|
|
1422
|
+
});
|
|
1423
|
+
toolbarEl.appendChild(pinBtn);
|
|
1424
|
+
toolbarEl.appendChild(recordBtn);
|
|
1425
|
+
toolbarEl.appendChild(dividerEl);
|
|
1426
|
+
toolbarEl.appendChild(closeBtn);
|
|
1427
|
+
document.documentElement.appendChild(host);
|
|
1428
|
+
window.__playwriterToolbarDestroy = function() {
|
|
1429
|
+
setPinMode(false);
|
|
1430
|
+
closeAnnotationEditor();
|
|
1431
|
+
clearAnnotationBadges();
|
|
1432
|
+
removeOverlay();
|
|
1433
|
+
host.remove();
|
|
1434
|
+
delete window.__playwriterToolbarInstalled;
|
|
1435
|
+
delete window.__playwriterToolbarDestroy;
|
|
1436
|
+
delete window.__playwriterPinCount;
|
|
1437
|
+
};
|
|
1438
|
+
sendToolbarBridgeRequest("recording-status").then((result) => {
|
|
1439
|
+
applyRecordingResult(result);
|
|
1440
|
+
updateRecordButton();
|
|
1441
|
+
}).catch(() => {});
|
|
1442
|
+
}
|
|
1443
|
+
var CURRENT_EXTENSION_FEATURES = Object.values({
|
|
1444
|
+
heartbeat: "heartbeat-v1",
|
|
1445
|
+
createInitialTab: "create-initial-tab-v1",
|
|
1446
|
+
rrwebRecording: "rrweb-recording-v1",
|
|
1447
|
+
toolbarRecording: "toolbar-recording-v1",
|
|
1448
|
+
multiExtension: "multi-extension-v1"
|
|
1449
|
+
});
|
|
1450
|
+
var RELAY_FEATURE = {
|
|
1451
|
+
extensionFeatureNegotiation: "extension-feature-negotiation-v1",
|
|
1452
|
+
capabilityOptions: "capability-options-v1",
|
|
1453
|
+
capabilityAuth: "capability-auth-v1",
|
|
1454
|
+
capabilityAuthAutoTab: "capability-auth-auto-tab-v1",
|
|
1455
|
+
rrwebRecording: "rrweb-recording-v1",
|
|
1456
|
+
multiExtension: "multi-extension-v1"
|
|
1457
|
+
};
|
|
1458
|
+
Object.values(RELAY_FEATURE);
|
|
1459
|
+
RELAY_FEATURE.capabilityOptions, RELAY_FEATURE.capabilityAuth, RELAY_FEATURE.capabilityAuthAutoTab, RELAY_FEATURE.rrwebRecording;
|
|
1460
|
+
//#endregion
|
|
1461
|
+
//#region ../tabwright/src/ghost-browser.ts
|
|
1462
|
+
/**
|
|
1463
|
+
* Handles ghost-browser commands in the extension.
|
|
1464
|
+
* Calls the appropriate chrome.* API and returns the result.
|
|
1465
|
+
*
|
|
1466
|
+
* @param params - Command parameters (namespace, method, args)
|
|
1467
|
+
* @param chromeApi - The chrome object (passed to avoid global dependency)
|
|
1468
|
+
* @returns Result object with success/error status
|
|
1469
|
+
*/
|
|
1470
|
+
async function handleGhostBrowserCommand(params, chromeApi) {
|
|
1471
|
+
const { namespace, method, args } = params;
|
|
1472
|
+
try {
|
|
1473
|
+
const api = chromeApi[namespace];
|
|
1474
|
+
if (!api) return {
|
|
1475
|
+
success: false,
|
|
1476
|
+
error: `chrome.${namespace} not available (not running in Ghost Browser?)`
|
|
1477
|
+
};
|
|
1478
|
+
const fn = api[method];
|
|
1479
|
+
if (typeof fn !== "function") {
|
|
1480
|
+
if (method in api) return {
|
|
1481
|
+
success: true,
|
|
1482
|
+
result: api[method]
|
|
1483
|
+
};
|
|
1484
|
+
return {
|
|
1485
|
+
success: false,
|
|
1486
|
+
error: `chrome.${namespace}.${method} is not a function or property`
|
|
1487
|
+
};
|
|
1488
|
+
}
|
|
1489
|
+
return {
|
|
1490
|
+
success: true,
|
|
1491
|
+
result: await new Promise((resolve, reject) => {
|
|
1492
|
+
fn.call(api, ...args, (result) => {
|
|
1493
|
+
if (chromeApi.runtime.lastError) reject(new Error(chromeApi.runtime.lastError.message));
|
|
1494
|
+
else resolve(result);
|
|
1495
|
+
});
|
|
1496
|
+
})
|
|
1497
|
+
};
|
|
1498
|
+
} catch (error) {
|
|
1499
|
+
return {
|
|
1500
|
+
success: false,
|
|
1501
|
+
error: error.message
|
|
1502
|
+
};
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
//#endregion
|
|
1506
|
+
//#region src/relay-warning.ts
|
|
1507
|
+
var RELAY_RECOVERY_COMMAND = "npm install -g tabwright@latest\ntabwright session list";
|
|
1508
|
+
function getRelayReviewIssue(options) {
|
|
1509
|
+
if (options.statuses.some((status) => status === 404)) return "outdated";
|
|
1510
|
+
if (options.statuses.some((status) => status < 200 || status >= 400)) return "unavailable";
|
|
1511
|
+
}
|
|
1512
|
+
function relayReviewIssueText(options) {
|
|
1513
|
+
if (options.issue === "outdated") return `Browser control is connected, but this Tabwright local service cannot list saved recordings or capabilities. Your files were not deleted. Restart or update the Tabwright CLI.\n\nRun:\n${RELAY_RECOVERY_COMMAND}`;
|
|
1514
|
+
return `Browser control is connected, but saved recordings and capabilities are temporarily unavailable. Your files were not deleted. Restart the Tabwright CLI.\n\nRun:\n${RELAY_RECOVERY_COMMAND}`;
|
|
1515
|
+
}
|
|
1516
|
+
function relayIssueText(options) {
|
|
1517
|
+
if (options.issue === "outdated") return `Tabwright local service is outdated. Restart or update the Tabwright CLI.\n\nRun:\n${RELAY_RECOVERY_COMMAND}`;
|
|
1518
|
+
if (options.issue === "unavailable") return `Tabwright local service is not responding correctly. Restart or update the Tabwright CLI.\n\nRun:\n${RELAY_RECOVERY_COMMAND}`;
|
|
1519
|
+
return `Tabwright local service is not running. Start or update the Tabwright CLI.\n\nRun:\n${RELAY_RECOVERY_COMMAND}`;
|
|
1520
|
+
}
|
|
1521
|
+
var RelayConnectionProblemError = class extends Error {
|
|
1522
|
+
issue;
|
|
1523
|
+
constructor(options) {
|
|
1524
|
+
const errorOptions = options.cause instanceof Error ? { cause: options.cause } : void 0;
|
|
1525
|
+
super(relayIssueText({ issue: options.issue }), errorOptions);
|
|
1526
|
+
this.name = "RelayConnectionProblemError";
|
|
1527
|
+
this.issue = options.issue;
|
|
1528
|
+
}
|
|
1529
|
+
};
|
|
1530
|
+
//#endregion
|
|
1531
|
+
//#region src/connection-ownership.ts
|
|
1532
|
+
var ConnectionOwnership = class {
|
|
1533
|
+
currentGeneration = 0;
|
|
1534
|
+
current = null;
|
|
1535
|
+
beginAttempt() {
|
|
1536
|
+
this.currentGeneration += 1;
|
|
1537
|
+
return this.currentGeneration;
|
|
1538
|
+
}
|
|
1539
|
+
invalidateAttempt(generation) {
|
|
1540
|
+
if (generation !== this.currentGeneration) return false;
|
|
1541
|
+
this.currentGeneration += 1;
|
|
1542
|
+
return true;
|
|
1543
|
+
}
|
|
1544
|
+
isCurrentAttempt(generation) {
|
|
1545
|
+
return generation === this.currentGeneration;
|
|
1546
|
+
}
|
|
1547
|
+
claimOpenedConnection(options) {
|
|
1548
|
+
if (!this.isCurrentAttempt(options.generation)) return false;
|
|
1549
|
+
this.current = options.connection;
|
|
1550
|
+
return true;
|
|
1551
|
+
}
|
|
1552
|
+
isCurrentConnection(connection) {
|
|
1553
|
+
return this.current === connection;
|
|
1554
|
+
}
|
|
1555
|
+
releaseConnection(connection) {
|
|
1556
|
+
if (!this.isCurrentConnection(connection)) return false;
|
|
1557
|
+
this.current = null;
|
|
1558
|
+
return true;
|
|
1559
|
+
}
|
|
1560
|
+
};
|
|
1561
|
+
//#endregion
|
|
1562
|
+
//#region ../tabwright/dist/ghost-cursor-client.js?raw
|
|
1563
|
+
var ghost_cursor_client_default = "(() => {\n var __defProp = Object.defineProperty;\n var __getOwnPropNames = Object.getOwnPropertyNames;\n var __getOwnPropDesc = Object.getOwnPropertyDescriptor;\n var __hasOwnProp = Object.prototype.hasOwnProperty;\n function __accessProp(key) {\n return this[key];\n }\n var __toCommonJS = (from) => {\n var entry = (__moduleCache ??= new WeakMap).get(from), desc;\n if (entry)\n return entry;\n entry = __defProp({}, \"__esModule\", { value: true });\n if (from && typeof from === \"object\" || typeof from === \"function\") {\n for (var key of __getOwnPropNames(from))\n if (!__hasOwnProp.call(entry, key))\n __defProp(entry, key, {\n get: __accessProp.bind(from, key),\n enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable\n });\n }\n __moduleCache.set(from, entry);\n return entry;\n };\n var __moduleCache;\n\n // src/ghost-cursor-client.ts\n var exports_ghost_cursor_client = {};\n\n // src/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.ts\n var SCREENSTUDIO_POINTER_MACOS_TAHOE_DATA_URL = \"data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjE4IiBoZWlnaHQ9Ijk1OCIgdmlld0JveD0iMCAwIDYxOCA5NTgiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxnIGZpbHRlcj0idXJsKCNmaWx0ZXIwX2RfMzg0XzI3KSI+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTI3LjA2MiAzNy4wMzMxTDU0MC42OTYgNDUxLjU1NUM1OTIuNjUzIDUwMy42NiA1NTUuNzk0IDU5Mi41NzQgNDgyLjIyNiA1OTIuNTc0TDQyMS44MzEgNTkyLjU2OUw0ODEuODIxIDczNS4wNTRDNDkyLjMzMSA3NjAuMDIxIDQ5Mi40NzkgNzg3LjY1MiA0ODIuMjY1IDgxMi43NjdDNDcyLjAwMiA4MzcuOTMyIDQ1Mi41NjEgODU3LjU3IDQyNy40OTYgODY4LjA4QzQxNC44NjQgODczLjM1OSA0MDEuNjQgODc2LjAyNCAzODguMTIxIDg3Ni4wMjRDMzQ3LjExNyA4NzYuMDI0IDMxMC4zNTggODUxLjYgMjk0LjQ3IDgxMy44MDRMMjMxLjQyIDY2My45MThMMTkwLjM2OCA3MDAuMzM3QzEzNy4wMjkgNzQ3LjUwOCA1MyA3MDkuNjYzIDUzIDYzOC40MTNWNjcuNjc0NEM1MyAyOC45OTAzIDk5LjcyNjggOS42NDgyOCAxMjcuMDYyIDM3LjAzMzFaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTEwMi4zMTYgOTkuNjUyQzEwMi4zMTYgOTMuMTg4MiAxMTAuMTYyIDg5LjkzMTYgMTE0LjcwMSA5NC41MjA0TDUwNC44OTcgNDg1LjU1NUM1MjYuMTY0IDUwNi44NzEgNTExLjA2NSA1NDMuMjM2IDQ4MC45NjcgNTQzLjIzNkwzNDcuNTQ2IDU0My4xNjFMNDM2LjM0MiA3NTQuMTQzQzQ0Ny41NDIgNzgwLjc4OCA0MzUuMDA5IDgxMS40MjkgNDA4LjQxNCA4MjIuNTgxQzM4MS43MiA4MzMuNzgxIDM1MS4xMjggODIxLjI5OCAzMzkuOTc3IDc5NC43MDJMMjUwLjI5MyA1ODEuMzUyTDE1OC41MTcgNjYyLjY0NEMxMzcuOTkxIDY4MC44MDEgMTA2LjMxOSA2NjguMTQ1IDEwMi42NjQgNjQyLjMyM0wxMDIuMzE2IDYzNy4zMzFWOTkuNjUyWiIgZmlsbD0iYmxhY2siLz4KPC9nPgo8ZGVmcz4KPGZpbHRlciBpZD0iZmlsdGVyMF9kXzM4NF8yNyIgeD0iMC4zNCIgeT0iMC43OTkyMTkiIHdpZHRoPSI2MTcuMzIiIGhlaWdodD0iOTU3LjE0NCIgZmlsdGVyVW5pdHM9InVzZXJTcGFjZU9uVXNlIiBjb2xvci1pbnRlcnBvbGF0aW9uLWZpbHRlcnM9InNSR0IiPgo8ZmVGbG9vZCBmbG9vZC1vcGFjaXR5PSIwIiByZXN1bHQ9IkJhY2tncm91bmRJbWFnZUZpeCIvPgo8ZmVDb2xvck1hdHJpeCBpbj0iU291cmNlQWxwaGEiIHR5cGU9Im1hdHJpeCIgdmFsdWVzPSIwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAxMjcgMCIgcmVzdWx0PSJoYXJkQWxwaGEiLz4KPGZlT2Zmc2V0IGR5PSIyOS4yNiIvPgo8ZmVHYXVzc2lhbkJsdXIgc3RkRGV2aWF0aW9uPSIyNi4zMyIvPgo8ZmVDb21wb3NpdGUgaW4yPSJoYXJkQWxwaGEiIG9wZXJhdG9yPSJvdXQiLz4KPGZlQ29sb3JNYXRyaXggdHlwZT0ibWF0cml4IiB2YWx1ZXM9IjAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAuNjUgMCIvPgo8ZmVCbGVuZCBtb2RlPSJub3JtYWwiIGluMj0iQmFja2dyb3VuZEltYWdlRml4IiByZXN1bHQ9ImVmZmVjdDFfZHJvcFNoYWRvd18zODRfMjciLz4KPGZlQmxlbmQgbW9kZT0ibm9ybWFsIiBpbj0iU291cmNlR3JhcGhpYyIgaW4yPSJlZmZlY3QxX2Ryb3BTaGFkb3dfMzg0XzI3IiByZXN1bHQ9InNoYXBlIi8+CjwvZmlsdGVyPgo8L2RlZnM+Cjwvc3ZnPg==\";\n\n // src/ghost-cursor-client.ts\n var isTopFrame = (() => {\n try {\n return window === window.top;\n } catch {\n return false;\n }\n })();\n var CURSOR_ID = \"__playwriter_ghost_cursor__\";\n var SCREENSTUDIO_POINTER_ASPECT_RATIO = 618 / 958;\n var SCREENSTUDIO_HOTSPOT_X_RATIO = 0.14;\n var SCREENSTUDIO_HOTSPOT_Y_RATIO = 0.06;\n var MINIMAL_TRIANGLE_HOTSPOT_X_RATIO = 0.07;\n var MINIMAL_TRIANGLE_HOTSPOT_Y_RATIO = 0.06;\n var MOVE_EASING = \"cubic-bezier(0.65, 0, 0.35, 1)\";\n var PRESS_EASING = \"cubic-bezier(0.23, 1, 0.32, 1)\";\n var PRESS_DURATION_MS = 140;\n var IDLE_HIDE_DELAY_MS = 5000;\n var IDLE_FADE_OUT_MS = 600;\n var DEFAULT_OPTIONS = {\n style: \"minimal\",\n color: \"#111827\",\n size: 22,\n zIndex: 2147483647,\n easing: MOVE_EASING,\n minDurationMs: 220,\n maxDurationMs: 1500,\n speedPxPerMs: 1.2\n };\n var runtime = {\n outerElement: null,\n innerElement: null,\n options: DEFAULT_OPTIONS,\n x: 0,\n y: 0,\n scale: 1,\n hasPosition: false,\n enabled: false,\n idleHidden: false\n };\n var idleHideTimer = null;\n function clamp(options) {\n const { value, min, max } = options;\n return Math.min(max, Math.max(min, value));\n }\n function mergeOptions(options) {\n if (!options) {\n return DEFAULT_OPTIONS;\n }\n return {\n style: options.style ?? DEFAULT_OPTIONS.style,\n color: options.color ?? DEFAULT_OPTIONS.color,\n size: options.size ?? DEFAULT_OPTIONS.size,\n zIndex: options.zIndex ?? DEFAULT_OPTIONS.zIndex,\n easing: options.easing ?? DEFAULT_OPTIONS.easing,\n minDurationMs: options.minDurationMs ?? DEFAULT_OPTIONS.minDurationMs,\n maxDurationMs: options.maxDurationMs ?? DEFAULT_OPTIONS.maxDurationMs,\n speedPxPerMs: options.speedPxPerMs ?? DEFAULT_OPTIONS.speedPxPerMs\n };\n }\n function getCursorDimensions() {\n if (runtime.options.style === \"screenstudio\") {\n const height = runtime.options.size;\n const width = Math.max(10, Math.round(height * SCREENSTUDIO_POINTER_ASPECT_RATIO));\n return { width, height };\n }\n if (runtime.options.style === \"minimal\") {\n const size = Math.max(12, runtime.options.size);\n return { width: size, height: size };\n }\n return { width: runtime.options.size, height: runtime.options.size };\n }\n function getHotspotOffsetPx() {\n const dimensions = getCursorDimensions();\n if (runtime.options.style === \"screenstudio\") {\n return {\n x: Math.round(dimensions.width * SCREENSTUDIO_HOTSPOT_X_RATIO),\n y: Math.round(dimensions.height * SCREENSTUDIO_HOTSPOT_Y_RATIO)\n };\n }\n if (runtime.options.style === \"minimal\") {\n return {\n x: Math.round(dimensions.width * MINIMAL_TRIANGLE_HOTSPOT_X_RATIO),\n y: Math.round(dimensions.height * MINIMAL_TRIANGLE_HOTSPOT_Y_RATIO)\n };\n }\n return {\n x: Math.round(dimensions.width / 2),\n y: Math.round(dimensions.height / 2)\n };\n }\n function getBaseOpacity() {\n if (runtime.options.style === \"screenstudio\") {\n return \"0.95\";\n }\n if (runtime.options.style === \"minimal\") {\n return \"1\";\n }\n return \"0.72\";\n }\n function applyTranslate() {\n if (!runtime.outerElement) {\n return;\n }\n const hotspot = getHotspotOffsetPx();\n runtime.outerElement.style.transform = `translate3d(${runtime.x - hotspot.x}px, ${runtime.y - hotspot.y}px, 0)`;\n }\n function applyScale() {\n if (!runtime.innerElement) {\n return;\n }\n runtime.innerElement.style.transform = `scale(${runtime.scale})`;\n }\n function computeDurationMs(options) {\n if (!runtime.hasPosition) {\n return 0;\n }\n const dx = options.targetX - runtime.x;\n const dy = options.targetY - runtime.y;\n const distance = Math.hypot(dx, dy);\n const rawDurationMs = distance / runtime.options.speedPxPerMs;\n return clamp({\n value: rawDurationMs,\n min: runtime.options.minDurationMs,\n max: runtime.options.maxDurationMs\n });\n }\n function createCursorElement() {\n const outer = document.createElement(\"div\");\n outer.id = CURSOR_ID;\n outer.setAttribute(\"aria-hidden\", \"true\");\n outer.style.position = \"fixed\";\n outer.style.left = \"0\";\n outer.style.top = \"0\";\n outer.style.pointerEvents = \"none\";\n outer.style.zIndex = `${runtime.options.zIndex}`;\n outer.style.transitionProperty = \"transform\";\n outer.style.transitionTimingFunction = runtime.options.easing;\n outer.style.transitionDuration = \"0ms\";\n outer.style.willChange = \"transform\";\n const inner = document.createElement(\"div\");\n inner.style.transitionProperty = \"transform, opacity\";\n inner.style.transitionTimingFunction = PRESS_EASING;\n inner.style.transitionDuration = `${PRESS_DURATION_MS}ms`;\n inner.style.opacity = getBaseOpacity();\n outer.appendChild(inner);\n runtime.outerElement = outer;\n runtime.innerElement = inner;\n applyRuntimeVisualOptions();\n return outer;\n }\n function ensureCursorElement() {\n const existing = document.getElementById(CURSOR_ID);\n if (existing) {\n runtime.outerElement = existing;\n runtime.innerElement = existing.firstElementChild || null;\n return existing;\n }\n const outer = createCursorElement();\n const root = document.documentElement || document.body;\n root.appendChild(outer);\n return outer;\n }\n function applyRuntimeVisualOptions() {\n if (!runtime.innerElement) {\n return;\n }\n const dimensions = getCursorDimensions();\n runtime.innerElement.style.width = `${dimensions.width}px`;\n runtime.innerElement.style.height = `${dimensions.height}px`;\n if (runtime.outerElement) {\n runtime.outerElement.style.zIndex = `${runtime.options.zIndex}`;\n runtime.outerElement.style.transitionTimingFunction = runtime.options.easing;\n }\n const hotspot = getHotspotOffsetPx();\n runtime.innerElement.style.transformOrigin = `${hotspot.x}px ${hotspot.y}px`;\n if (runtime.options.style === \"screenstudio\") {\n runtime.innerElement.style.borderRadius = \"0\";\n runtime.innerElement.style.border = \"none\";\n runtime.innerElement.style.backgroundColor = \"transparent\";\n runtime.innerElement.style.backgroundImage = `url(\"${SCREENSTUDIO_POINTER_MACOS_TAHOE_DATA_URL}\")`;\n runtime.innerElement.style.backgroundRepeat = \"no-repeat\";\n runtime.innerElement.style.backgroundPosition = \"left top\";\n runtime.innerElement.style.backgroundSize = \"contain\";\n runtime.innerElement.style.backdropFilter = \"none\";\n runtime.innerElement.style.filter = \"none\";\n runtime.innerElement.style.boxShadow = \"none\";\n runtime.innerElement.style.opacity = getBaseOpacity();\n return;\n }\n if (runtime.options.style === \"minimal\") {\n const triangleSvg = `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"32\" height=\"32\" viewBox=\"-1 -1 26 26\"><path fill=\"white\" stroke=\"${runtime.options.color}\" stroke-width=\"1.5\" stroke-linejoin=\"round\" d=\"m23.284 19.124l-6.866-6.895a.4.4 0 0 1-.118-.296a.43.43 0 0 1 .163-.282l4.439-3.077a1.48 1.48 0 0 0 .621-1.48a1.48 1.48 0 0 0-1.036-1.198L1.623.302a1.14 1.14 0 0 0-1.11.282A1.13 1.13 0 0 0 .29 1.649L5.928 20.44a1.48 1.48 0 0 0 1.183 1.035a1.48 1.48 0 0 0 1.48-.621l3.078-4.44a.37.37 0 0 1 .31-.118a.43.43 0 0 1 .296.104l6.91 6.91a1.48 1.48 0 0 0 2.087 0l2.086-2.086a1.48 1.48 0 0 0-.074-2.101\"/></svg>`;\n const triangleDataUrl = `url(\"data:image/svg+xml,${encodeURIComponent(triangleSvg)}\")`;\n runtime.innerElement.style.borderRadius = \"0\";\n runtime.innerElement.style.border = \"none\";\n runtime.innerElement.style.backgroundColor = \"transparent\";\n runtime.innerElement.style.backgroundImage = triangleDataUrl;\n runtime.innerElement.style.backgroundRepeat = \"no-repeat\";\n runtime.innerElement.style.backgroundSize = \"contain\";\n runtime.innerElement.style.backgroundPosition = \"left top\";\n runtime.innerElement.style.backdropFilter = \"none\";\n runtime.innerElement.style.boxShadow = \"none\";\n runtime.innerElement.style.filter = \"drop-shadow(0 1px 2px rgba(0, 0, 0, 0.4))\";\n runtime.innerElement.style.opacity = getBaseOpacity();\n return;\n }\n runtime.innerElement.style.borderRadius = \"999px\";\n runtime.innerElement.style.border = \"none\";\n runtime.innerElement.style.backgroundColor = runtime.options.color;\n runtime.innerElement.style.backgroundImage = \"none\";\n runtime.innerElement.style.backdropFilter = \"none\";\n runtime.innerElement.style.filter = \"none\";\n runtime.innerElement.style.boxShadow = \"0 2px 10px rgba(0, 0, 0, 0.18), inset 0 0 0 2px rgba(255, 255, 255, 0.55)\";\n runtime.innerElement.style.opacity = getBaseOpacity();\n }\n function clearIdleHideTimer() {\n if (idleHideTimer !== null) {\n clearTimeout(idleHideTimer);\n idleHideTimer = null;\n }\n }\n function scheduleIdleHide() {\n clearIdleHideTimer();\n idleHideTimer = setTimeout(() => {\n idleHideTimer = null;\n if (!runtime.enabled || !runtime.innerElement) {\n return;\n }\n runtime.idleHidden = true;\n runtime.innerElement.style.transitionDuration = `${IDLE_FADE_OUT_MS}ms`;\n runtime.innerElement.style.transitionTimingFunction = PRESS_EASING;\n runtime.innerElement.style.opacity = \"0\";\n }, IDLE_HIDE_DELAY_MS);\n }\n function wakeFromIdle(options) {\n runtime.x = options.x;\n runtime.y = options.y;\n runtime.hasPosition = true;\n if (runtime.innerElement) {\n runtime.innerElement.style.transitionDuration = `${PRESS_DURATION_MS}ms`;\n runtime.innerElement.style.transitionTimingFunction = PRESS_EASING;\n runtime.innerElement.style.opacity = getBaseOpacity();\n }\n }\n function moveCursor(options) {\n if (!runtime.enabled) {\n return;\n }\n ensureCursorElement();\n const durationMs = computeDurationMs({ targetX: options.x, targetY: options.y });\n if (runtime.outerElement) {\n runtime.outerElement.style.transitionDuration = `${Math.round(durationMs)}ms`;\n runtime.outerElement.style.transitionTimingFunction = runtime.options.easing;\n }\n runtime.x = options.x;\n runtime.y = options.y;\n runtime.hasPosition = true;\n applyTranslate();\n }\n function setPressed(options) {\n if (!runtime.enabled || !runtime.innerElement) {\n return;\n }\n runtime.scale = options.pressed ? runtime.options.style === \"dot\" ? 0.92 : 0.95 : 1;\n runtime.innerElement.style.transitionDuration = `${PRESS_DURATION_MS}ms`;\n runtime.innerElement.style.transitionTimingFunction = PRESS_EASING;\n runtime.innerElement.style.opacity = options.pressed ? \"1\" : getBaseOpacity();\n applyScale();\n }\n function enable(options) {\n runtime.options = mergeOptions(options);\n runtime.enabled = true;\n ensureCursorElement();\n applyRuntimeVisualOptions();\n if (!runtime.hasPosition) {\n runtime.x = Math.round(window.innerWidth / 2);\n runtime.y = Math.round(window.innerHeight / 2);\n runtime.scale = 1;\n runtime.hasPosition = true;\n }\n runtime.idleHidden = false;\n if (runtime.innerElement) {\n runtime.innerElement.style.opacity = getBaseOpacity();\n }\n applyTranslate();\n applyScale();\n scheduleIdleHide();\n }\n function disable() {\n runtime.enabled = false;\n runtime.scale = 1;\n runtime.hasPosition = false;\n runtime.idleHidden = false;\n clearIdleHideTimer();\n if (runtime.outerElement) {\n runtime.outerElement.remove();\n runtime.outerElement = null;\n runtime.innerElement = null;\n }\n }\n function applyMouseAction(action) {\n if (!runtime.enabled) {\n return;\n }\n if (runtime.idleHidden) {\n runtime.idleHidden = false;\n wakeFromIdle({ x: action.x, y: action.y });\n }\n if (action.type === \"move\" || action.type === \"wheel\") {\n moveCursor({ x: action.x, y: action.y });\n } else if (action.type === \"down\") {\n moveCursor({ x: action.x, y: action.y });\n setPressed({ pressed: true });\n } else if (action.type === \"up\") {\n moveCursor({ x: action.x, y: action.y });\n setPressed({ pressed: false });\n }\n scheduleIdleHide();\n }\n var api = {\n enable,\n disable,\n applyMouseAction,\n isEnabled: () => {\n return runtime.enabled;\n }\n };\n if (isTopFrame) {\n globalThis.__playwriterGhostCursor = api;\n try {\n if (document.readyState === \"loading\") {\n document.addEventListener(\"DOMContentLoaded\", () => {\n try {\n api.enable();\n } catch {}\n }, { once: true });\n } else {\n api.enable();\n }\n } catch {}\n }\n})();\n";
|
|
1564
|
+
//#endregion
|
|
1565
|
+
//#region ../tabwright/dist/bippy.js?raw
|
|
1566
|
+
var bippy_default = "(() => {\n // ../node_modules/.pnpm/bippy@0.5.43_react@19.2.7/node_modules/bippy/dist/rdt-hook.js\n var e = `0.5.43`;\n var t = `bippy-${e}`;\n var n = Object.defineProperty;\n var r = Object.prototype.hasOwnProperty;\n var i = () => {};\n var a = (e2) => {\n try {\n Function.prototype.toString.call(e2).indexOf(`^_^`) > -1 && setTimeout(() => {\n throw Error(`React is running in production mode, but dead code elimination has not been applied. Read how to correctly configure React for production: https://reactjs.org/link/perf-use-production-build`);\n });\n } catch {}\n };\n var o = (e2 = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__) => !!(e2 && (`getFiberRoots` in e2));\n var s = false;\n var c;\n var l = (e2 = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__) => s ? true : (e2 && typeof e2.inject == `function` && (c = e2.inject.toString()), !!c?.includes(`(injected)`));\n var u = new Set;\n var d = new Set;\n var f = (e2) => {\n let r2 = new Map, o2 = 0, s2 = { _instrumentationIsActive: false, _instrumentationSource: t, checkDCE: a, hasUnsupportedRendererAttached: false, inject(e3) {\n let t2 = ++o2;\n return r2.set(t2, e3), d.add(e3), s2._instrumentationIsActive || (s2._instrumentationIsActive = true, u.forEach((e4) => e4())), t2;\n }, on: i, onCommitFiberRoot: i, onCommitFiberUnmount: i, onPostCommitFiberRoot: i, renderers: r2, supportsFiber: true, supportsFlight: true };\n try {\n n(globalThis, `__REACT_DEVTOOLS_GLOBAL_HOOK__`, { configurable: true, enumerable: true, get() {\n return s2;\n }, set(t3) {\n if (t3 && typeof t3 == `object`) {\n let n2 = s2.renderers;\n s2 = t3, n2.size > 0 && (n2.forEach((e3, n3) => {\n d.add(e3), t3.renderers.set(n3, e3);\n }), p(e2));\n }\n } });\n let t2 = window.hasOwnProperty, r3 = false;\n n(window, `hasOwnProperty`, { configurable: true, value: function(...e3) {\n try {\n if (!r3 && e3[0] === `__REACT_DEVTOOLS_GLOBAL_HOOK__`)\n return globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__ = undefined, r3 = true, -0;\n } catch {}\n return t2.apply(this, e3);\n }, writable: true });\n } catch {\n p(e2);\n }\n return s2;\n };\n var p = (e2) => {\n e2 && u.add(e2);\n try {\n let n2 = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;\n if (!n2)\n return;\n if (!n2._instrumentationSource) {\n n2.checkDCE = a, n2.supportsFiber = true, n2.supportsFlight = true, n2.hasUnsupportedRendererAttached = false, n2._instrumentationSource = t, n2._instrumentationIsActive = false;\n let e3 = o(n2);\n if (e3 || (n2.on = i), n2.renderers.size) {\n n2._instrumentationIsActive = true, u.forEach((e4) => e4());\n return;\n }\n let r2 = n2.inject, c2 = l(n2);\n c2 && !e3 && (s = true, n2.inject({ scheduleRefresh() {} }) && (n2._instrumentationIsActive = true)), n2.inject = (e4) => {\n let t2 = r2(e4);\n return d.add(e4), c2 && n2.renderers.set(t2, e4), n2._instrumentationIsActive = true, u.forEach((e5) => e5()), t2;\n };\n }\n (n2.renderers.size || n2._instrumentationIsActive || l()) && e2?.();\n } catch {}\n };\n var m = () => r.call(globalThis, `__REACT_DEVTOOLS_GLOBAL_HOOK__`);\n var h = (e2) => m() ? (p(e2), globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__) : f(e2);\n var g = () => !!(typeof window < `u` && (window.document?.createElement || window.navigator?.product === `ReactNative`));\n var _ = () => {\n try {\n g() && h();\n } catch {}\n };\n\n // ../node_modules/.pnpm/bippy@0.5.43_react@19.2.7/node_modules/bippy/dist/install-hook-only.js\n _();\n\n // ../node_modules/.pnpm/bippy@0.5.43_react@19.2.7/node_modules/bippy/dist/core.js\n var b = (e2) => {\n switch (e2.tag) {\n case 5:\n case 26:\n case 27:\n return true;\n default:\n return typeof e2.type == `string`;\n }\n };\n var be = (e2) => {\n switch (e2.tag) {\n case 1:\n case 11:\n case 0:\n case 14:\n case 15:\n return true;\n default:\n return false;\n }\n };\n function A(e2, t2, n2 = false) {\n if (!e2)\n return null;\n let r2 = t2(e2);\n if (r2 instanceof Promise)\n return (async () => {\n if (await r2 === true)\n return e2;\n let i3 = n2 ? e2.return : e2.child;\n for (;i3; ) {\n let e3 = await M(i3, t2, n2);\n if (e3)\n return e3;\n i3 = n2 ? null : i3.sibling;\n }\n return null;\n })();\n if (r2 === true)\n return e2;\n let i2 = n2 ? e2.return : e2.child;\n for (;i2; ) {\n let e3 = j(i2, t2, n2);\n if (e3)\n return e3;\n i2 = n2 ? null : i2.sibling;\n }\n return null;\n }\n var j = (e2, t2, n2 = false) => {\n if (!e2)\n return null;\n if (t2(e2) === true)\n return e2;\n let r2 = n2 ? e2.return : e2.child;\n for (;r2; ) {\n let e3 = j(r2, t2, n2);\n if (e3)\n return e3;\n r2 = n2 ? null : r2.sibling;\n }\n return null;\n };\n var M = async (e2, t2, n2 = false) => {\n if (!e2)\n return null;\n if (await t2(e2) === true)\n return e2;\n let r2 = n2 ? e2.return : e2.child;\n for (;r2; ) {\n let e3 = await M(r2, t2, n2);\n if (e3)\n return e3;\n r2 = n2 ? null : r2.sibling;\n }\n return null;\n };\n var N = (e2) => {\n let t2 = e2;\n return typeof t2 == `function` ? t2 : typeof t2 == `object` && t2 ? N(t2.type || t2.render) : null;\n };\n var Ee = (e2) => {\n let t2 = e2;\n if (typeof t2 == `string`)\n return t2;\n if (typeof t2 != `function` && !(typeof t2 == `object` && t2))\n return null;\n let n2 = t2.displayName || t2.name || null;\n if (n2)\n return n2;\n let r2 = N(t2);\n return r2 && (r2.displayName || r2.name) || null;\n };\n var I = new WeakMap;\n var U = new WeakMap;\n var Pe = (e2) => {\n let t2 = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;\n if (t2?.renderers)\n for (let n2 of t2.renderers.values())\n try {\n let t3 = n2.findFiberByHostInstance?.(e2);\n if (t3)\n return t3;\n } catch {}\n if (typeof e2 == `object` && e2) {\n if (`_reactRootContainer` in e2)\n return e2._reactRootContainer?._internalRoot?.current?.child;\n for (let t3 in e2)\n if (t3.startsWith(`__reactContainer$`) || t3.startsWith(`__reactInternalInstance$`) || t3.startsWith(`__reactFiber`))\n return e2[t3] || null;\n }\n return null;\n };\n var Z = Error();\n var Q = new Set;\n\n // ../node_modules/.pnpm/bippy@0.5.43_react@19.2.7/node_modules/bippy/dist/source.js\n var i2 = /^[a-zA-Z][a-zA-Z\\d+\\-.]*:/;\n var a2 = [`rsc://`, `file:///`, `webpack-internal://`, `webpack://`, `node:`, `turbopack://`, `metro://`, `/app-pages-browser/`, `/(app-pages-browser)/`];\n var o2 = [`<anonymous>`, `eval`, ``];\n var s2 = /\\.(jsx|tsx|ts|js)$/;\n var c2 = /(\\.min|bundle|chunk|vendor|vendors|runtime|polyfill|polyfills)\\.(js|mjs|cjs)$|(chunk|bundle|vendor|vendors|runtime|polyfill|polyfills|framework|app|main|index)[-_.][A-Za-z0-9_-]{4,}\\.(js|mjs|cjs)$|[\\da-f]{8,}\\.(js|mjs|cjs)$|[-_.][\\da-f]{20,}\\.(js|mjs|cjs)$|\\/dist\\/|\\/build\\/|\\/.next\\/|\\/out\\/|\\/node_modules\\/|\\.webpack\\.|\\.vite\\.|\\.turbopack\\./i;\n var l2 = /^\\?[\\w~.-]+(?:=[^&#]*)?(?:&[\\w~.-]+(?:=[^&#]*)?)*$/;\n var u2 = /\\(at [^)]+\\)$/;\n var d3 = /(^|@)\\S+:\\d+/;\n var f3 = /^\\s*at .*(\\S+:\\d+|\\(native\\))/m;\n var ee2 = /^(eval@)?(\\[native code\\])?$/;\n var m3 = (e2, t2) => {\n if (t2?.includeInElement !== false) {\n let n2 = e2.split(`\n`), r2 = [];\n for (let e3 of n2)\n if (/^\\s*at\\s+/.test(e3)) {\n let t3 = _3(e3, undefined)[0];\n t3 && r2.push(t3);\n } else if (/^\\s*in\\s+/.test(e3)) {\n let t3 = e3.replace(/^\\s*in\\s+/, ``).replace(/\\s*\\(at .*\\)$/, ``);\n r2.push({ functionName: t3, source: e3 });\n } else if (e3.match(d3)) {\n let t3 = v2(e3, undefined)[0];\n t3 && r2.push(t3);\n }\n return g3(r2, t2);\n }\n return e2.match(f3) ? _3(e2, t2) : v2(e2, t2);\n };\n var h3 = (e2) => {\n if (!e2.includes(`:`))\n return [e2, undefined, undefined];\n let t2 = e2.startsWith(`(`) && /:\\d+\\)$/.test(e2) ? e2.slice(1, -1) : e2, n2 = /(.+?)(?::(\\d+))?(?::(\\d+))?$/.exec(t2);\n return n2 ? [n2[1], n2[2] || undefined, n2[3] || undefined] : [t2, undefined, undefined];\n };\n var g3 = (e2, t2) => t2 && t2.slice != null ? Array.isArray(t2.slice) ? e2.slice(t2.slice[0], t2.slice[1]) : e2.slice(0, t2.slice) : e2;\n var _3 = (e2, t2) => g3(e2.split(`\n`).filter((e3) => !!e3.match(f3)), t2).map((e3) => {\n let t3 = e3;\n t3.includes(`(eval `) && (t3 = t3.replace(/eval code/g, `eval`).replace(/(\\(eval at [^()]*)|(,.*$)/g, ``));\n let n2 = t3.replace(/^\\s+/, ``).replace(/\\(eval code/g, `(`).replace(/^.*?\\s+/, ``), r2 = n2.match(/ (\\(.+\\)$)/);\n n2 = r2 ? n2.replace(r2[0], ``) : n2;\n let i3 = h3(r2 ? r2[1] : n2);\n return { functionName: r2 && n2 || undefined, fileName: [`eval`, `<anonymous>`].includes(i3[0]) ? undefined : i3[0], lineNumber: i3[1] ? +i3[1] : undefined, columnNumber: i3[2] ? +i3[2] : undefined, source: t3 };\n });\n var v2 = (e2, t2) => g3(e2.split(`\n`).filter((e3) => !e3.match(ee2)), t2).map((e3) => {\n let t3 = e3;\n if (t3.includes(` > eval`) && (t3 = t3.replace(/ line (\\d+)(?: > eval line \\d+)* > eval:\\d+:\\d+/g, `:$1`)), !t3.includes(`@`) && !t3.includes(`:`))\n return { functionName: t3 };\n {\n let e4 = /(([^\\n\\r\"\\u2028\\u2029]*\".[^\\n\\r\"\\u2028\\u2029]*\"[^\\n\\r@\\u2028\\u2029]*(?:@[^\\n\\r\"\\u2028\\u2029]*\"[^\\n\\r@\\u2028\\u2029]*)*(?:[\\n\\r\\u2028\\u2029][^@]*)?)?[^@]*)@/, n2 = t3.match(e4), r2 = n2 && n2[1] ? n2[1] : undefined, i3 = h3(t3.replace(e4, ``));\n return { functionName: r2, fileName: i3[0], lineNumber: i3[1] ? +i3[1] : undefined, columnNumber: i3[2] ? +i3[2] : undefined, source: t3 };\n }\n });\n var se2 = 44;\n var y = `ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/`;\n var ce2 = new Uint8Array(64);\n var le2 = new Uint8Array(128);\n for (let e2 = 0;e2 < y.length; e2++) {\n let t2 = y.charCodeAt(e2);\n ce2[e2] = t2, le2[t2] = e2;\n }\n function b2(e2, t2) {\n let n2 = 0, r2 = 0, i3 = 0;\n do\n i3 = le2[e2.next()], n2 |= (i3 & 31) << r2, r2 += 5;\n while (i3 & 32);\n let a3 = n2 & 1;\n return n2 >>>= 1, a3 && (n2 = -2147483648 | -n2), t2 + n2;\n }\n function ue2(e2, t2) {\n return e2.pos >= t2 ? false : e2.peek() !== se2;\n }\n var de2 = class {\n constructor(e2) {\n this.pos = 0, this.buffer = e2;\n }\n next() {\n return this.buffer.charCodeAt(this.pos++);\n }\n peek() {\n return this.buffer.charCodeAt(this.pos);\n }\n indexOf(e2) {\n let { buffer: t2, pos: n2 } = this, r2 = t2.indexOf(e2, n2);\n return r2 === -1 ? t2.length : r2;\n }\n };\n function x2(e2) {\n let { length: t2 } = e2, n2 = new de2(e2), r2 = [], i3 = 0, a3 = 0, o3 = 0, s3 = 0, c3 = 0;\n do {\n let e3 = n2.indexOf(`;`), t3 = [], l3 = true, u3 = 0;\n for (i3 = 0;n2.pos < e3; ) {\n let r3;\n i3 = b2(n2, i3), i3 < u3 && (l3 = false), u3 = i3, ue2(n2, e3) ? (a3 = b2(n2, a3), o3 = b2(n2, o3), s3 = b2(n2, s3), ue2(n2, e3) ? (c3 = b2(n2, c3), r3 = [i3, a3, o3, s3, c3]) : r3 = [i3, a3, o3, s3]) : r3 = [i3], t3.push(r3), n2.pos++;\n }\n l3 || fe2(t3), r2.push(t3), n2.pos = e3 + 1;\n } while (n2.pos <= t2);\n return r2;\n }\n function fe2(e2) {\n e2.sort(pe2);\n }\n function pe2(e2, t2) {\n return e2[0] - t2[0];\n }\n var S2 = /^[a-zA-Z][a-zA-Z\\d+\\-.]*:/;\n var me2 = /^data:application\\/json[^,]+base64,/;\n var he2 = /(?:\\/\\/[@#][ \\t]+sourceMappingURL=([^\\s'\"]+?)[ \\t]*$)|(?:\\/\\*[@#][ \\t]+sourceMappingURL=([^*]+?)[ \\t]*(?:\\*\\/)[ \\t]*$)/;\n var C2 = new Map;\n var w2 = new Map;\n var T2 = (e2, t2, n2, r2) => {\n if (n2 < 0 || n2 >= e2.length)\n return null;\n let i3 = e2[n2];\n if (!i3 || i3.length === 0)\n return null;\n let a3 = null;\n for (let e3 of i3)\n if (e3[0] <= r2)\n a3 = e3;\n else\n break;\n if (!a3 || a3.length < 4)\n return null;\n let [, o3, s3, c3] = a3;\n if (o3 === undefined || s3 === undefined || c3 === undefined)\n return null;\n let l3 = t2[o3];\n return l3 ? { columnNumber: c3, fileName: l3, lineNumber: s3 + 1 } : null;\n };\n var E2 = (e2, t2, n2) => {\n if (e2.sections) {\n let r2 = null;\n for (let i4 of e2.sections)\n if (t2 > i4.offset.line || t2 === i4.offset.line && n2 >= i4.offset.column)\n r2 = i4;\n else\n break;\n if (!r2)\n return null;\n let i3 = t2 - r2.offset.line, a3 = t2 === r2.offset.line ? n2 - r2.offset.column : n2;\n return T2(r2.map.mappings, r2.map.sources, i3, a3);\n }\n return T2(e2.mappings, e2.sources, t2 - 1, n2);\n };\n var ge2 = (e2, t2) => {\n let n2 = t2.split(`\n`), r2;\n for (let e3 = n2.length - 1;e3 >= 0 && !r2; e3--) {\n let t3 = n2[e3].match(he2);\n t3 && (r2 = t3[1] || t3[2]);\n }\n if (!r2)\n return null;\n let i3 = S2.test(r2);\n if (!(me2.test(r2) || i3 || r2.startsWith(`/`))) {\n let t3 = e2.split(`/`);\n t3[t3.length - 1] = r2, r2 = t3.join(`/`);\n }\n return r2;\n };\n var _e2 = (e2) => ({ file: e2.file, mappings: x2(e2.mappings), names: e2.names, sourceRoot: e2.sourceRoot, sources: e2.sources, sourcesContent: e2.sourcesContent, version: 3 });\n var ve2 = (e2) => {\n let t2 = e2.sections.map(({ map: e3, offset: t3 }) => ({ map: { ...e3, mappings: x2(e3.mappings) }, offset: t3 })), n2 = new Set;\n for (let e3 of t2)\n for (let t3 of e3.map.sources)\n n2.add(t3);\n return { file: e2.file, mappings: [], names: [], sections: t2, sourceRoot: undefined, sources: Array.from(n2), sourcesContent: undefined, version: 3 };\n };\n var D2 = (e2) => {\n if (!e2)\n return false;\n let t2 = e2.trim();\n if (!t2)\n return false;\n let n2 = t2.match(S2);\n if (!n2)\n return true;\n let r2 = n2[0].toLowerCase();\n return r2 === `http:` || r2 === `https:`;\n };\n var ye2 = async (e2, t2 = fetch) => {\n if (!D2(e2))\n return null;\n let n2 = await t2(e2);\n if (!n2.ok)\n return null;\n let r2 = await n2.text();\n if (!r2)\n return null;\n let i3 = ge2(e2, r2);\n if (!i3 || !D2(i3))\n return null;\n let a3 = await t2(i3);\n if (!a3.ok)\n return null;\n try {\n let e3 = await a3.json();\n return `sections` in e3 ? ve2(e3) : _e2(e3);\n } catch {\n return null;\n }\n };\n var O2 = async (e2, t2 = true, n2) => {\n if (t2 && C2.has(e2))\n return C2.get(e2) ?? null;\n let r2 = t2 ? w2.get(e2) : undefined;\n if (r2)\n return (await r2).sourceMap;\n let i3 = ye2(e2, n2).then((e3) => ({ sourceMap: e3, isTransientFailure: false }), () => ({ sourceMap: null, isTransientFailure: true }));\n t2 && w2.set(e2, i3);\n let { sourceMap: a3, isTransientFailure: o3 } = await i3;\n return t2 && (w2.delete(e2), o3 || C2.set(e2, a3)), a3;\n };\n var be2 = async (e2, t2 = true, n2) => await Promise.all(e2.map(async (e3) => {\n if (!e3.fileName)\n return e3;\n let r2 = await O2(e3.fileName, t2, n2);\n if (!r2 || typeof e3.lineNumber != `number` || typeof e3.columnNumber != `number`)\n return e3;\n let i3 = E2(r2, e3.lineNumber, e3.columnNumber);\n return i3 ? { ...e3, source: i3.fileName && e3.source ? e3.source.replace(e3.fileName, i3.fileName) : e3.source, fileName: i3.fileName, lineNumber: i3.lineNumber, columnNumber: i3.columnNumber, isSymbolicated: true } : e3;\n }));\n var xe2 = (e2) => e2._debugStack instanceof Error && typeof e2._debugStack?.stack == `string`;\n var Se2 = () => {\n let n2 = h();\n for (let t2 of [...Array.from(d), ...Array.from(n2.renderers.values())]) {\n let e2 = t2.currentDispatcherRef;\n if (e2 && typeof e2 == `object`)\n return `H` in e2 ? e2.H : e2.current;\n }\n return null;\n };\n var Ce2 = (t2) => {\n for (let n2 of d) {\n let e2 = n2.currentDispatcherRef;\n e2 && typeof e2 == `object` && (`H` in e2 ? e2.H = t2 : e2.current = t2);\n }\n };\n var k2 = (e2) => `\n in ${e2}`;\n var we2 = (e2, t2) => {\n let n2 = k2(e2);\n return t2 && (n2 += ` (at ${t2})`), n2;\n };\n var A2 = false;\n var j2 = (e2, t2) => {\n if (!e2 || A2)\n return ``;\n let r2 = Error.prepareStackTrace;\n Error.prepareStackTrace = undefined, A2 = true;\n let i3 = Se2();\n Ce2(null);\n let { error: a3, warn: o3 } = console;\n console.error = () => {}, console.warn = () => {};\n try {\n let r3 = { DetermineComponentFrameRoot() {\n let n2;\n try {\n if (t2) {\n let t3 = function() {\n throw Error();\n };\n if (Object.defineProperty(t3.prototype, `props`, { set: function() {\n throw Error();\n } }), typeof Reflect == `object` && Reflect.construct) {\n try {\n Reflect.construct(t3, []);\n } catch (e3) {\n n2 = e3;\n }\n Reflect.construct(e2, [], t3);\n } else {\n try {\n t3.call();\n } catch (e3) {\n n2 = e3;\n }\n e2.call(t3.prototype);\n }\n } else {\n try {\n throw Error();\n } catch (e3) {\n n2 = e3;\n }\n let t3 = e2();\n t3 && typeof t3.catch == `function` && t3.catch(() => {});\n }\n } catch (e3) {\n if (e3 instanceof Error && n2 instanceof Error && typeof e3.stack == `string`)\n return [e3.stack, n2.stack];\n }\n return [null, null];\n } };\n r3.DetermineComponentFrameRoot.displayName = `DetermineComponentFrameRoot`, Object.getOwnPropertyDescriptor(r3.DetermineComponentFrameRoot, `name`)?.configurable && Object.defineProperty(r3.DetermineComponentFrameRoot, `name`, { value: `DetermineComponentFrameRoot` });\n let [i4, a4] = r3.DetermineComponentFrameRoot();\n if (i4 && a4) {\n let t3 = i4.split(`\n`), r4 = a4.split(`\n`), o4 = 0, s4 = 0;\n for (;o4 < t3.length && !t3[o4].includes(`DetermineComponentFrameRoot`); )\n o4++;\n for (;s4 < r4.length && !r4[s4].includes(`DetermineComponentFrameRoot`); )\n s4++;\n if (o4 === t3.length || s4 === r4.length)\n for (o4 = t3.length - 1, s4 = r4.length - 1;o4 >= 1 && s4 >= 0 && t3[o4] !== r4[s4]; )\n s4--;\n for (;o4 >= 1 && s4 >= 0; o4--, s4--)\n if (t3[o4] !== r4[s4]) {\n if (o4 !== 1 || s4 !== 1)\n do\n if (o4--, s4--, s4 < 0 || t3[o4] !== r4[s4]) {\n let r5 = `\n${t3[o4].replace(` at new `, ` at `)}`, i5 = Ee(e2);\n return i5 && r5.includes(`<anonymous>`) && (r5 = r5.replace(`<anonymous>`, i5)), r5;\n }\n while (o4 >= 1 && s4 >= 0);\n break;\n }\n }\n } finally {\n A2 = false, Error.prepareStackTrace = r2, Ce2(i3), console.error = a3, console.warn = o3;\n }\n let s3 = e2 ? Ee(e2) : ``;\n return s3 ? k2(s3) : ``;\n };\n var M2 = (e2, t2) => {\n let n2 = e2.tag, r2 = ``;\n switch (n2) {\n case 28:\n r2 = k2(`Activity`);\n break;\n case 1:\n r2 = j2(e2.type, true);\n break;\n case 11:\n r2 = j2(e2.type.render, false);\n break;\n case 0:\n case 15:\n r2 = j2(e2.type, false);\n break;\n case 5:\n case 26:\n case 27:\n r2 = k2(e2.type);\n break;\n case 16:\n r2 = k2(`Lazy`);\n break;\n case 13:\n r2 = e2.child !== t2 && t2 !== null ? k2(`Suspense Fallback`) : k2(`Suspense`);\n break;\n case 19:\n r2 = k2(`SuspenseList`);\n break;\n case 30:\n r2 = k2(`ViewTransition`);\n break;\n default:\n return ``;\n }\n return r2;\n };\n var N2 = (e2) => {\n try {\n let t2 = ``, n2 = e2, r2 = null;\n do {\n t2 += M2(n2, r2);\n let e3 = n2._debugInfo;\n if (e3 && Array.isArray(e3))\n for (let n3 = e3.length - 1;n3 >= 0; n3--) {\n let r3 = e3[n3];\n typeof r3.name == `string` && (t2 += we2(r3.name, r3.env));\n }\n r2 = n2, n2 = n2.return;\n } while (n2);\n return t2;\n } catch (e3) {\n return e3 instanceof Error ? `\nError generating stack: ${e3.message}\n${e3.stack}` : ``;\n }\n };\n var P2 = (e2) => {\n let t2 = Error.prepareStackTrace;\n Error.prepareStackTrace = undefined;\n let n2 = e2;\n if (!n2)\n return ``;\n Error.prepareStackTrace = t2, n2.startsWith(`Error: react-stack-top-frame\n`) && (n2 = n2.slice(29));\n let r2 = n2.indexOf(`\n`);\n if (r2 !== -1 && (n2 = n2.slice(r2 + 1)), r2 = Math.max(n2.indexOf(`react_stack_bottom_frame`), n2.indexOf(`react-stack-bottom-frame`)), r2 !== -1 && (r2 = n2.lastIndexOf(`\n`, r2)), r2 !== -1)\n n2 = n2.slice(0, r2);\n else\n return ``;\n return n2;\n };\n var Te2 = (e2) => !!(e2.functionName && e2.fileName && (e2.fileName.startsWith(`rsc://`) || e2.fileName.startsWith(`about://React/`)));\n var Ee2 = (e2, t2) => e2.fileName === t2.fileName && e2.lineNumber === t2.lineNumber && e2.columnNumber === t2.columnNumber;\n var De2 = (e2) => {\n let t2 = new Map;\n for (let n2 of e2)\n for (let e3 of n2.stackFrames) {\n if (!Te2(e3))\n continue;\n let n3 = e3.functionName, r2 = t2.get(n3) ?? [];\n r2.some((t3) => Ee2(t3, e3)) || (r2.push(e3), t2.set(n3, r2));\n }\n return t2;\n };\n var Oe2 = (e2, t2, n2) => {\n if (!e2.functionName)\n return { ...e2, isServer: true };\n let r2 = t2.get(e2.functionName);\n if (!r2 || r2.length === 0)\n return { ...e2, isServer: true };\n let i3 = n2.get(e2.functionName) ?? 0, a3 = r2[i3 % r2.length];\n return n2.set(e2.functionName, i3 + 1), { ...e2, isServer: true, fileName: a3.fileName, lineNumber: a3.lineNumber, columnNumber: a3.columnNumber, source: e2.source?.replace(`(at Server)`, `(${a3.fileName}:${a3.lineNumber}:${a3.columnNumber})`) };\n };\n var ke = (e2) => {\n let t2 = [];\n return A(e2, (e3) => {\n if (!xe2(e3))\n return;\n let r2 = typeof e3.type == `string` ? e3.type : Ee(e3.type) || `<anonymous>`;\n t2.push({ componentName: r2, stackFrames: m3(P2(e3._debugStack?.stack)) });\n }, true), t2;\n };\n var F = async (e2, t2 = true, n2) => {\n let r2 = ke(e2), i3 = m3(N2(e2)), a3 = De2(r2), o3 = new Map;\n return be2(i3.map((e3) => (e3.source?.includes(`(at Server)`) ?? false) || e3.source != null && u2.test(e3.source) ? Oe2(e3, a3, o3) : e3).filter((e3, t3, n3) => {\n if (t3 === 0)\n return true;\n let r3 = n3[t3 - 1];\n return e3.functionName !== r3.functionName;\n }), t2, n2);\n };\n var I2 = (e2) => {\n let t2 = e2._debugSource;\n return t2 ? typeof t2 == `object` && !!t2 && `fileName` in t2 && typeof t2.fileName == `string` && `lineNumber` in t2 && typeof t2.lineNumber == `number` : false;\n };\n var Ae2 = async (e2, t2 = true, n2) => {\n if (I2(e2))\n return e2._debugSource || null;\n let r2 = await F(e2, t2, n2);\n for (let e3 of r2)\n if (e3.fileName)\n return { fileName: e3.fileName, lineNumber: e3.lineNumber, columnNumber: e3.columnNumber, functionName: e3.functionName };\n return null;\n };\n var L2 = (e2) => e2.split(`/`).filter(Boolean).length;\n var je2 = (e2) => e2.split(`/`).filter(Boolean)[0] ?? null;\n var Me2 = (e2) => {\n let t2 = e2.indexOf(`/`, 1);\n if (t2 === -1 || L2(e2.slice(0, t2)) !== 1)\n return e2;\n let n2 = e2.slice(t2);\n if (!s2.test(n2) || L2(n2) < 2)\n return e2;\n let r2 = je2(n2);\n return !r2 || r2.startsWith(`@`) || r2.length > 4 ? e2 : n2;\n };\n var R2 = (e2) => {\n if (!e2 || o2.some((t3) => t3 === e2))\n return ``;\n let t2 = e2, n2 = t2.startsWith(`http://`) || t2.startsWith(`https://`);\n if (n2)\n try {\n t2 = new URL(t2).pathname;\n } catch {}\n if (n2 && (t2 = Me2(t2)), t2.startsWith(`about://React/`)) {\n let e3 = t2.slice(14), n3 = e3.indexOf(`/`), r3 = e3.indexOf(`:`);\n t2 = n3 !== -1 && (r3 === -1 || n3 < r3) ? e3.slice(n3 + 1) : e3;\n }\n let r2 = true;\n for (;r2; ) {\n r2 = false;\n for (let e3 of a2)\n if (t2.startsWith(e3)) {\n t2 = t2.slice(e3.length), e3 === `file:///` && (t2 = `/${t2.replace(/^\\/+/, ``)}`), r2 = true;\n break;\n }\n }\n if (i2.test(t2)) {\n let e3 = t2.match(i2);\n e3 && (t2 = t2.slice(e3[0].length));\n }\n if (t2.startsWith(`//`)) {\n let e3 = t2.indexOf(`/`, 2);\n t2 = e3 === -1 ? `` : t2.slice(e3);\n }\n let s3 = t2.indexOf(`?`);\n if (s3 !== -1) {\n let e3 = t2.slice(s3);\n l2.test(e3) && (t2 = t2.slice(0, s3));\n }\n return t2;\n };\n var Ne2 = (e2) => {\n let t2 = R2(e2);\n return !(!t2 || !s2.test(t2) || c2.test(t2));\n };\n var z2 = Symbol.for(`react.context`);\n var Ie2 = Symbol.for(`react.memo_cache_sentinel`);\n var B2 = [];\n var H2 = null;\n var U2 = null;\n var W = null;\n var G = 0;\n var K = null;\n var q2 = Error(\"Suspense Exception: This is not a real error! It's an implementation detail of `use` to interrupt the current render.\");\n var Y = () => {\n let e2 = U2;\n return e2 !== null && (U2 = e2.next), e2;\n };\n var X2 = (e2) => {\n if (H2 === null)\n return e2._currentValue;\n if (W === null)\n throw Error(`Context reads do not line up with context dependencies.`);\n if (Object.prototype.hasOwnProperty.call(W, `memoizedValue`)) {\n let e3 = W.memoizedValue;\n return W = W.next, e3;\n }\n return e2._currentValue;\n };\n var Z2 = (e2, t2, n2, r2 = null) => {\n B2.push({ displayName: r2, primitive: e2, stackError: Error(), value: t2, dispatcherHookName: n2 });\n };\n var Be = (e2) => {\n if (typeof e2 == `object` && e2) {\n let t2 = e2;\n if (typeof t2.then == `function`) {\n let e3 = K !== null && G < K.length ? K[G++] : t2;\n switch (e3.status) {\n case `fulfilled`:\n return Z2(`Promise`, e3.value, `Use`), e3.value;\n case `rejected`:\n throw e3.reason;\n }\n throw Z2(`Unresolved`, e3, `Use`), q2;\n }\n if (t2.$$typeof === z2 && `_currentValue` in t2) {\n let e3 = t2, n2 = X2(e3);\n return Z2(`Context (use)`, n2, `Use`, e3.displayName || `Context`), n2;\n }\n }\n throw Error(`An unsupported type was passed to use(): ` + String(e2));\n };\n var Ve = (e2) => {\n let t2 = X2(e2);\n return Z2(`Context`, t2, `Context`, e2.displayName || null), t2;\n };\n var He = (e2) => {\n let t2 = Y(), n2 = t2 === null ? typeof e2 == `function` ? e2() : e2 : t2.memoizedState;\n return Z2(`State`, n2, `State`), [n2, () => {}];\n };\n var Ue = (e2, t2, n2) => {\n let r2 = Y(), i3 = r2 === null ? n2 === undefined ? t2 : n2(t2) : r2.memoizedState;\n return Z2(`Reducer`, i3, `Reducer`), [i3, () => {}];\n };\n var We = (e2) => {\n let t2 = Y(), n2 = t2 === null ? { current: e2 } : t2.memoizedState;\n return Z2(`Ref`, n2.current, `Ref`), n2;\n };\n var Ge = () => {\n let e2 = Y();\n return Z2(`CacheRefresh`, e2 === null ? () => {} : e2.memoizedState, `CacheRefresh`), () => {};\n };\n var Ke = (e2) => {\n Y(), Z2(`LayoutEffect`, e2, `LayoutEffect`);\n };\n var qe = (e2) => {\n Y(), Z2(`InsertionEffect`, e2, `InsertionEffect`);\n };\n var Je = (e2) => {\n Y(), Z2(`Effect`, e2, `Effect`);\n };\n var Ye = (e2) => {\n Y();\n let t2;\n typeof e2 == `object` && e2 && `current` in e2 && (t2 = e2.current), Z2(`ImperativeHandle`, t2, `ImperativeHandle`);\n };\n var Xe = (e2, t2) => {\n Z2(`DebugValue`, typeof t2 == `function` ? t2(e2) : e2, `DebugValue`);\n };\n var Ze = (e2) => {\n let t2 = Y();\n return Z2(`Callback`, t2 === null ? e2 : t2.memoizedState[0], `Callback`), e2;\n };\n var Qe = (e2) => {\n let t2 = Y(), n2 = t2 === null ? e2() : t2.memoizedState[0];\n return Z2(`Memo`, n2, `Memo`), n2;\n };\n var $e = (e2, t2) => {\n let n2 = Y();\n Y();\n let r2 = n2 === null ? t2() : n2.memoizedState;\n return Z2(`SyncExternalStore`, r2, `SyncExternalStore`), r2;\n };\n var et = () => {\n let e2 = Y();\n Y();\n let t2 = e2 === null ? false : e2.memoizedState;\n return Z2(`Transition`, t2, `Transition`), [t2, () => {}];\n };\n var tt = (e2) => {\n let t2 = Y(), n2 = t2 === null ? e2 : t2.memoizedState;\n return Z2(`DeferredValue`, n2, `DeferredValue`), n2;\n };\n var nt = () => {\n let e2 = Y(), t2 = e2 === null ? `` : e2.memoizedState;\n return Z2(`Id`, t2, `Id`), t2;\n };\n var rt = (e2) => {\n let t2 = H2;\n if (t2 == null)\n return [];\n let n2 = t2.updateQueue?.memoCache;\n if (n2 == null)\n return [];\n let r2 = n2.data[n2.index];\n return r2 === undefined && (r2 = n2.data[n2.index] = Array.from({ length: e2 }, () => Ie2)), n2.index++, r2;\n };\n var it = (e2) => {\n let t2 = Y(), n2 = t2 === null ? e2 : t2.memoizedState;\n return Z2(`Optimistic`, n2, `Optimistic`), [n2, () => {}];\n };\n var at = (e2, t2) => {\n let n2, r2 = null;\n if (e2 !== null) {\n let t3 = e2.memoizedState;\n if (typeof t3 == `object` && t3 && `then` in t3 && typeof t3.then == `function`) {\n let e3 = t3;\n switch (e3.status) {\n case `fulfilled`:\n n2 = e3.value;\n break;\n case `rejected`:\n r2 = e3.reason;\n break;\n default:\n r2 = q2, n2 = e3;\n }\n } else\n n2 = t3;\n } else\n n2 = t2;\n return { value: n2, error: r2 };\n };\n var ot = (e2) => (t2, n2) => {\n let r2 = Y();\n Y(), Y();\n let i3 = Error(), { value: a3, error: o3 } = at(r2, n2);\n if (B2.push({ displayName: null, primitive: e2, stackError: i3, value: a3, dispatcherHookName: e2 }), o3 !== null)\n throw o3;\n return [a3, () => {}, false];\n };\n var st = ot(`ActionState`);\n var Q2 = { readContext: X2, use: Be, useCallback: Ze, useContext: Ve, useEffect: Je, useImperativeHandle: Ye, useLayoutEffect: Ke, useInsertionEffect: qe, useMemo: Qe, useReducer: Ue, useRef: We, useState: He, useDebugValue: Xe, useDeferredValue: tt, useTransition: et, useSyncExternalStore: $e, useId: nt, useHostTransitionStatus: () => {\n let e2 = X2({ _currentValue: null });\n return Z2(`HostTransitionStatus`, e2, `HostTransitionStatus`), e2;\n }, useFormState: ot(`FormState`), useActionState: st, useOptimistic: it, useMemoCache: rt, useCacheRefresh: Ge, useEffectEvent: (e2) => (Y(), Z2(`EffectEvent`, e2, `EffectEvent`), e2) };\n var ct = typeof Proxy > `u` ? Q2 : new Proxy(Q2, { get(e2, t2) {\n if (Object.prototype.hasOwnProperty.call(e2, t2))\n return e2[t2];\n let n2 = Error(`Missing method in Dispatcher: ` + t2);\n throw n2.name = `ReactDebugToolsUnsupportedHookError`, n2;\n } });\n var gt = new Set([`Context`, `Context (use)`, `DebugValue`, `Promise`, `Unresolved`, `HostTransitionStatus`]);\n var kt = new Set([`Effect`, `LayoutEffect`, `InsertionEffect`, `ImperativeHandle`, `DebugValue`]);\n\n // dist/_bippy-entry-68953-1784086938398.js\n globalThis.__bippy = {\n getFiberFromHostInstance: Pe,\n getDisplayName: Ee,\n traverseFiber: A,\n isCompositeFiber: be,\n isHostFiber: b,\n getSource: Ae2,\n getOwnerStack: F,\n normalizeFileName: R2,\n isSourceFile: Ne2\n };\n})();\n";
|
|
1567
|
+
//#endregion
|
|
1568
|
+
//#region src/rrweb-recording.ts
|
|
1569
|
+
var activeRrwebRecordings = /* @__PURE__ */ new Map();
|
|
1570
|
+
function resolveTabIdFromSessionId(sessionId) {
|
|
1571
|
+
if (!sessionId) {
|
|
1572
|
+
for (const [tabId, tab] of store.getState().tabs) if (tab.state === "connected") return tabId;
|
|
1573
|
+
return;
|
|
1574
|
+
}
|
|
1575
|
+
return getTabBySessionId(sessionId)?.tabId;
|
|
1576
|
+
}
|
|
1577
|
+
function isRrwebRecorderResponse(value) {
|
|
1578
|
+
if (!value || typeof value !== "object") return false;
|
|
1579
|
+
const candidate = value;
|
|
1580
|
+
return typeof candidate.success === "boolean" && (candidate.error === void 0 || typeof candidate.error === "string") && (candidate.isRecording === void 0 || typeof candidate.isRecording === "boolean") && (candidate.startedAt === void 0 || typeof candidate.startedAt === "number") && (candidate.duration === void 0 || typeof candidate.duration === "number") && (candidate.url === void 0 || typeof candidate.url === "string") && (candidate.eventCount === void 0 || typeof candidate.eventCount === "number");
|
|
1581
|
+
}
|
|
1582
|
+
function isRrwebEvent(value) {
|
|
1583
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
1584
|
+
}
|
|
1585
|
+
function isRrwebEventBatchMessage(value) {
|
|
1586
|
+
if (!value || typeof value !== "object") return false;
|
|
1587
|
+
const candidate = value;
|
|
1588
|
+
return candidate.action === "playwriterRrwebEvents" && Array.isArray(candidate.events) && candidate.events.every(isRrwebEvent) && (candidate.final === void 0 || typeof candidate.final === "boolean");
|
|
1589
|
+
}
|
|
1590
|
+
function isRrwebCancelledMessage(value) {
|
|
1591
|
+
if (!value || typeof value !== "object") return false;
|
|
1592
|
+
return value.action === "playwriterRrwebCancelled";
|
|
1593
|
+
}
|
|
1594
|
+
function isFailedRrwebFrameMessageResult(result) {
|
|
1595
|
+
return !result.response.success;
|
|
1596
|
+
}
|
|
1597
|
+
async function getRrwebFrameIds(tabId) {
|
|
1598
|
+
try {
|
|
1599
|
+
const frames = await chrome.webNavigation.getAllFrames({ tabId });
|
|
1600
|
+
return Array.from(/* @__PURE__ */ new Set([0, ...(frames || []).map((frame) => {
|
|
1601
|
+
return frame.frameId;
|
|
1602
|
+
})]));
|
|
1603
|
+
} catch (error) {
|
|
1604
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1605
|
+
logger.debug("Could not enumerate rrweb recorder frames:", tabId, message);
|
|
1606
|
+
return [0];
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
async function sendFrameMessage(options) {
|
|
1610
|
+
const response = await chrome.tabs.sendMessage(options.tabId, options.message, { frameId: options.frameId });
|
|
1611
|
+
if (!isRrwebRecorderResponse(response)) return {
|
|
1612
|
+
success: false,
|
|
1613
|
+
error: "Invalid rrweb recorder response"
|
|
1614
|
+
};
|
|
1615
|
+
return response;
|
|
1616
|
+
}
|
|
1617
|
+
async function sendTabMessage(options) {
|
|
1618
|
+
return await sendFrameMessage({
|
|
1619
|
+
tabId: options.tabId,
|
|
1620
|
+
frameId: 0,
|
|
1621
|
+
message: options.message
|
|
1622
|
+
});
|
|
1623
|
+
}
|
|
1624
|
+
async function sendTabMessageToAllFrames(options) {
|
|
1625
|
+
const frameIds = await getRrwebFrameIds(options.tabId);
|
|
1626
|
+
return await Promise.all(frameIds.map(async (frameId) => {
|
|
1627
|
+
try {
|
|
1628
|
+
return {
|
|
1629
|
+
frameId,
|
|
1630
|
+
response: await sendFrameMessage({
|
|
1631
|
+
tabId: options.tabId,
|
|
1632
|
+
frameId,
|
|
1633
|
+
message: options.message
|
|
1634
|
+
})
|
|
1635
|
+
};
|
|
1636
|
+
} catch (error) {
|
|
1637
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1638
|
+
logger.debug("rrweb recorder frame message failed:", options.tabId, frameId, message);
|
|
1639
|
+
return {
|
|
1640
|
+
frameId,
|
|
1641
|
+
response: {
|
|
1642
|
+
success: false,
|
|
1643
|
+
error: message
|
|
1644
|
+
}
|
|
1645
|
+
};
|
|
1646
|
+
}
|
|
1647
|
+
}));
|
|
1648
|
+
}
|
|
1649
|
+
function pickPrimaryRecorderResponse(results) {
|
|
1650
|
+
const topFrameResponse = results.find((result) => {
|
|
1651
|
+
return result.frameId === 0 && result.response.success;
|
|
1652
|
+
})?.response;
|
|
1653
|
+
if (topFrameResponse) return topFrameResponse;
|
|
1654
|
+
const firstSuccessfulResponse = results.find((result) => {
|
|
1655
|
+
return result.response.success;
|
|
1656
|
+
})?.response;
|
|
1657
|
+
if (firstSuccessfulResponse) return firstSuccessfulResponse;
|
|
1658
|
+
return {
|
|
1659
|
+
success: false,
|
|
1660
|
+
error: results.find(isFailedRrwebFrameMessageResult)?.response.error || "No rrweb recorder frame responded"
|
|
1661
|
+
};
|
|
1662
|
+
}
|
|
1663
|
+
async function injectRrwebRecorder(tabId) {
|
|
1664
|
+
await chrome.scripting.executeScript({
|
|
1665
|
+
target: {
|
|
1666
|
+
tabId,
|
|
1667
|
+
allFrames: true
|
|
1668
|
+
},
|
|
1669
|
+
files: ["rrweb-recorder.js"]
|
|
1670
|
+
});
|
|
1671
|
+
}
|
|
1672
|
+
async function ensureRrwebRecorderReady(tabId) {
|
|
1673
|
+
try {
|
|
1674
|
+
await injectRrwebRecorder(tabId);
|
|
1675
|
+
} catch (error) {
|
|
1676
|
+
logger.debug("rrweb recorder all-frame injection failed before status check:", error);
|
|
1677
|
+
}
|
|
1678
|
+
try {
|
|
1679
|
+
const response = await sendTabMessage({
|
|
1680
|
+
tabId,
|
|
1681
|
+
message: { action: "playwriterRrwebStatus" }
|
|
1682
|
+
});
|
|
1683
|
+
if (response.success) return;
|
|
1684
|
+
throw new Error(response.error);
|
|
1685
|
+
} catch (error) {
|
|
1686
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1687
|
+
throw new Error("rrweb recorder content script is not available for this tab.", { cause: new Error(message) });
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
async function startRecorderInTab(options) {
|
|
1691
|
+
await ensureRrwebRecorderReady(options.tabId);
|
|
1692
|
+
return pickPrimaryRecorderResponse(await sendTabMessageToAllFrames({
|
|
1693
|
+
tabId: options.tabId,
|
|
1694
|
+
message: {
|
|
1695
|
+
action: "playwriterRrwebStart",
|
|
1696
|
+
params: {
|
|
1697
|
+
...options.params,
|
|
1698
|
+
startedAt: options.startedAt
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
}));
|
|
1702
|
+
}
|
|
1703
|
+
async function handleStartRrwebRecording(params) {
|
|
1704
|
+
const tabId = resolveTabIdFromSessionId(params.sessionId);
|
|
1705
|
+
if (!tabId) return {
|
|
1706
|
+
success: false,
|
|
1707
|
+
error: "No connected tab found for rrweb recording. Click the Tabwright extension icon on the tab you want to record."
|
|
1708
|
+
};
|
|
1709
|
+
if (activeRrwebRecordings.has(tabId)) return {
|
|
1710
|
+
success: false,
|
|
1711
|
+
error: "rrweb recording already in progress for this tab"
|
|
1712
|
+
};
|
|
1713
|
+
const tabInfo = store.getState().tabs.get(tabId);
|
|
1714
|
+
if (!tabInfo || tabInfo.state !== "connected") return {
|
|
1715
|
+
success: false,
|
|
1716
|
+
error: "Tab is not connected"
|
|
1717
|
+
};
|
|
1718
|
+
const startedAt = Date.now();
|
|
1719
|
+
logger.debug("Starting rrweb recording for tab:", tabId, "params:", params);
|
|
1720
|
+
try {
|
|
1721
|
+
const response = await startRecorderInTab({
|
|
1722
|
+
tabId,
|
|
1723
|
+
startedAt,
|
|
1724
|
+
params
|
|
1725
|
+
});
|
|
1726
|
+
if (!response.success) return {
|
|
1727
|
+
success: false,
|
|
1728
|
+
error: response.error || "Failed to start rrweb recorder"
|
|
1729
|
+
};
|
|
1730
|
+
activeRrwebRecordings.set(tabId, {
|
|
1731
|
+
tabId,
|
|
1732
|
+
startedAt,
|
|
1733
|
+
options: params,
|
|
1734
|
+
url: response.url
|
|
1735
|
+
});
|
|
1736
|
+
logger.debug("rrweb recording started for tab:", tabId, "url:", response.url);
|
|
1737
|
+
return {
|
|
1738
|
+
success: true,
|
|
1739
|
+
tabId,
|
|
1740
|
+
startedAt,
|
|
1741
|
+
url: response.url
|
|
1742
|
+
};
|
|
1743
|
+
} catch (error) {
|
|
1744
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1745
|
+
logger.error("Failed to start rrweb recording:", error);
|
|
1746
|
+
return {
|
|
1747
|
+
success: false,
|
|
1748
|
+
error: errorMessage
|
|
1749
|
+
};
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
async function handleStopRrwebRecording(params) {
|
|
1753
|
+
const tabId = resolveTabIdFromSessionId(params.sessionId);
|
|
1754
|
+
if (!tabId) return {
|
|
1755
|
+
success: false,
|
|
1756
|
+
error: "No connected tab found"
|
|
1757
|
+
};
|
|
1758
|
+
const recording = activeRrwebRecordings.get(tabId);
|
|
1759
|
+
if (!recording) return {
|
|
1760
|
+
success: false,
|
|
1761
|
+
error: "No active rrweb recording for this tab"
|
|
1762
|
+
};
|
|
1763
|
+
logger.debug("Stopping rrweb recording for tab:", tabId);
|
|
1764
|
+
try {
|
|
1765
|
+
const response = pickPrimaryRecorderResponse(await sendTabMessageToAllFrames({
|
|
1766
|
+
tabId,
|
|
1767
|
+
message: { action: "playwriterRrwebStop" }
|
|
1768
|
+
}));
|
|
1769
|
+
if (!response.success) return {
|
|
1770
|
+
success: false,
|
|
1771
|
+
error: response.error || "Failed to stop rrweb recorder"
|
|
1772
|
+
};
|
|
1773
|
+
activeRrwebRecordings.delete(tabId);
|
|
1774
|
+
const duration = response.duration || Date.now() - recording.startedAt;
|
|
1775
|
+
logger.debug("rrweb recording stopped for tab:", tabId, "duration:", duration);
|
|
1776
|
+
return {
|
|
1777
|
+
success: true,
|
|
1778
|
+
tabId,
|
|
1779
|
+
duration
|
|
1780
|
+
};
|
|
1781
|
+
} catch (error) {
|
|
1782
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1783
|
+
logger.error("Failed to stop rrweb recording:", error);
|
|
1784
|
+
return {
|
|
1785
|
+
success: false,
|
|
1786
|
+
error: errorMessage
|
|
1787
|
+
};
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
async function handleIsRrwebRecording(params) {
|
|
1791
|
+
const tabId = resolveTabIdFromSessionId(params.sessionId);
|
|
1792
|
+
if (!tabId) return { isRecording: false };
|
|
1793
|
+
const recording = activeRrwebRecordings.get(tabId);
|
|
1794
|
+
if (!recording) return {
|
|
1795
|
+
isRecording: false,
|
|
1796
|
+
tabId
|
|
1797
|
+
};
|
|
1798
|
+
try {
|
|
1799
|
+
const response = await sendTabMessage({
|
|
1800
|
+
tabId,
|
|
1801
|
+
message: { action: "playwriterRrwebStatus" }
|
|
1802
|
+
});
|
|
1803
|
+
return {
|
|
1804
|
+
isRecording: response.success ? Boolean(response.isRecording) : false,
|
|
1805
|
+
tabId,
|
|
1806
|
+
startedAt: recording.startedAt,
|
|
1807
|
+
url: response.success ? response.url || recording.url : recording.url
|
|
1808
|
+
};
|
|
1809
|
+
} catch {
|
|
1810
|
+
return {
|
|
1811
|
+
isRecording: false,
|
|
1812
|
+
tabId
|
|
1813
|
+
};
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
async function handleCancelRrwebRecording(params) {
|
|
1817
|
+
const tabId = resolveTabIdFromSessionId(params.sessionId);
|
|
1818
|
+
if (!tabId) return {
|
|
1819
|
+
success: false,
|
|
1820
|
+
error: "No connected tab found"
|
|
1821
|
+
};
|
|
1822
|
+
if (!activeRrwebRecordings.get(tabId)) return { success: true };
|
|
1823
|
+
logger.debug("Cancelling rrweb recording for tab:", tabId);
|
|
1824
|
+
try {
|
|
1825
|
+
await sendTabMessageToAllFrames({
|
|
1826
|
+
tabId,
|
|
1827
|
+
message: { action: "playwriterRrwebCancel" }
|
|
1828
|
+
});
|
|
1829
|
+
activeRrwebRecordings.delete(tabId);
|
|
1830
|
+
if (connectionManager.ws?.readyState === WebSocket.OPEN) sendMessage({
|
|
1831
|
+
method: "rrwebRecordingCancelled",
|
|
1832
|
+
params: { tabId }
|
|
1833
|
+
});
|
|
1834
|
+
return { success: true };
|
|
1835
|
+
} catch (error) {
|
|
1836
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1837
|
+
logger.error("Failed to cancel rrweb recording:", error);
|
|
1838
|
+
return {
|
|
1839
|
+
success: false,
|
|
1840
|
+
error: errorMessage
|
|
1841
|
+
};
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
async function resumeRrwebRecordingForNavigation(tabId) {
|
|
1845
|
+
const recording = activeRrwebRecordings.get(tabId);
|
|
1846
|
+
if (!recording) return;
|
|
1847
|
+
try {
|
|
1848
|
+
const response = await startRecorderInTab({
|
|
1849
|
+
tabId,
|
|
1850
|
+
startedAt: recording.startedAt,
|
|
1851
|
+
params: recording.options
|
|
1852
|
+
});
|
|
1853
|
+
if (response.success) activeRrwebRecordings.set(tabId, {
|
|
1854
|
+
...recording,
|
|
1855
|
+
url: response.url || recording.url
|
|
1856
|
+
});
|
|
1857
|
+
else logger.debug("Could not resume rrweb recorder after navigation:", response.error);
|
|
1858
|
+
} catch (error) {
|
|
1859
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1860
|
+
logger.debug("Could not resume rrweb recorder after navigation:", message);
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
async function cleanupRrwebRecordingForTab(tabId) {
|
|
1864
|
+
if (!activeRrwebRecordings.has(tabId)) return;
|
|
1865
|
+
try {
|
|
1866
|
+
await sendTabMessageToAllFrames({
|
|
1867
|
+
tabId,
|
|
1868
|
+
message: { action: "playwriterRrwebCancel" }
|
|
1869
|
+
});
|
|
1870
|
+
} catch (error) {
|
|
1871
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1872
|
+
logger.debug("Failed to cancel rrweb recorder during tab cleanup:", tabId, message);
|
|
1873
|
+
}
|
|
1874
|
+
activeRrwebRecordings.delete(tabId);
|
|
1875
|
+
if (connectionManager.ws?.readyState === WebSocket.OPEN) sendMessage({
|
|
1876
|
+
method: "rrwebRecordingCancelled",
|
|
1877
|
+
params: { tabId }
|
|
1878
|
+
});
|
|
1879
|
+
}
|
|
1880
|
+
//#endregion
|
|
1881
|
+
//#region src/background.ts
|
|
1882
|
+
var js = dedent;
|
|
1883
|
+
var RELAY_HOST = "127.0.0.1";
|
|
1884
|
+
var RELAY_PORT = 19988;
|
|
1885
|
+
var FAST_CDP_COMMAND_TIMEOUT_MS = /* @__PURE__ */ new Map([
|
|
1886
|
+
["Browser.getWindowForTarget", 1e4],
|
|
1887
|
+
["Page.enable", 1e4],
|
|
1888
|
+
["Page.getFrameTree", 1e4],
|
|
1889
|
+
["Page.setLifecycleEventsEnabled", 1e4],
|
|
1890
|
+
["Page.createIsolatedWorld", 1e4],
|
|
1891
|
+
["Page.setDownloadBehavior", 1e4],
|
|
1892
|
+
["Log.enable", 1e4],
|
|
1893
|
+
["Network.enable", 1e4],
|
|
1894
|
+
["Emulation.setFocusEmulationEnabled", 1e4],
|
|
1895
|
+
["Emulation.setEmulatedMedia", 1e4],
|
|
1896
|
+
["Runtime.runIfWaitingForDebugger", 1e4],
|
|
1897
|
+
["Target.setAutoAttach", 1e4]
|
|
1898
|
+
]);
|
|
1899
|
+
async function sendCommandWithTimeout(debuggee, method, params, timeout) {
|
|
1900
|
+
let timeoutId;
|
|
1901
|
+
try {
|
|
1902
|
+
return await Promise.race([chrome.debugger.sendCommand(debuggee, method, params), new Promise((_, reject) => {
|
|
1903
|
+
timeoutId = setTimeout(() => {
|
|
1904
|
+
reject(/* @__PURE__ */ new Error(`CDP command timed out after ${timeout}ms: ${method} (tab may be frozen/hibernated)`));
|
|
1905
|
+
}, timeout);
|
|
1906
|
+
})]);
|
|
1907
|
+
} finally {
|
|
1908
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
function sleep(ms) {
|
|
1912
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1913
|
+
}
|
|
1914
|
+
function relayHttpUrl(pathname) {
|
|
1915
|
+
return new URL(pathname, `http://${RELAY_HOST}:${RELAY_PORT}/`).toString();
|
|
1916
|
+
}
|
|
1917
|
+
async function fetchRelayHead(options) {
|
|
1918
|
+
try {
|
|
1919
|
+
return await fetch(relayHttpUrl(options.pathname), {
|
|
1920
|
+
method: "HEAD",
|
|
1921
|
+
signal: AbortSignal.timeout(2e3)
|
|
1922
|
+
});
|
|
1923
|
+
} catch (error) {
|
|
1924
|
+
throw new RelayConnectionProblemError({
|
|
1925
|
+
issue: "offline",
|
|
1926
|
+
cause: error
|
|
1927
|
+
});
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
async function checkRelayCompatibility() {
|
|
1931
|
+
if (!(await fetchRelayHead({ pathname: "/version" })).ok) throw new RelayConnectionProblemError({ issue: "unavailable" });
|
|
1932
|
+
}
|
|
1933
|
+
async function probeRelayReviewState() {
|
|
1934
|
+
try {
|
|
1935
|
+
const issue = getRelayReviewIssue({ statuses: (await Promise.all(["/capabilities", "/rrweb-recordings?limit=1"].map((pathname) => {
|
|
1936
|
+
return fetchRelayHead({ pathname });
|
|
1937
|
+
}))).map((response) => {
|
|
1938
|
+
return response.status;
|
|
1939
|
+
}) });
|
|
1940
|
+
if (!issue) return { status: "ready" };
|
|
1941
|
+
return {
|
|
1942
|
+
status: "degraded",
|
|
1943
|
+
issue,
|
|
1944
|
+
errorText: relayReviewIssueText({ issue })
|
|
1945
|
+
};
|
|
1946
|
+
} catch (cause) {
|
|
1947
|
+
const issue = "unavailable";
|
|
1948
|
+
const errorText = relayReviewIssueText({ issue });
|
|
1949
|
+
logger.debug("Relay review endpoints are unavailable:", cause);
|
|
1950
|
+
return {
|
|
1951
|
+
status: "degraded",
|
|
1952
|
+
issue,
|
|
1953
|
+
errorText
|
|
1954
|
+
};
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
function createInstallId() {
|
|
1958
|
+
const values = /* @__PURE__ */ new Uint32Array(2);
|
|
1959
|
+
crypto.getRandomValues(values);
|
|
1960
|
+
return Array.from(values).map((value) => {
|
|
1961
|
+
return value.toString(36);
|
|
1962
|
+
}).join("");
|
|
1963
|
+
}
|
|
1964
|
+
function browserNameFromBrands(brands) {
|
|
1965
|
+
const brandNames = brands.map((brand) => {
|
|
1966
|
+
return brand.brand.trim().toLowerCase();
|
|
1967
|
+
});
|
|
1968
|
+
if (brandNames.some((brand) => brand === "brave")) return "Brave";
|
|
1969
|
+
if (brandNames.some((brand) => brand === "microsoft edge")) return "Edge";
|
|
1970
|
+
if (brandNames.some((brand) => brand === "opera")) return "Opera";
|
|
1971
|
+
if (brandNames.some((brand) => brand === "vivaldi")) return "Vivaldi";
|
|
1972
|
+
if (brandNames.some((brand) => brand === "google chrome canary")) return "Chrome Canary";
|
|
1973
|
+
if (brandNames.some((brand) => brand === "google chrome")) return "Chrome";
|
|
1974
|
+
if (brandNames.some((brand) => brand === "chromium")) return "Chromium";
|
|
1975
|
+
return null;
|
|
1976
|
+
}
|
|
1977
|
+
async function detectBrowserName() {
|
|
1978
|
+
if (chrome.ghostPublicAPI) return "Ghost";
|
|
1979
|
+
const navigatorWithUaData = navigator;
|
|
1980
|
+
const brands = navigatorWithUaData.userAgentData?.brands;
|
|
1981
|
+
const highEntropyName = browserNameFromBrands((await navigatorWithUaData.userAgentData?.getHighEntropyValues?.(["fullVersionList"]).catch(() => {
|
|
1982
|
+
return null;
|
|
1983
|
+
}))?.fullVersionList || []);
|
|
1984
|
+
if (highEntropyName) return highEntropyName;
|
|
1985
|
+
if (brands && brands.length > 0) {
|
|
1986
|
+
const lowEntropyName = browserNameFromBrands(brands);
|
|
1987
|
+
if (lowEntropyName) return lowEntropyName;
|
|
1988
|
+
}
|
|
1989
|
+
const ua = navigator.userAgent.toLowerCase();
|
|
1990
|
+
if (ua.includes("edg/")) return "Edge";
|
|
1991
|
+
if (ua.includes("opr/")) return "Opera";
|
|
1992
|
+
if (ua.includes("vivaldi")) return "Vivaldi";
|
|
1993
|
+
if (ua.includes("brave")) return "Brave";
|
|
1994
|
+
if (ua.includes("chrome")) return "Chrome";
|
|
1995
|
+
return "Chromium";
|
|
1996
|
+
}
|
|
1997
|
+
var identityPromise = null;
|
|
1998
|
+
var installIdPromise = null;
|
|
1999
|
+
var tabSessionScope = (() => {
|
|
2000
|
+
const values = /* @__PURE__ */ new Uint32Array(2);
|
|
2001
|
+
crypto.getRandomValues(values);
|
|
2002
|
+
return Array.from(values).map((value) => {
|
|
2003
|
+
return value.toString(36);
|
|
2004
|
+
}).join("");
|
|
2005
|
+
})();
|
|
2006
|
+
async function getInstallId() {
|
|
2007
|
+
if (installIdPromise) return installIdPromise;
|
|
2008
|
+
installIdPromise = (async () => {
|
|
2009
|
+
const existing = await chrome.storage.local.get("playwriterInstallId");
|
|
2010
|
+
const storedInstallId = typeof existing.playwriterInstallId === "string" ? existing.playwriterInstallId : "";
|
|
2011
|
+
if (storedInstallId) return storedInstallId;
|
|
2012
|
+
const installId = createInstallId();
|
|
2013
|
+
await chrome.storage.local.set({ playwriterInstallId: installId });
|
|
2014
|
+
return installId;
|
|
2015
|
+
})().catch((error) => {
|
|
2016
|
+
installIdPromise = null;
|
|
2017
|
+
throw error;
|
|
2018
|
+
});
|
|
2019
|
+
return installIdPromise;
|
|
2020
|
+
}
|
|
2021
|
+
async function getExtensionIdentity() {
|
|
2022
|
+
if (identityPromise) return identityPromise;
|
|
2023
|
+
identityPromise = (async () => {
|
|
2024
|
+
const browser = await detectBrowserName();
|
|
2025
|
+
const installId = await getInstallId().catch(() => {
|
|
2026
|
+
return tabSessionScope;
|
|
2027
|
+
});
|
|
2028
|
+
try {
|
|
2029
|
+
const info = await chrome.identity.getProfileUserInfo({ accountStatus: "ANY" });
|
|
2030
|
+
return {
|
|
2031
|
+
browser,
|
|
2032
|
+
email: info.email || "",
|
|
2033
|
+
id: info.id || "",
|
|
2034
|
+
installId
|
|
2035
|
+
};
|
|
2036
|
+
} catch {
|
|
2037
|
+
return {
|
|
2038
|
+
browser,
|
|
2039
|
+
email: "",
|
|
2040
|
+
id: "",
|
|
2041
|
+
installId
|
|
2042
|
+
};
|
|
2043
|
+
}
|
|
2044
|
+
})();
|
|
2045
|
+
return identityPromise;
|
|
2046
|
+
}
|
|
2047
|
+
var TAB_GROUP_COLOR = "green";
|
|
2048
|
+
var TAB_GROUP_TITLE = "Tabwright";
|
|
2049
|
+
var childSessions = /* @__PURE__ */ new Map();
|
|
2050
|
+
var nextSessionId = 1;
|
|
2051
|
+
var tabGroupQueue = Promise.resolve();
|
|
2052
|
+
var autoAttachParams = null;
|
|
2053
|
+
var toolbarRecordingPendingRequests = /* @__PURE__ */ new Map();
|
|
2054
|
+
var nextToolbarRecordingRequestId = 1;
|
|
2055
|
+
function isToolbarRecordingMessage(message) {
|
|
2056
|
+
if (!message || typeof message !== "object") return false;
|
|
2057
|
+
const candidate = message;
|
|
2058
|
+
return candidate.action === "playwriterToolbarRecordingStatus" || candidate.action === "playwriterToolbarToggleRecording";
|
|
2059
|
+
}
|
|
2060
|
+
function isToolbarRecordingPortMessage(message) {
|
|
2061
|
+
if (!isToolbarRecordingMessage(message)) return false;
|
|
2062
|
+
return typeof message.requestId === "string";
|
|
2063
|
+
}
|
|
2064
|
+
function getToolbarTabSessionId(sender) {
|
|
2065
|
+
const tabId = sender.tab?.id;
|
|
2066
|
+
if (!tabId) return void 0;
|
|
2067
|
+
return store.getState().tabs.get(tabId)?.sessionId;
|
|
2068
|
+
}
|
|
2069
|
+
function handleToolbarRecordingResponse(message) {
|
|
2070
|
+
logger.debug("Toolbar recording response received:", message.params.requestId, message.params.result);
|
|
2071
|
+
const pending = toolbarRecordingPendingRequests.get(message.params.requestId);
|
|
2072
|
+
if (!pending) {
|
|
2073
|
+
logger.debug("Toolbar recording response has no pending request:", message.params.requestId);
|
|
2074
|
+
return;
|
|
2075
|
+
}
|
|
2076
|
+
clearTimeout(pending.timeoutId);
|
|
2077
|
+
toolbarRecordingPendingRequests.delete(message.params.requestId);
|
|
2078
|
+
pending.resolve(message.params.result);
|
|
2079
|
+
}
|
|
2080
|
+
function requestToolbarRecording(options) {
|
|
2081
|
+
const sessionId = getToolbarTabSessionId(options.sender);
|
|
2082
|
+
if (!sessionId) return Promise.resolve({
|
|
2083
|
+
success: false,
|
|
2084
|
+
isRecording: false,
|
|
2085
|
+
error: "Tabwright tab is not connected"
|
|
2086
|
+
});
|
|
2087
|
+
if (connectionManager.ws?.readyState !== WebSocket.OPEN) return Promise.resolve({
|
|
2088
|
+
success: false,
|
|
2089
|
+
isRecording: false,
|
|
2090
|
+
error: "Tabwright relay is not connected"
|
|
2091
|
+
});
|
|
2092
|
+
const requestId = `toolbar-recording-${Date.now()}-${nextToolbarRecordingRequestId++}`;
|
|
2093
|
+
logger.debug("Toolbar recording request:", requestId, options.action, "sessionId:", sessionId);
|
|
2094
|
+
return new Promise((resolve, reject) => {
|
|
2095
|
+
const timeoutId = setTimeout(() => {
|
|
2096
|
+
toolbarRecordingPendingRequests.delete(requestId);
|
|
2097
|
+
logger.debug("Toolbar recording request timed out:", requestId, options.action);
|
|
2098
|
+
reject(/* @__PURE__ */ new Error("Timed out waiting for relay recording response"));
|
|
2099
|
+
}, 12e4);
|
|
2100
|
+
toolbarRecordingPendingRequests.set(requestId, {
|
|
2101
|
+
resolve,
|
|
2102
|
+
reject,
|
|
2103
|
+
timeoutId
|
|
2104
|
+
});
|
|
2105
|
+
sendMessage({
|
|
2106
|
+
method: "toolbarRecordingRequest",
|
|
2107
|
+
params: {
|
|
2108
|
+
requestId,
|
|
2109
|
+
action: options.action,
|
|
2110
|
+
sessionId
|
|
2111
|
+
}
|
|
2112
|
+
});
|
|
2113
|
+
});
|
|
2114
|
+
}
|
|
2115
|
+
async function getToolbarRecordingStatus(sender) {
|
|
2116
|
+
return requestToolbarRecording({
|
|
2117
|
+
sender,
|
|
2118
|
+
action: "status"
|
|
2119
|
+
});
|
|
2120
|
+
}
|
|
2121
|
+
async function toggleToolbarRecording(sender) {
|
|
2122
|
+
return requestToolbarRecording({
|
|
2123
|
+
sender,
|
|
2124
|
+
action: "toggle"
|
|
2125
|
+
});
|
|
2126
|
+
}
|
|
2127
|
+
function postToolbarRecordingPortResponse(options) {
|
|
2128
|
+
try {
|
|
2129
|
+
options.port.postMessage(options.response);
|
|
2130
|
+
} catch (error) {
|
|
2131
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2132
|
+
logger.debug("Toolbar recording port response failed:", message);
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
function handleToolbarRecordingPortMessage(options) {
|
|
2136
|
+
const sender = options.port.sender;
|
|
2137
|
+
if (!sender) {
|
|
2138
|
+
postToolbarRecordingPortResponse({
|
|
2139
|
+
port: options.port,
|
|
2140
|
+
response: {
|
|
2141
|
+
requestId: options.message.requestId,
|
|
2142
|
+
result: {
|
|
2143
|
+
success: false,
|
|
2144
|
+
isRecording: false,
|
|
2145
|
+
error: "Tabwright toolbar port has no sender"
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
});
|
|
2149
|
+
return;
|
|
2150
|
+
}
|
|
2151
|
+
logger.debug("Toolbar port message received:", options.message.action, "tabId:", sender.tab?.id);
|
|
2152
|
+
(options.message.action === "playwriterToolbarToggleRecording" ? toggleToolbarRecording(sender) : getToolbarRecordingStatus(sender)).then((result) => {
|
|
2153
|
+
logger.debug("Toolbar port message response:", options.message.action, result);
|
|
2154
|
+
postToolbarRecordingPortResponse({
|
|
2155
|
+
port: options.port,
|
|
2156
|
+
response: {
|
|
2157
|
+
requestId: options.message.requestId,
|
|
2158
|
+
result
|
|
2159
|
+
}
|
|
2160
|
+
});
|
|
2161
|
+
}).catch((error) => {
|
|
2162
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2163
|
+
logger.debug("Toolbar port message failed:", options.message.action, errorMessage);
|
|
2164
|
+
postToolbarRecordingPortResponse({
|
|
2165
|
+
port: options.port,
|
|
2166
|
+
response: {
|
|
2167
|
+
requestId: options.message.requestId,
|
|
2168
|
+
result: {
|
|
2169
|
+
success: false,
|
|
2170
|
+
isRecording: false,
|
|
2171
|
+
error: errorMessage
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
});
|
|
2175
|
+
});
|
|
2176
|
+
}
|
|
2177
|
+
function rejectToolbarRecordingRequests(error) {
|
|
2178
|
+
for (const [requestId, pending] of toolbarRecordingPendingRequests) {
|
|
2179
|
+
clearTimeout(pending.timeoutId);
|
|
2180
|
+
pending.reject(error);
|
|
2181
|
+
toolbarRecordingPendingRequests.delete(requestId);
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
function isToolbarRecordingResponseMessage(message) {
|
|
2185
|
+
if (!message || typeof message !== "object") return false;
|
|
2186
|
+
const candidate = message;
|
|
2187
|
+
return candidate.method === "toolbarRecordingResponse" && typeof candidate.params?.requestId === "string" && Boolean(candidate.params.result);
|
|
2188
|
+
}
|
|
2189
|
+
function injectToolbar(tabId) {
|
|
2190
|
+
chrome.scripting.executeScript({
|
|
2191
|
+
target: {
|
|
2192
|
+
tabId,
|
|
2193
|
+
allFrames: false
|
|
2194
|
+
},
|
|
2195
|
+
world: "ISOLATED",
|
|
2196
|
+
func: initTabwrightToolbarBridge
|
|
2197
|
+
}).catch((err) => {
|
|
2198
|
+
logger.debug("Could not inject toolbar bridge (restricted page):", err.message);
|
|
2199
|
+
}).then(() => {
|
|
2200
|
+
return chrome.scripting.executeScript({
|
|
2201
|
+
target: {
|
|
2202
|
+
tabId,
|
|
2203
|
+
allFrames: false
|
|
2204
|
+
},
|
|
2205
|
+
world: "MAIN",
|
|
2206
|
+
func: initTabwrightToolbar
|
|
2207
|
+
});
|
|
2208
|
+
}).catch((err) => {
|
|
2209
|
+
logger.debug("Could not inject toolbar (restricted page):", err.message);
|
|
2210
|
+
});
|
|
2211
|
+
}
|
|
2212
|
+
var ConnectionManager = class {
|
|
2213
|
+
connectionOwnership = new ConnectionOwnership();
|
|
2214
|
+
connectionPromise = null;
|
|
2215
|
+
lastRelayReviewProbeAt = 0;
|
|
2216
|
+
preserveTabsOnDetach = false;
|
|
2217
|
+
get ws() {
|
|
2218
|
+
return this.connectionOwnership.current;
|
|
2219
|
+
}
|
|
2220
|
+
async refreshRelayReviewState(options) {
|
|
2221
|
+
this.lastRelayReviewProbeAt = Date.now();
|
|
2222
|
+
const relayReviewState = await probeRelayReviewState();
|
|
2223
|
+
if (!this.connectionOwnership.isCurrentConnection(options.socket)) return;
|
|
2224
|
+
store.setState({ relayReviewState });
|
|
2225
|
+
}
|
|
2226
|
+
async ensureConnection() {
|
|
2227
|
+
if (this.ws?.readyState === WebSocket.OPEN) return;
|
|
2228
|
+
if (store.getState().connectionState === "extension-replaced") throw new Error("Another Tabwright extension is already connected");
|
|
2229
|
+
if (this.connectionPromise) return this.connectionPromise;
|
|
2230
|
+
const GLOBAL_TIMEOUT_MS = 15e3;
|
|
2231
|
+
const generation = this.connectionOwnership.beginAttempt();
|
|
2232
|
+
let globalTimeoutId;
|
|
2233
|
+
const connectionPromise = Promise.race([this.connect({ generation }), new Promise((_, reject) => {
|
|
2234
|
+
globalTimeoutId = setTimeout(() => {
|
|
2235
|
+
this.connectionOwnership.invalidateAttempt(generation);
|
|
2236
|
+
reject(/* @__PURE__ */ new Error("Connection timeout (global)"));
|
|
2237
|
+
}, GLOBAL_TIMEOUT_MS);
|
|
2238
|
+
})]);
|
|
2239
|
+
this.connectionPromise = connectionPromise;
|
|
2240
|
+
try {
|
|
2241
|
+
await connectionPromise;
|
|
2242
|
+
} finally {
|
|
2243
|
+
if (globalTimeoutId) clearTimeout(globalTimeoutId);
|
|
2244
|
+
if (this.connectionPromise === connectionPromise) this.connectionPromise = null;
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
async connect(options) {
|
|
2248
|
+
logger.debug(`Waiting for server at http://${RELAY_HOST}:${RELAY_PORT}...`);
|
|
2249
|
+
const maxAttempts = 5;
|
|
2250
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) try {
|
|
2251
|
+
await fetch(relayHttpUrl("/"), {
|
|
2252
|
+
method: "HEAD",
|
|
2253
|
+
signal: AbortSignal.timeout(2e3)
|
|
2254
|
+
});
|
|
2255
|
+
logger.debug("Server is available");
|
|
2256
|
+
break;
|
|
2257
|
+
} catch (error) {
|
|
2258
|
+
if (attempt === maxAttempts - 1) throw new RelayConnectionProblemError({
|
|
2259
|
+
issue: "offline",
|
|
2260
|
+
cause: error
|
|
2261
|
+
});
|
|
2262
|
+
logger.debug(`Server not available, retrying... (attempt ${attempt + 1}/${maxAttempts})`);
|
|
2263
|
+
await sleep(1e3);
|
|
2264
|
+
}
|
|
2265
|
+
await checkRelayCompatibility();
|
|
2266
|
+
const identity = await getExtensionIdentity();
|
|
2267
|
+
if (!this.connectionOwnership.isCurrentAttempt(options.generation)) throw new Error("Connection attempt superseded");
|
|
2268
|
+
const relayUrl = new URL(`ws://${RELAY_HOST}:${RELAY_PORT}/extension`);
|
|
2269
|
+
if (identity.browser) relayUrl.searchParams.set("browser", identity.browser);
|
|
2270
|
+
if (identity.email) relayUrl.searchParams.set("email", identity.email);
|
|
2271
|
+
if (identity.id) relayUrl.searchParams.set("id", identity.id);
|
|
2272
|
+
if (identity.installId) relayUrl.searchParams.set("installId", identity.installId);
|
|
2273
|
+
relayUrl.searchParams.set("v", "1.0.0");
|
|
2274
|
+
relayUrl.searchParams.set("protocolVersion", String(1));
|
|
2275
|
+
relayUrl.searchParams.set("features", CURRENT_EXTENSION_FEATURES.join(","));
|
|
2276
|
+
logger.debug("Creating WebSocket connection to:", relayUrl);
|
|
2277
|
+
const socket = new WebSocket(relayUrl.toString());
|
|
2278
|
+
await new Promise((resolve, reject) => {
|
|
2279
|
+
let settled = false;
|
|
2280
|
+
const timeout = setTimeout(() => {
|
|
2281
|
+
if (settled) return;
|
|
2282
|
+
settled = true;
|
|
2283
|
+
logger.debug("WebSocket connection TIMEOUT after 5 seconds");
|
|
2284
|
+
try {
|
|
2285
|
+
socket.close();
|
|
2286
|
+
} catch {}
|
|
2287
|
+
reject(/* @__PURE__ */ new Error("Connection timeout"));
|
|
2288
|
+
}, 5e3);
|
|
2289
|
+
socket.onopen = () => {
|
|
2290
|
+
if (settled) return;
|
|
2291
|
+
settled = true;
|
|
2292
|
+
clearTimeout(timeout);
|
|
2293
|
+
if (!this.connectionOwnership.claimOpenedConnection({
|
|
2294
|
+
generation: options.generation,
|
|
2295
|
+
connection: socket
|
|
2296
|
+
})) {
|
|
2297
|
+
try {
|
|
2298
|
+
socket.close(1e3, "Connection attempt superseded");
|
|
2299
|
+
} catch {}
|
|
2300
|
+
reject(/* @__PURE__ */ new Error("Connection attempt superseded"));
|
|
2301
|
+
return;
|
|
2302
|
+
}
|
|
2303
|
+
logger.debug("WebSocket connected");
|
|
2304
|
+
resolve();
|
|
2305
|
+
};
|
|
2306
|
+
socket.onerror = (error) => {
|
|
2307
|
+
if (settled) return;
|
|
2308
|
+
settled = true;
|
|
2309
|
+
clearTimeout(timeout);
|
|
2310
|
+
if (!this.connectionOwnership.isCurrentAttempt(options.generation)) {
|
|
2311
|
+
reject(/* @__PURE__ */ new Error("Connection attempt superseded"));
|
|
2312
|
+
return;
|
|
2313
|
+
}
|
|
2314
|
+
logger.debug("WebSocket error during connection:", error);
|
|
2315
|
+
reject(/* @__PURE__ */ new Error("WebSocket connection failed"));
|
|
2316
|
+
};
|
|
2317
|
+
socket.onclose = (event) => {
|
|
2318
|
+
if (settled) return;
|
|
2319
|
+
settled = true;
|
|
2320
|
+
clearTimeout(timeout);
|
|
2321
|
+
if (!this.connectionOwnership.isCurrentAttempt(options.generation)) {
|
|
2322
|
+
reject(/* @__PURE__ */ new Error("Connection attempt superseded"));
|
|
2323
|
+
return;
|
|
2324
|
+
}
|
|
2325
|
+
logger.debug("WebSocket closed during connection:", {
|
|
2326
|
+
code: event.code,
|
|
2327
|
+
reason: event.reason
|
|
2328
|
+
});
|
|
2329
|
+
if (event.code === 4002 || event.reason === "Extension Already In Use") reject(/* @__PURE__ */ new Error("Extension Already In Use"));
|
|
2330
|
+
else reject(/* @__PURE__ */ new Error(`WebSocket closed: ${event.reason || event.code}`));
|
|
2331
|
+
};
|
|
2332
|
+
});
|
|
2333
|
+
const isCurrentSocket = () => {
|
|
2334
|
+
return this.connectionOwnership.isCurrentConnection(socket);
|
|
2335
|
+
};
|
|
2336
|
+
const sendCurrentSocketMessage = (message) => {
|
|
2337
|
+
if (!isCurrentSocket()) return;
|
|
2338
|
+
sendMessage(message);
|
|
2339
|
+
};
|
|
2340
|
+
socket.onmessage = async (event) => {
|
|
2341
|
+
if (!isCurrentSocket()) return;
|
|
2342
|
+
let message;
|
|
2343
|
+
try {
|
|
2344
|
+
message = JSON.parse(event.data);
|
|
2345
|
+
} catch (error) {
|
|
2346
|
+
logger.debug("Error parsing message:", error);
|
|
2347
|
+
sendCurrentSocketMessage({ error: {
|
|
2348
|
+
code: -32700,
|
|
2349
|
+
message: `Error parsing message: ${error.message}`
|
|
2350
|
+
} });
|
|
2351
|
+
return;
|
|
2352
|
+
}
|
|
2353
|
+
if (message.method === "ping") {
|
|
2354
|
+
sendCurrentSocketMessage({ method: "pong" });
|
|
2355
|
+
return;
|
|
2356
|
+
}
|
|
2357
|
+
if (isToolbarRecordingResponseMessage(message)) {
|
|
2358
|
+
handleToolbarRecordingResponse(message);
|
|
2359
|
+
return;
|
|
2360
|
+
}
|
|
2361
|
+
if (message.method === "createInitialTab") {
|
|
2362
|
+
try {
|
|
2363
|
+
logger.debug("Creating initial tab for Playwright client");
|
|
2364
|
+
const tab = await createTabInPreferredWindow({
|
|
2365
|
+
url: "about:blank",
|
|
2366
|
+
active: false
|
|
2367
|
+
});
|
|
2368
|
+
if (tab.id) {
|
|
2369
|
+
setTabConnecting(tab.id);
|
|
2370
|
+
const { targetInfo, sessionId } = await attachTab(tab.id, { skipAttachedEvent: true });
|
|
2371
|
+
logger.debug("Initial tab created and connected:", tab.id, "sessionId:", sessionId);
|
|
2372
|
+
sendCurrentSocketMessage({
|
|
2373
|
+
id: message.id,
|
|
2374
|
+
result: {
|
|
2375
|
+
success: true,
|
|
2376
|
+
tabId: tab.id,
|
|
2377
|
+
sessionId,
|
|
2378
|
+
targetInfo
|
|
2379
|
+
}
|
|
2380
|
+
});
|
|
2381
|
+
} else throw new Error("Failed to create tab - no tab ID returned");
|
|
2382
|
+
} catch (error) {
|
|
2383
|
+
logger.debug("Failed to create initial tab:", error);
|
|
2384
|
+
sendCurrentSocketMessage({
|
|
2385
|
+
id: message.id,
|
|
2386
|
+
error: error.message
|
|
2387
|
+
});
|
|
2388
|
+
}
|
|
2389
|
+
return;
|
|
2390
|
+
}
|
|
2391
|
+
if (message.method === "startRecording" || message.method === "stopRecording" || message.method === "cancelRecording") {
|
|
2392
|
+
sendCurrentSocketMessage({
|
|
2393
|
+
id: message.id,
|
|
2394
|
+
result: {
|
|
2395
|
+
success: false,
|
|
2396
|
+
error: "Legacy video recording has been removed. Use rrweb replay recording instead."
|
|
2397
|
+
}
|
|
2398
|
+
});
|
|
2399
|
+
return;
|
|
2400
|
+
}
|
|
2401
|
+
if (message.method === "isRecording") {
|
|
2402
|
+
sendCurrentSocketMessage({
|
|
2403
|
+
id: message.id,
|
|
2404
|
+
result: { isRecording: false }
|
|
2405
|
+
});
|
|
2406
|
+
return;
|
|
2407
|
+
}
|
|
2408
|
+
if (message.method === "startRrwebRecording") {
|
|
2409
|
+
try {
|
|
2410
|
+
const result = await handleStartRrwebRecording(message.params);
|
|
2411
|
+
sendCurrentSocketMessage({
|
|
2412
|
+
id: message.id,
|
|
2413
|
+
result
|
|
2414
|
+
});
|
|
2415
|
+
} catch (error) {
|
|
2416
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2417
|
+
logger.error("Failed to start rrweb recording:", error);
|
|
2418
|
+
sendCurrentSocketMessage({
|
|
2419
|
+
id: message.id,
|
|
2420
|
+
result: {
|
|
2421
|
+
success: false,
|
|
2422
|
+
error: errorMessage
|
|
2423
|
+
}
|
|
2424
|
+
});
|
|
2425
|
+
}
|
|
2426
|
+
return;
|
|
2427
|
+
}
|
|
2428
|
+
if (message.method === "stopRrwebRecording") {
|
|
2429
|
+
try {
|
|
2430
|
+
const result = await handleStopRrwebRecording(message.params);
|
|
2431
|
+
sendCurrentSocketMessage({
|
|
2432
|
+
id: message.id,
|
|
2433
|
+
result
|
|
2434
|
+
});
|
|
2435
|
+
} catch (error) {
|
|
2436
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2437
|
+
logger.error("Failed to stop rrweb recording:", error);
|
|
2438
|
+
sendCurrentSocketMessage({
|
|
2439
|
+
id: message.id,
|
|
2440
|
+
result: {
|
|
2441
|
+
success: false,
|
|
2442
|
+
error: errorMessage
|
|
2443
|
+
}
|
|
2444
|
+
});
|
|
2445
|
+
}
|
|
2446
|
+
return;
|
|
2447
|
+
}
|
|
2448
|
+
if (message.method === "isRrwebRecording") {
|
|
2449
|
+
try {
|
|
2450
|
+
const result = await handleIsRrwebRecording(message.params);
|
|
2451
|
+
sendCurrentSocketMessage({
|
|
2452
|
+
id: message.id,
|
|
2453
|
+
result
|
|
2454
|
+
});
|
|
2455
|
+
} catch (error) {
|
|
2456
|
+
logger.error("Failed to check rrweb recording status:", error);
|
|
2457
|
+
sendCurrentSocketMessage({
|
|
2458
|
+
id: message.id,
|
|
2459
|
+
result: { isRecording: false }
|
|
2460
|
+
});
|
|
2461
|
+
}
|
|
2462
|
+
return;
|
|
2463
|
+
}
|
|
2464
|
+
if (message.method === "cancelRrwebRecording") {
|
|
2465
|
+
try {
|
|
2466
|
+
const result = await handleCancelRrwebRecording(message.params);
|
|
2467
|
+
sendCurrentSocketMessage({
|
|
2468
|
+
id: message.id,
|
|
2469
|
+
result
|
|
2470
|
+
});
|
|
2471
|
+
} catch (error) {
|
|
2472
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2473
|
+
logger.error("Failed to cancel rrweb recording:", error);
|
|
2474
|
+
sendCurrentSocketMessage({
|
|
2475
|
+
id: message.id,
|
|
2476
|
+
result: {
|
|
2477
|
+
success: false,
|
|
2478
|
+
error: errorMessage
|
|
2479
|
+
}
|
|
2480
|
+
});
|
|
2481
|
+
}
|
|
2482
|
+
return;
|
|
2483
|
+
}
|
|
2484
|
+
if (message.method === "ghost-browser") {
|
|
2485
|
+
const params = message.params;
|
|
2486
|
+
const result = await handleGhostBrowserCommand(params, chrome);
|
|
2487
|
+
if (!result.success) logger.error("Ghost Browser API error:", result.error);
|
|
2488
|
+
if (result.success && params.namespace === "ghostPublicAPI" && params.method === "openTab") {
|
|
2489
|
+
const tabId = result.result;
|
|
2490
|
+
if (tabId) {
|
|
2491
|
+
logger.debug("Auto-connecting Ghost Browser tab:", tabId);
|
|
2492
|
+
setTabConnecting(tabId);
|
|
2493
|
+
await sleep(100);
|
|
2494
|
+
await attachTab(tabId);
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
sendCurrentSocketMessage({
|
|
2498
|
+
id: message.id,
|
|
2499
|
+
result
|
|
2500
|
+
});
|
|
2501
|
+
return;
|
|
2502
|
+
}
|
|
2503
|
+
const response = { id: message.id };
|
|
2504
|
+
try {
|
|
2505
|
+
response.result = await handleCommand(message);
|
|
2506
|
+
} catch (error) {
|
|
2507
|
+
logger.debug("Error handling command:", error);
|
|
2508
|
+
response.error = error.message;
|
|
2509
|
+
}
|
|
2510
|
+
sendCurrentSocketMessage(response);
|
|
2511
|
+
};
|
|
2512
|
+
socket.onclose = (event) => {
|
|
2513
|
+
if (!isCurrentSocket()) return;
|
|
2514
|
+
this.handleClose({
|
|
2515
|
+
socket,
|
|
2516
|
+
reason: event.reason,
|
|
2517
|
+
code: event.code
|
|
2518
|
+
});
|
|
2519
|
+
};
|
|
2520
|
+
socket.onerror = (event) => {
|
|
2521
|
+
if (!isCurrentSocket()) return;
|
|
2522
|
+
logger.debug("WebSocket error:", event);
|
|
2523
|
+
};
|
|
2524
|
+
chrome.debugger.onEvent.addListener(onDebuggerEvent);
|
|
2525
|
+
chrome.debugger.onDetach.addListener(onDebuggerDetach);
|
|
2526
|
+
this.refreshRelayReviewState({ socket });
|
|
2527
|
+
logger.debug("Connection established");
|
|
2528
|
+
}
|
|
2529
|
+
handleClose(options) {
|
|
2530
|
+
if (!this.connectionOwnership.releaseConnection(options.socket)) return;
|
|
2531
|
+
const { reason, code } = options;
|
|
2532
|
+
rejectToolbarRecordingRequests(/* @__PURE__ */ new Error(`Tabwright relay disconnected: ${reason || code}`));
|
|
2533
|
+
try {
|
|
2534
|
+
const mem = performance.memory;
|
|
2535
|
+
if (mem) {
|
|
2536
|
+
const formatMB = (b) => (b / 1024 / 1024).toFixed(2) + "MB";
|
|
2537
|
+
logger.warn(`DISCONNECT MEMORY: used=${formatMB(mem.usedJSHeapSize)} total=${formatMB(mem.totalJSHeapSize)} limit=${formatMB(mem.jsHeapSizeLimit)}`);
|
|
2538
|
+
}
|
|
2539
|
+
} catch {}
|
|
2540
|
+
logger.warn(`DISCONNECT: WS closed code=${code} reason=${reason || "none"} stack=${getCallStack()}`);
|
|
2541
|
+
chrome.debugger.onEvent.removeListener(onDebuggerEvent);
|
|
2542
|
+
chrome.debugger.onDetach.removeListener(onDebuggerDetach);
|
|
2543
|
+
const isExtensionReplaced = reason === "Extension Replaced" || code === 4001;
|
|
2544
|
+
const isExtensionInUse = reason === "Extension Already In Use" || code === 4002;
|
|
2545
|
+
this.preserveTabsOnDetach = !(isExtensionReplaced || isExtensionInUse);
|
|
2546
|
+
const { tabs } = store.getState();
|
|
2547
|
+
for (const [tabId] of tabs) chrome.debugger.detach({ tabId }).catch((err) => {
|
|
2548
|
+
logger.debug("Error detaching from tab:", tabId, err.message);
|
|
2549
|
+
});
|
|
2550
|
+
childSessions.clear();
|
|
2551
|
+
if (isExtensionReplaced) {
|
|
2552
|
+
logger.debug("Disconnected: another Tabwright extension connected (this one was idle)");
|
|
2553
|
+
store.setState({
|
|
2554
|
+
tabs: /* @__PURE__ */ new Map(),
|
|
2555
|
+
connectionState: "extension-replaced",
|
|
2556
|
+
errorText: "Another Tabwright extension took over the connection"
|
|
2557
|
+
});
|
|
2558
|
+
return;
|
|
2559
|
+
}
|
|
2560
|
+
if (isExtensionInUse) {
|
|
2561
|
+
logger.debug("Rejected: another Tabwright extension is actively in use");
|
|
2562
|
+
store.setState({
|
|
2563
|
+
tabs: /* @__PURE__ */ new Map(),
|
|
2564
|
+
connectionState: "extension-replaced",
|
|
2565
|
+
errorText: "Another Tabwright extension is actively in use"
|
|
2566
|
+
});
|
|
2567
|
+
return;
|
|
2568
|
+
}
|
|
2569
|
+
store.setState((state) => {
|
|
2570
|
+
const newTabs = new Map(state.tabs);
|
|
2571
|
+
for (const [tabId, tab] of newTabs) newTabs.set(tabId, {
|
|
2572
|
+
...tab,
|
|
2573
|
+
state: "connecting"
|
|
2574
|
+
});
|
|
2575
|
+
return {
|
|
2576
|
+
tabs: newTabs,
|
|
2577
|
+
connectionState: "idle",
|
|
2578
|
+
errorText: void 0
|
|
2579
|
+
};
|
|
2580
|
+
});
|
|
2581
|
+
}
|
|
2582
|
+
async maintainLoop() {
|
|
2583
|
+
while (true) {
|
|
2584
|
+
const openSocket = this.ws;
|
|
2585
|
+
if (openSocket?.readyState === WebSocket.OPEN) {
|
|
2586
|
+
if (Date.now() - this.lastRelayReviewProbeAt >= 3e4) this.refreshRelayReviewState({ socket: openSocket });
|
|
2587
|
+
await sleep(1e3);
|
|
2588
|
+
continue;
|
|
2589
|
+
}
|
|
2590
|
+
if (store.getState().connectionState === "extension-replaced") {
|
|
2591
|
+
try {
|
|
2592
|
+
const data = await (await fetch(`http://${RELAY_HOST}:${RELAY_PORT}/extension/status`, {
|
|
2593
|
+
method: "GET",
|
|
2594
|
+
signal: AbortSignal.timeout(2e3)
|
|
2595
|
+
})).json();
|
|
2596
|
+
if (!data.connected) {
|
|
2597
|
+
store.setState({
|
|
2598
|
+
connectionState: "idle",
|
|
2599
|
+
errorText: void 0
|
|
2600
|
+
});
|
|
2601
|
+
logger.debug("Extension slot is free (connected:", data.connected, "activeTargets:", data.activeTargets, "), cleared error state");
|
|
2602
|
+
} else logger.debug("Extension slot still taken (activeTargets:", data.activeTargets, "), will retry...");
|
|
2603
|
+
} catch {
|
|
2604
|
+
logger.debug("Server not available, will retry...");
|
|
2605
|
+
}
|
|
2606
|
+
await sleep(3e3);
|
|
2607
|
+
continue;
|
|
2608
|
+
}
|
|
2609
|
+
const currentTabs = store.getState().tabs;
|
|
2610
|
+
if (Array.from(currentTabs.values()).some((t) => t.state === "connected")) store.setState((state) => {
|
|
2611
|
+
const newTabs = new Map(state.tabs);
|
|
2612
|
+
for (const [tabId, tab] of newTabs) if (tab.state === "connected") newTabs.set(tabId, {
|
|
2613
|
+
...tab,
|
|
2614
|
+
state: "connecting"
|
|
2615
|
+
});
|
|
2616
|
+
return { tabs: newTabs };
|
|
2617
|
+
});
|
|
2618
|
+
try {
|
|
2619
|
+
await this.ensureConnection();
|
|
2620
|
+
store.setState({ connectionState: "connected" });
|
|
2621
|
+
const tabsToReattach = Array.from(store.getState().tabs.entries()).filter(([_, tab]) => tab.state === "connecting").map(([tabId]) => tabId);
|
|
2622
|
+
for (const tabId of tabsToReattach) {
|
|
2623
|
+
const currentTab = store.getState().tabs.get(tabId);
|
|
2624
|
+
if (!currentTab || currentTab.state !== "connecting") {
|
|
2625
|
+
logger.debug("Skipping reattach, tab state changed:", tabId, currentTab?.state);
|
|
2626
|
+
continue;
|
|
2627
|
+
}
|
|
2628
|
+
try {
|
|
2629
|
+
await chrome.tabs.get(tabId);
|
|
2630
|
+
await attachTab(tabId);
|
|
2631
|
+
logger.debug("Successfully re-attached tab:", tabId);
|
|
2632
|
+
} catch (error) {
|
|
2633
|
+
logger.debug("Failed to re-attach tab:", tabId, error.message);
|
|
2634
|
+
store.setState((state) => {
|
|
2635
|
+
const newTabs = new Map(state.tabs);
|
|
2636
|
+
newTabs.delete(tabId);
|
|
2637
|
+
return { tabs: newTabs };
|
|
2638
|
+
});
|
|
2639
|
+
}
|
|
2640
|
+
}
|
|
2641
|
+
this.preserveTabsOnDetach = false;
|
|
2642
|
+
} catch (error) {
|
|
2643
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2644
|
+
logger.debug("Connection attempt failed:", errorMessage);
|
|
2645
|
+
if (error instanceof RelayConnectionProblemError) store.setState({
|
|
2646
|
+
connectionState: "relay-warning",
|
|
2647
|
+
errorText: error.message
|
|
2648
|
+
});
|
|
2649
|
+
else if (errorMessage === "Extension Already In Use") store.setState({
|
|
2650
|
+
connectionState: "extension-replaced",
|
|
2651
|
+
errorText: "Another Tabwright extension is actively in use"
|
|
2652
|
+
});
|
|
2653
|
+
else store.setState({ connectionState: "idle" });
|
|
2654
|
+
}
|
|
2655
|
+
await sleep(3e3);
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
2658
|
+
};
|
|
2659
|
+
var connectionManager = new ConnectionManager();
|
|
2660
|
+
var store = createStore(() => ({
|
|
2661
|
+
tabs: /* @__PURE__ */ new Map(),
|
|
2662
|
+
connectionState: "idle",
|
|
2663
|
+
relayReviewState: { status: "unknown" },
|
|
2664
|
+
currentTabId: void 0,
|
|
2665
|
+
preferredWindowId: void 0,
|
|
2666
|
+
errorText: void 0
|
|
2667
|
+
}));
|
|
2668
|
+
globalThis.toggleExtensionForActiveTab = toggleExtensionForActiveTab;
|
|
2669
|
+
globalThis.disconnectEverything = disconnectEverything;
|
|
2670
|
+
globalThis.getExtensionState = () => store.getState();
|
|
2671
|
+
var MAX_LOG_STRING_LENGTH = 2e3;
|
|
2672
|
+
function truncateLogString(value) {
|
|
2673
|
+
if (value.length <= MAX_LOG_STRING_LENGTH) return value;
|
|
2674
|
+
return `${value.slice(0, MAX_LOG_STRING_LENGTH)}…[truncated ${value.length - MAX_LOG_STRING_LENGTH} chars]`;
|
|
2675
|
+
}
|
|
2676
|
+
function safeSerialize(arg) {
|
|
2677
|
+
if (arg === void 0) return "undefined";
|
|
2678
|
+
if (arg === null) return "null";
|
|
2679
|
+
if (typeof arg === "function") return `[Function: ${arg.name || "anonymous"}]`;
|
|
2680
|
+
if (typeof arg === "symbol") return String(arg);
|
|
2681
|
+
if (typeof arg === "string") return truncateLogString(arg);
|
|
2682
|
+
if (arg instanceof Error) return truncateLogString(arg.stack || arg.message || String(arg));
|
|
2683
|
+
if (typeof arg === "object") try {
|
|
2684
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
2685
|
+
return truncateLogString(JSON.stringify(arg, (key, value) => {
|
|
2686
|
+
if (typeof value === "object" && value !== null) {
|
|
2687
|
+
if (seen.has(value)) return "[Circular]";
|
|
2688
|
+
seen.add(value);
|
|
2689
|
+
if (value instanceof Map) return {
|
|
2690
|
+
dataType: "Map",
|
|
2691
|
+
value: Array.from(value.entries())
|
|
2692
|
+
};
|
|
2693
|
+
if (value instanceof Set) return {
|
|
2694
|
+
dataType: "Set",
|
|
2695
|
+
value: Array.from(value.values())
|
|
2696
|
+
};
|
|
2697
|
+
}
|
|
2698
|
+
return value;
|
|
2699
|
+
}));
|
|
2700
|
+
} catch {
|
|
2701
|
+
return truncateLogString(String(arg));
|
|
2702
|
+
}
|
|
2703
|
+
return truncateLogString(String(arg));
|
|
2704
|
+
}
|
|
2705
|
+
function sendLog(level, args) {
|
|
2706
|
+
sendMessage({
|
|
2707
|
+
method: "log",
|
|
2708
|
+
params: {
|
|
2709
|
+
level,
|
|
2710
|
+
args: args.map(safeSerialize)
|
|
2711
|
+
}
|
|
2712
|
+
});
|
|
2713
|
+
}
|
|
2714
|
+
var logger = {
|
|
2715
|
+
log: (...args) => {
|
|
2716
|
+
console.log(...args);
|
|
2717
|
+
sendLog("log", args);
|
|
2718
|
+
},
|
|
2719
|
+
debug: (...args) => {
|
|
2720
|
+
console.debug(...args);
|
|
2721
|
+
sendLog("debug", args);
|
|
2722
|
+
},
|
|
2723
|
+
info: (...args) => {
|
|
2724
|
+
console.info(...args);
|
|
2725
|
+
sendLog("info", args);
|
|
2726
|
+
},
|
|
2727
|
+
warn: (...args) => {
|
|
2728
|
+
console.warn(...args);
|
|
2729
|
+
sendLog("warn", args);
|
|
2730
|
+
},
|
|
2731
|
+
error: (...args) => {
|
|
2732
|
+
console.error(...args);
|
|
2733
|
+
sendLog("error", args);
|
|
2734
|
+
}
|
|
2735
|
+
};
|
|
2736
|
+
function getCallStack() {
|
|
2737
|
+
return ((/* @__PURE__ */ new Error()).stack || "").split("\n").slice(2, 6).join(" <- ").replace(/\s+/g, " ");
|
|
2738
|
+
}
|
|
2739
|
+
self.addEventListener("error", (event) => {
|
|
2740
|
+
const stack = event.error?.stack || `${event.message} at ${event.filename}:${event.lineno}:${event.colno}`;
|
|
2741
|
+
logger.error("Uncaught error:", stack);
|
|
2742
|
+
});
|
|
2743
|
+
self.addEventListener("unhandledrejection", (event) => {
|
|
2744
|
+
const reason = event.reason;
|
|
2745
|
+
const stack = reason?.stack || String(reason);
|
|
2746
|
+
logger.error("Unhandled promise rejection:", stack);
|
|
2747
|
+
});
|
|
2748
|
+
var messageCount = 0;
|
|
2749
|
+
function sendMessage(message) {
|
|
2750
|
+
if (connectionManager.ws?.readyState === WebSocket.OPEN) try {
|
|
2751
|
+
connectionManager.ws.send(JSON.stringify(message));
|
|
2752
|
+
if (++messageCount % 100 === 0) checkMemory();
|
|
2753
|
+
} catch (error) {
|
|
2754
|
+
console.debug("ERROR sending message:", error, "message type:", message.method || "response");
|
|
2755
|
+
}
|
|
2756
|
+
}
|
|
2757
|
+
async function getPreferredWindowId() {
|
|
2758
|
+
const { preferredWindowId, currentTabId } = store.getState();
|
|
2759
|
+
if (preferredWindowId !== void 0) try {
|
|
2760
|
+
await chrome.windows.get(preferredWindowId);
|
|
2761
|
+
return preferredWindowId;
|
|
2762
|
+
} catch {
|
|
2763
|
+
store.setState({ preferredWindowId: void 0 });
|
|
2764
|
+
}
|
|
2765
|
+
if (currentTabId !== void 0) try {
|
|
2766
|
+
const tab = await chrome.tabs.get(currentTabId);
|
|
2767
|
+
if (tab.windowId !== void 0) return tab.windowId;
|
|
2768
|
+
} catch {}
|
|
2769
|
+
try {
|
|
2770
|
+
return (await chrome.windows.getLastFocused({ populate: false })).id;
|
|
2771
|
+
} catch {
|
|
2772
|
+
return;
|
|
2773
|
+
}
|
|
2774
|
+
}
|
|
2775
|
+
async function createTabInPreferredWindow(options) {
|
|
2776
|
+
const windowId = await getPreferredWindowId();
|
|
2777
|
+
const createProperties = {
|
|
2778
|
+
url: options.url,
|
|
2779
|
+
active: options.active,
|
|
2780
|
+
...windowId !== void 0 ? { windowId } : {}
|
|
2781
|
+
};
|
|
2782
|
+
try {
|
|
2783
|
+
return await chrome.tabs.create(createProperties);
|
|
2784
|
+
} catch (error) {
|
|
2785
|
+
logger.debug("Could not create tab in preferred window, falling back:", error.message);
|
|
2786
|
+
return await chrome.tabs.create({
|
|
2787
|
+
url: options.url,
|
|
2788
|
+
active: options.active
|
|
2789
|
+
});
|
|
2790
|
+
}
|
|
2791
|
+
}
|
|
2792
|
+
async function syncTabGroup() {
|
|
2793
|
+
try {
|
|
2794
|
+
const { connectionState } = store.getState();
|
|
2795
|
+
const isRelayConnected = connectionState === "connected";
|
|
2796
|
+
const connectedTabIds = Array.from(store.getState().tabs.entries()).filter(([_, info]) => info.state === "connected" || info.state === "connecting" && isRelayConnected).map(([tabId]) => tabId);
|
|
2797
|
+
const existingGroups = await chrome.tabGroups.query({ title: TAB_GROUP_TITLE });
|
|
2798
|
+
if (connectedTabIds.length === 0) {
|
|
2799
|
+
for (const group of existingGroups) {
|
|
2800
|
+
const tabIdsToUngroup = (await chrome.tabs.query({ groupId: group.id })).map((t) => t.id).filter((id) => id !== void 0);
|
|
2801
|
+
if (tabIdsToUngroup.length > 0) await chrome.tabs.ungroup(tabIdsToUngroup);
|
|
2802
|
+
logger.debug("Cleared Tabwright group:", group.id);
|
|
2803
|
+
}
|
|
2804
|
+
return;
|
|
2805
|
+
}
|
|
2806
|
+
let groupId = existingGroups[0]?.id;
|
|
2807
|
+
if (existingGroups.length > 1) {
|
|
2808
|
+
const [keep, ...duplicates] = existingGroups;
|
|
2809
|
+
groupId = keep.id;
|
|
2810
|
+
for (const group of duplicates) {
|
|
2811
|
+
const tabIdsToUngroup = (await chrome.tabs.query({ groupId: group.id })).map((t) => t.id).filter((id) => id !== void 0);
|
|
2812
|
+
if (tabIdsToUngroup.length > 0) await chrome.tabs.ungroup(tabIdsToUngroup);
|
|
2813
|
+
logger.debug("Removed duplicate Tabwright group:", group.id);
|
|
2814
|
+
}
|
|
2815
|
+
}
|
|
2816
|
+
const tabsInGroup = (await chrome.tabs.query({})).filter((t) => t.groupId === groupId && t.id !== void 0);
|
|
2817
|
+
const tabIdsInGroup = new Set(tabsInGroup.map((t) => t.id));
|
|
2818
|
+
const tabsToAdd = connectedTabIds.filter((id) => !tabIdsInGroup.has(id));
|
|
2819
|
+
const tabsToRemove = Array.from(tabIdsInGroup).filter((id) => !connectedTabIds.includes(id));
|
|
2820
|
+
if (tabsToRemove.length > 0) try {
|
|
2821
|
+
await chrome.tabs.ungroup(tabsToRemove);
|
|
2822
|
+
logger.debug("Removed tabs from group:", tabsToRemove);
|
|
2823
|
+
} catch (e) {
|
|
2824
|
+
logger.debug("Failed to ungroup tabs:", tabsToRemove, e.message);
|
|
2825
|
+
}
|
|
2826
|
+
if (tabsToAdd.length > 0) if (groupId === void 0) {
|
|
2827
|
+
const newGroupId = await chrome.tabs.group({ tabIds: tabsToAdd });
|
|
2828
|
+
await chrome.tabGroups.update(newGroupId, {
|
|
2829
|
+
title: TAB_GROUP_TITLE,
|
|
2830
|
+
color: TAB_GROUP_COLOR
|
|
2831
|
+
});
|
|
2832
|
+
logger.debug("Created tab group:", newGroupId, "with tabs:", tabsToAdd);
|
|
2833
|
+
} else {
|
|
2834
|
+
await chrome.tabs.group({
|
|
2835
|
+
tabIds: tabsToAdd,
|
|
2836
|
+
groupId
|
|
2837
|
+
});
|
|
2838
|
+
await chrome.tabGroups.update(groupId, {
|
|
2839
|
+
title: TAB_GROUP_TITLE,
|
|
2840
|
+
color: TAB_GROUP_COLOR
|
|
2841
|
+
});
|
|
2842
|
+
logger.debug("Added tabs to existing group:", tabsToAdd);
|
|
2843
|
+
}
|
|
2844
|
+
else if (groupId !== void 0) await chrome.tabGroups.update(groupId, {
|
|
2845
|
+
title: TAB_GROUP_TITLE,
|
|
2846
|
+
color: TAB_GROUP_COLOR
|
|
2847
|
+
});
|
|
2848
|
+
} catch (error) {
|
|
2849
|
+
logger.debug("Failed to sync tab group:", error.message);
|
|
2850
|
+
}
|
|
2851
|
+
}
|
|
2852
|
+
function getTabBySessionId(sessionId) {
|
|
2853
|
+
for (const [tabId, tab] of store.getState().tabs) if (tab.sessionId === sessionId) return {
|
|
2854
|
+
tabId,
|
|
2855
|
+
tab
|
|
2856
|
+
};
|
|
2857
|
+
}
|
|
2858
|
+
function getTabByTargetId(targetId) {
|
|
2859
|
+
for (const [tabId, tab] of store.getState().tabs) if (tab.targetId === targetId) return {
|
|
2860
|
+
tabId,
|
|
2861
|
+
tab
|
|
2862
|
+
};
|
|
2863
|
+
}
|
|
2864
|
+
function emitChildDetachesForTab(tabId) {
|
|
2865
|
+
Array.from(childSessions.entries()).filter(([_, parentTab]) => parentTab.tabId === tabId).forEach(([childSessionId, parentTab]) => {
|
|
2866
|
+
sendMessage({
|
|
2867
|
+
method: "forwardCDPEvent",
|
|
2868
|
+
params: {
|
|
2869
|
+
method: "Target.detachedFromTarget",
|
|
2870
|
+
params: parentTab.targetId ? {
|
|
2871
|
+
sessionId: childSessionId,
|
|
2872
|
+
targetId: parentTab.targetId
|
|
2873
|
+
} : { sessionId: childSessionId }
|
|
2874
|
+
}
|
|
2875
|
+
});
|
|
2876
|
+
logger.debug("Cleaning up child session:", childSessionId, "for tab:", tabId);
|
|
2877
|
+
childSessions.delete(childSessionId);
|
|
2878
|
+
});
|
|
2879
|
+
}
|
|
2880
|
+
function getTabForCommand(msg) {
|
|
2881
|
+
const sessionId = msg.params.sessionId;
|
|
2882
|
+
if (sessionId) {
|
|
2883
|
+
const found = getTabBySessionId(sessionId);
|
|
2884
|
+
if (found) return found;
|
|
2885
|
+
const child = childSessions.get(sessionId);
|
|
2886
|
+
if (child) {
|
|
2887
|
+
const tab = store.getState().tabs.get(child.tabId);
|
|
2888
|
+
if (tab) return {
|
|
2889
|
+
tabId: child.tabId,
|
|
2890
|
+
tab
|
|
2891
|
+
};
|
|
2892
|
+
}
|
|
2893
|
+
}
|
|
2894
|
+
const paramsSessionId = msg.params.params && "sessionId" in msg.params.params && typeof msg.params.params.sessionId === "string" ? msg.params.params.sessionId : void 0;
|
|
2895
|
+
if (paramsSessionId) {
|
|
2896
|
+
const found = getTabBySessionId(paramsSessionId);
|
|
2897
|
+
if (found) return found;
|
|
2898
|
+
const child = childSessions.get(paramsSessionId);
|
|
2899
|
+
if (child) {
|
|
2900
|
+
const tab = store.getState().tabs.get(child.tabId);
|
|
2901
|
+
if (tab) return {
|
|
2902
|
+
tabId: child.tabId,
|
|
2903
|
+
tab
|
|
2904
|
+
};
|
|
2905
|
+
}
|
|
2906
|
+
}
|
|
2907
|
+
const targetId = msg.params.params && "targetId" in msg.params.params && typeof msg.params.params.targetId === "string" ? msg.params.params.targetId : void 0;
|
|
2908
|
+
if (targetId) return getTabByTargetId(targetId);
|
|
2909
|
+
}
|
|
2910
|
+
async function handleCommand(msg) {
|
|
2911
|
+
if (msg.method !== "forwardCDPCommand") return;
|
|
2912
|
+
const resolved = getTabForCommand(msg);
|
|
2913
|
+
let targetTabId = resolved?.tabId;
|
|
2914
|
+
let targetTab = resolved?.tab;
|
|
2915
|
+
const debuggee = targetTabId ? { tabId: targetTabId } : void 0;
|
|
2916
|
+
if (msg.params.method === "Target.setAutoAttach" && !msg.params.sessionId) {
|
|
2917
|
+
const params = msg.params.params;
|
|
2918
|
+
if (!params) return {};
|
|
2919
|
+
autoAttachParams = params;
|
|
2920
|
+
const connectedTabIds = Array.from(store.getState().tabs.entries()).filter(([_, info]) => info.state === "connected").map(([tabId]) => tabId);
|
|
2921
|
+
await Promise.all(connectedTabIds.map(async (tabId) => {
|
|
2922
|
+
try {
|
|
2923
|
+
await sendCommandWithTimeout({ tabId }, "Target.setAutoAttach", params, 1e4);
|
|
2924
|
+
} catch (error) {
|
|
2925
|
+
logger.debug("Failed to set auto-attach for tab:", tabId, error);
|
|
2926
|
+
}
|
|
2927
|
+
}));
|
|
2928
|
+
return {};
|
|
2929
|
+
}
|
|
2930
|
+
switch (msg.params.method) {
|
|
2931
|
+
case "Runtime.enable": {
|
|
2932
|
+
if (!debuggee) throw new Error(`No debuggee found for Runtime.enable (sessionId: ${msg.params.sessionId})`);
|
|
2933
|
+
const runtimeSession = {
|
|
2934
|
+
...debuggee,
|
|
2935
|
+
sessionId: msg.params.sessionId !== targetTab?.sessionId ? msg.params.sessionId : void 0
|
|
2936
|
+
};
|
|
2937
|
+
try {
|
|
2938
|
+
await sendCommandWithTimeout(runtimeSession, "Runtime.disable", void 0, 1e4);
|
|
2939
|
+
await sleep(50);
|
|
2940
|
+
} catch (e) {
|
|
2941
|
+
logger.debug("Error disabling Runtime (ignoring):", e);
|
|
2942
|
+
}
|
|
2943
|
+
return await sendCommandWithTimeout(runtimeSession, "Runtime.enable", msg.params.params, 1e4);
|
|
2944
|
+
}
|
|
2945
|
+
case "Target.createTarget": {
|
|
2946
|
+
const url = msg.params.params?.url || "about:blank";
|
|
2947
|
+
logger.debug("Creating new tab with URL:", url);
|
|
2948
|
+
const tab = await createTabInPreferredWindow({
|
|
2949
|
+
url,
|
|
2950
|
+
active: false
|
|
2951
|
+
});
|
|
2952
|
+
if (!tab.id) throw new Error("Failed to create tab");
|
|
2953
|
+
setTabConnecting(tab.id);
|
|
2954
|
+
logger.debug("Created tab:", tab.id, "waiting for it to load...");
|
|
2955
|
+
await sleep(100);
|
|
2956
|
+
const { targetInfo } = await attachTab(tab.id);
|
|
2957
|
+
return { targetId: targetInfo.targetId };
|
|
2958
|
+
}
|
|
2959
|
+
case "Target.closeTarget":
|
|
2960
|
+
if (!targetTabId) {
|
|
2961
|
+
logger.log(`Target not found: ${msg.params.params?.targetId}`);
|
|
2962
|
+
return { success: false };
|
|
2963
|
+
}
|
|
2964
|
+
await chrome.tabs.remove(targetTabId);
|
|
2965
|
+
return { success: true };
|
|
2966
|
+
}
|
|
2967
|
+
if (!debuggee || !targetTab) {
|
|
2968
|
+
if (msg.params.method === "Target.detachFromTarget") return {};
|
|
2969
|
+
throw new Error(`No tab found for method ${msg.params.method} sessionId: ${msg.params.sessionId} params: ${JSON.stringify(msg.params.params || null)}`);
|
|
2970
|
+
}
|
|
2971
|
+
logger.debug("CDP command:", msg.params.method, "for tab:", targetTabId);
|
|
2972
|
+
const debuggerSession = {
|
|
2973
|
+
...debuggee,
|
|
2974
|
+
sessionId: msg.params.sessionId !== targetTab.sessionId ? msg.params.sessionId : void 0
|
|
2975
|
+
};
|
|
2976
|
+
const timeout = FAST_CDP_COMMAND_TIMEOUT_MS.get(msg.params.method);
|
|
2977
|
+
if (timeout) return await sendCommandWithTimeout(debuggerSession, msg.params.method, msg.params.params, timeout);
|
|
2978
|
+
return await chrome.debugger.sendCommand(debuggerSession, msg.params.method, msg.params.params);
|
|
2979
|
+
}
|
|
2980
|
+
function onDebuggerEvent(source, method, params) {
|
|
2981
|
+
const tab = source.tabId ? store.getState().tabs.get(source.tabId) : void 0;
|
|
2982
|
+
if (!tab) return;
|
|
2983
|
+
logger.debug("Forwarding CDP event:", method, "from tab:", source.tabId);
|
|
2984
|
+
if (method === "Target.attachedToTarget" && params?.sessionId) {
|
|
2985
|
+
const targetUrl = params.targetInfo?.url;
|
|
2986
|
+
if (isRestrictedUrl(targetUrl)) {
|
|
2987
|
+
logger.debug("Ignoring restricted child target:", targetUrl, "sessionId:", params.sessionId, "for tab:", source.tabId);
|
|
2988
|
+
if (source.tabId) chrome.debugger.sendCommand({ tabId: source.tabId }, "Target.detachFromTarget", { sessionId: params.sessionId }).catch((e) => {
|
|
2989
|
+
logger.debug("Failed to detach restricted child target (expected):", e);
|
|
2990
|
+
});
|
|
2991
|
+
return;
|
|
2992
|
+
}
|
|
2993
|
+
logger.debug("Child target attached:", params.sessionId, "for tab:", source.tabId);
|
|
2994
|
+
const targetId = params.targetInfo?.targetId;
|
|
2995
|
+
childSessions.set(params.sessionId, {
|
|
2996
|
+
tabId: source.tabId,
|
|
2997
|
+
targetId
|
|
2998
|
+
});
|
|
2999
|
+
}
|
|
3000
|
+
if (method === "Target.detachedFromTarget" && params?.sessionId) {
|
|
3001
|
+
const mainTab = getTabBySessionId(params.sessionId);
|
|
3002
|
+
if (mainTab) {
|
|
3003
|
+
logger.debug("Main tab detached via CDP event:", mainTab.tabId, "sessionId:", params.sessionId);
|
|
3004
|
+
store.setState((state) => {
|
|
3005
|
+
const newTabs = new Map(state.tabs);
|
|
3006
|
+
newTabs.delete(mainTab.tabId);
|
|
3007
|
+
return { tabs: newTabs };
|
|
3008
|
+
});
|
|
3009
|
+
emitChildDetachesForTab(mainTab.tabId);
|
|
3010
|
+
} else {
|
|
3011
|
+
logger.debug("Child target detached:", params.sessionId);
|
|
3012
|
+
childSessions.delete(params.sessionId);
|
|
3013
|
+
}
|
|
3014
|
+
}
|
|
3015
|
+
sendMessage({
|
|
3016
|
+
method: "forwardCDPEvent",
|
|
3017
|
+
params: {
|
|
3018
|
+
sessionId: source.sessionId || tab.sessionId,
|
|
3019
|
+
method,
|
|
3020
|
+
params
|
|
3021
|
+
}
|
|
3022
|
+
});
|
|
3023
|
+
}
|
|
3024
|
+
function onDebuggerDetach(source, reason) {
|
|
3025
|
+
const tabId = source.tabId;
|
|
3026
|
+
if (!tabId || !store.getState().tabs.has(tabId)) {
|
|
3027
|
+
logger.debug("Ignoring debugger detach event for untracked tab:", tabId);
|
|
3028
|
+
return;
|
|
3029
|
+
}
|
|
3030
|
+
if (connectionManager.preserveTabsOnDetach) {
|
|
3031
|
+
logger.debug("Ignoring debugger detach during relay reconnect:", tabId, reason);
|
|
3032
|
+
return;
|
|
3033
|
+
}
|
|
3034
|
+
logger.warn(`DISCONNECT: onDebuggerDetach tabId=${tabId} reason=${reason}`);
|
|
3035
|
+
const detachTabFromPlaywright = (detachedTabId, tab) => {
|
|
3036
|
+
sendMessage({
|
|
3037
|
+
method: "forwardCDPEvent",
|
|
3038
|
+
params: {
|
|
3039
|
+
method: "Target.detachedFromTarget",
|
|
3040
|
+
params: {
|
|
3041
|
+
sessionId: tab.sessionId,
|
|
3042
|
+
targetId: tab.targetId
|
|
3043
|
+
}
|
|
3044
|
+
}
|
|
3045
|
+
});
|
|
3046
|
+
emitChildDetachesForTab(detachedTabId);
|
|
3047
|
+
};
|
|
3048
|
+
if (reason === chrome.debugger.DetachReason.CANCELED_BY_USER) {
|
|
3049
|
+
for (const [detachedTabId, tab] of store.getState().tabs.entries()) detachTabFromPlaywright(detachedTabId, tab);
|
|
3050
|
+
store.setState({
|
|
3051
|
+
tabs: /* @__PURE__ */ new Map(),
|
|
3052
|
+
connectionState: "idle",
|
|
3053
|
+
errorText: void 0
|
|
3054
|
+
});
|
|
3055
|
+
return;
|
|
3056
|
+
}
|
|
3057
|
+
const tab = store.getState().tabs.get(tabId);
|
|
3058
|
+
if (tab) detachTabFromPlaywright(tabId, tab);
|
|
3059
|
+
store.setState((state) => {
|
|
3060
|
+
const newTabs = new Map(state.tabs);
|
|
3061
|
+
newTabs.delete(tabId);
|
|
3062
|
+
return { tabs: newTabs };
|
|
3063
|
+
});
|
|
3064
|
+
}
|
|
3065
|
+
async function removeRestrictedIframes(tabId) {
|
|
3066
|
+
try {
|
|
3067
|
+
const totalRemoved = (await chrome.scripting.executeScript({
|
|
3068
|
+
target: {
|
|
3069
|
+
tabId,
|
|
3070
|
+
allFrames: true
|
|
3071
|
+
},
|
|
3072
|
+
func: (ownExtIds) => {
|
|
3073
|
+
const roots = [document];
|
|
3074
|
+
document.querySelectorAll("*").forEach((el) => {
|
|
3075
|
+
const shadow = el.shadowRoot;
|
|
3076
|
+
if (shadow) roots.push(shadow);
|
|
3077
|
+
});
|
|
3078
|
+
let removed = 0;
|
|
3079
|
+
for (const root of roots) root.querySelectorAll("iframe").forEach((iframe) => {
|
|
3080
|
+
const src = iframe.src || iframe.getAttribute("src") || "";
|
|
3081
|
+
if (!src.startsWith("chrome-extension://")) return;
|
|
3082
|
+
const extId = src.replace("chrome-extension://", "").split("/")[0];
|
|
3083
|
+
if (ownExtIds.includes(extId)) return;
|
|
3084
|
+
iframe.remove();
|
|
3085
|
+
removed++;
|
|
3086
|
+
});
|
|
3087
|
+
return removed;
|
|
3088
|
+
},
|
|
3089
|
+
args: [OUR_EXTENSION_IDS]
|
|
3090
|
+
})).reduce((sum, r) => sum + (r.result ?? 0), 0);
|
|
3091
|
+
if (totalRemoved > 0) logger.debug(`Removed ${totalRemoved} restricted chrome-extension:// iframe(s) from tab:`, tabId);
|
|
3092
|
+
return totalRemoved;
|
|
3093
|
+
} catch (e) {
|
|
3094
|
+
logger.debug("Could not remove restricted iframes (expected on some pages):", e.message);
|
|
3095
|
+
return 0;
|
|
3096
|
+
}
|
|
3097
|
+
}
|
|
3098
|
+
async function attachTab(tabId, { skipAttachedEvent = false } = {}) {
|
|
3099
|
+
const debuggee = { tabId };
|
|
3100
|
+
let debuggerAttached = false;
|
|
3101
|
+
try {
|
|
3102
|
+
logger.debug("Attaching debugger to tab:", tabId);
|
|
3103
|
+
const maxAttachAttempts = 3;
|
|
3104
|
+
for (let attempt = 1; attempt <= maxAttachAttempts; attempt++) try {
|
|
3105
|
+
await chrome.debugger.attach(debuggee, "1.3");
|
|
3106
|
+
break;
|
|
3107
|
+
} catch (attachError) {
|
|
3108
|
+
const msg = attachError.message ?? "";
|
|
3109
|
+
if (!(msg.includes("chrome-extension://") || msg.includes("different extension")) || attempt === maxAttachAttempts) throw attachError;
|
|
3110
|
+
logger.debug(`Debugger attach blocked by chrome-extension:// iframe (attempt ${attempt}/${maxAttachAttempts}), removing and retrying:`, tabId);
|
|
3111
|
+
await removeRestrictedIframes(tabId);
|
|
3112
|
+
await sleep(50);
|
|
3113
|
+
}
|
|
3114
|
+
debuggerAttached = true;
|
|
3115
|
+
logger.debug("Debugger attached successfully to tab:", tabId);
|
|
3116
|
+
await chrome.debugger.sendCommand(debuggee, "Page.enable");
|
|
3117
|
+
if (autoAttachParams) try {
|
|
3118
|
+
await chrome.debugger.sendCommand(debuggee, "Target.setAutoAttach", autoAttachParams);
|
|
3119
|
+
} catch (error) {
|
|
3120
|
+
logger.debug("Failed to apply auto-attach for tab:", tabId, error);
|
|
3121
|
+
}
|
|
3122
|
+
const contextMenuScript = js`
|
|
3123
|
+
document.addEventListener('contextmenu', (e) => {
|
|
3124
|
+
window.__playwriter_lastRightClicked = e.target;
|
|
3125
|
+
}, true);
|
|
3126
|
+
`;
|
|
3127
|
+
await chrome.debugger.sendCommand(debuggee, "Page.addScriptToEvaluateOnNewDocument", { source: contextMenuScript });
|
|
3128
|
+
await chrome.debugger.sendCommand(debuggee, "Runtime.evaluate", { expression: contextMenuScript });
|
|
3129
|
+
try {
|
|
3130
|
+
await chrome.debugger.sendCommand(debuggee, "Page.addScriptToEvaluateOnNewDocument", { source: ghost_cursor_client_default });
|
|
3131
|
+
await chrome.debugger.sendCommand(debuggee, "Runtime.evaluate", { expression: ghost_cursor_client_default });
|
|
3132
|
+
} catch (err) {
|
|
3133
|
+
logger.debug("Could not inject ghost cursor (restricted page):", err.message);
|
|
3134
|
+
}
|
|
3135
|
+
const targetInfo = (await chrome.debugger.sendCommand(debuggee, "Target.getTargetInfo")).targetInfo;
|
|
3136
|
+
if (!targetInfo.url || targetInfo.url === "" || targetInfo.url === ":") logger.error("WARNING: Target.attachedToTarget will be sent with empty URL! tabId:", tabId, "targetInfo:", JSON.stringify(targetInfo));
|
|
3137
|
+
const attachOrder = nextSessionId;
|
|
3138
|
+
const sessionId = `pw-tab-${tabSessionScope}-${nextSessionId++}`;
|
|
3139
|
+
store.setState((state) => {
|
|
3140
|
+
const newTabs = new Map(state.tabs);
|
|
3141
|
+
newTabs.set(tabId, {
|
|
3142
|
+
sessionId,
|
|
3143
|
+
targetId: targetInfo.targetId,
|
|
3144
|
+
state: "connected",
|
|
3145
|
+
attachOrder
|
|
3146
|
+
});
|
|
3147
|
+
return {
|
|
3148
|
+
tabs: newTabs,
|
|
3149
|
+
connectionState: "connected",
|
|
3150
|
+
errorText: void 0
|
|
3151
|
+
};
|
|
3152
|
+
});
|
|
3153
|
+
if (!skipAttachedEvent) sendMessage({
|
|
3154
|
+
method: "forwardCDPEvent",
|
|
3155
|
+
params: {
|
|
3156
|
+
method: "Target.attachedToTarget",
|
|
3157
|
+
params: {
|
|
3158
|
+
sessionId,
|
|
3159
|
+
targetInfo: {
|
|
3160
|
+
...targetInfo,
|
|
3161
|
+
attached: true
|
|
3162
|
+
},
|
|
3163
|
+
waitingForDebugger: false
|
|
3164
|
+
}
|
|
3165
|
+
}
|
|
3166
|
+
});
|
|
3167
|
+
logger.debug("Tab attached successfully:", tabId, "sessionId:", sessionId, "targetId:", targetInfo.targetId, "url:", targetInfo.url, "skipAttachedEvent:", skipAttachedEvent);
|
|
3168
|
+
injectToolbar(tabId);
|
|
3169
|
+
return {
|
|
3170
|
+
targetInfo,
|
|
3171
|
+
sessionId
|
|
3172
|
+
};
|
|
3173
|
+
} catch (error) {
|
|
3174
|
+
if (debuggerAttached) {
|
|
3175
|
+
logger.debug("Cleaning up debugger after partial attach failure:", tabId);
|
|
3176
|
+
chrome.debugger.detach(debuggee).catch(() => {});
|
|
3177
|
+
}
|
|
3178
|
+
throw error;
|
|
3179
|
+
}
|
|
3180
|
+
}
|
|
3181
|
+
function detachTab(tabId, shouldDetachDebugger) {
|
|
3182
|
+
const tab = store.getState().tabs.get(tabId);
|
|
3183
|
+
if (!tab) {
|
|
3184
|
+
logger.debug("detachTab: tab not found in map:", tabId);
|
|
3185
|
+
return;
|
|
3186
|
+
}
|
|
3187
|
+
cleanupRrwebRecordingForTab(tabId);
|
|
3188
|
+
chrome.scripting.executeScript({
|
|
3189
|
+
target: { tabId },
|
|
3190
|
+
world: "MAIN",
|
|
3191
|
+
func: () => {
|
|
3192
|
+
window.__playwriterToolbarDestroy?.();
|
|
3193
|
+
}
|
|
3194
|
+
}).catch(() => {});
|
|
3195
|
+
chrome.scripting.executeScript({
|
|
3196
|
+
target: { tabId },
|
|
3197
|
+
world: "MAIN",
|
|
3198
|
+
func: () => {
|
|
3199
|
+
globalThis.__playwriterGhostCursor?.disable?.();
|
|
3200
|
+
}
|
|
3201
|
+
}).catch(() => {});
|
|
3202
|
+
logger.warn(`DISCONNECT: detachTab tabId=${tabId} shouldDetach=${shouldDetachDebugger} stack=${getCallStack()}`);
|
|
3203
|
+
if (tab.sessionId && tab.targetId) sendMessage({
|
|
3204
|
+
method: "forwardCDPEvent",
|
|
3205
|
+
params: {
|
|
3206
|
+
method: "Target.detachedFromTarget",
|
|
3207
|
+
params: {
|
|
3208
|
+
sessionId: tab.sessionId,
|
|
3209
|
+
targetId: tab.targetId
|
|
3210
|
+
}
|
|
3211
|
+
}
|
|
3212
|
+
});
|
|
3213
|
+
store.setState((state) => {
|
|
3214
|
+
const newTabs = new Map(state.tabs);
|
|
3215
|
+
newTabs.delete(tabId);
|
|
3216
|
+
return { tabs: newTabs };
|
|
3217
|
+
});
|
|
3218
|
+
emitChildDetachesForTab(tabId);
|
|
3219
|
+
if (shouldDetachDebugger) chrome.debugger.detach({ tabId }).catch((err) => {
|
|
3220
|
+
logger.debug("Error detaching debugger from tab:", tabId, err.message);
|
|
3221
|
+
});
|
|
3222
|
+
}
|
|
3223
|
+
async function connectTab(tabId) {
|
|
3224
|
+
try {
|
|
3225
|
+
logger.debug(`Starting connection to tab ${tabId}`);
|
|
3226
|
+
setTabConnecting(tabId);
|
|
3227
|
+
await connectionManager.ensureConnection();
|
|
3228
|
+
await attachTab(tabId);
|
|
3229
|
+
logger.debug(`Successfully connected to tab ${tabId}`);
|
|
3230
|
+
} catch (error) {
|
|
3231
|
+
logger.debug(`Failed to connect to tab ${tabId}:`, error);
|
|
3232
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
3233
|
+
const isExtensionInUse = errorMessage === "Extension Already In Use" || errorMessage === "Another Tabwright extension is already connected";
|
|
3234
|
+
const isWsError = errorMessage === "Connection timeout" || errorMessage.startsWith("WebSocket");
|
|
3235
|
+
if (isExtensionInUse) {
|
|
3236
|
+
logger.debug(`Another extension is in use, entering polling mode`);
|
|
3237
|
+
store.setState((state) => {
|
|
3238
|
+
const newTabs = new Map(state.tabs);
|
|
3239
|
+
newTabs.delete(tabId);
|
|
3240
|
+
return {
|
|
3241
|
+
tabs: newTabs,
|
|
3242
|
+
connectionState: "extension-replaced",
|
|
3243
|
+
errorText: "Another Tabwright extension is actively in use"
|
|
3244
|
+
};
|
|
3245
|
+
});
|
|
3246
|
+
} else if (error instanceof RelayConnectionProblemError || isWsError) {
|
|
3247
|
+
logger.debug(`Relay connection problem, showing warning for tab ${tabId}`);
|
|
3248
|
+
store.setState((state) => {
|
|
3249
|
+
const newTabs = new Map(state.tabs);
|
|
3250
|
+
newTabs.delete(tabId);
|
|
3251
|
+
return {
|
|
3252
|
+
tabs: newTabs,
|
|
3253
|
+
connectionState: "relay-warning",
|
|
3254
|
+
errorText: error instanceof RelayConnectionProblemError ? error.message : relayIssueText({ issue: "offline" })
|
|
3255
|
+
};
|
|
3256
|
+
});
|
|
3257
|
+
} else {
|
|
3258
|
+
let tabStillExists = true;
|
|
3259
|
+
try {
|
|
3260
|
+
await chrome.tabs.get(tabId);
|
|
3261
|
+
} catch {
|
|
3262
|
+
tabStillExists = false;
|
|
3263
|
+
}
|
|
3264
|
+
if (!tabStillExists) {
|
|
3265
|
+
logger.debug(`Tab ${tabId} was closed during connect, dropping error state`);
|
|
3266
|
+
store.setState((state) => {
|
|
3267
|
+
const newTabs = new Map(state.tabs);
|
|
3268
|
+
newTabs.delete(tabId);
|
|
3269
|
+
return { tabs: newTabs };
|
|
3270
|
+
});
|
|
3271
|
+
return;
|
|
3272
|
+
}
|
|
3273
|
+
if (!store.getState().tabs.has(tabId)) {
|
|
3274
|
+
logger.debug(`Tab ${tabId} was detached during connect, dropping error state`);
|
|
3275
|
+
return;
|
|
3276
|
+
}
|
|
3277
|
+
store.setState((state) => {
|
|
3278
|
+
const newTabs = new Map(state.tabs);
|
|
3279
|
+
newTabs.set(tabId, {
|
|
3280
|
+
state: "error",
|
|
3281
|
+
errorText: `Error: ${errorMessage}`
|
|
3282
|
+
});
|
|
3283
|
+
return { tabs: newTabs };
|
|
3284
|
+
});
|
|
3285
|
+
}
|
|
3286
|
+
}
|
|
3287
|
+
}
|
|
3288
|
+
function setTabConnecting(tabId) {
|
|
3289
|
+
store.setState((state) => {
|
|
3290
|
+
const newTabs = new Map(state.tabs);
|
|
3291
|
+
const existing = newTabs.get(tabId);
|
|
3292
|
+
newTabs.set(tabId, {
|
|
3293
|
+
...existing,
|
|
3294
|
+
state: "connecting"
|
|
3295
|
+
});
|
|
3296
|
+
return { tabs: newTabs };
|
|
3297
|
+
});
|
|
3298
|
+
}
|
|
3299
|
+
async function disconnectTab(tabId) {
|
|
3300
|
+
logger.debug(`Disconnecting tab ${tabId}`);
|
|
3301
|
+
const { tabs } = store.getState();
|
|
3302
|
+
if (!tabs.has(tabId)) {
|
|
3303
|
+
logger.debug("Tab not in tabs map, ignoring disconnect");
|
|
3304
|
+
return;
|
|
3305
|
+
}
|
|
3306
|
+
detachTab(tabId, true);
|
|
3307
|
+
}
|
|
3308
|
+
async function toggleExtensionForActiveTab() {
|
|
3309
|
+
const tab = (await chrome.tabs.query({
|
|
3310
|
+
active: true,
|
|
3311
|
+
currentWindow: true
|
|
3312
|
+
}))[0];
|
|
3313
|
+
if (!tab?.id) throw new Error("No active tab found");
|
|
3314
|
+
await onActionClicked(tab);
|
|
3315
|
+
await new Promise((resolve) => {
|
|
3316
|
+
const check = () => {
|
|
3317
|
+
if (store.getState().tabs.get(tab.id)?.state === "connecting") {
|
|
3318
|
+
setTimeout(check, 100);
|
|
3319
|
+
return;
|
|
3320
|
+
}
|
|
3321
|
+
resolve();
|
|
3322
|
+
};
|
|
3323
|
+
check();
|
|
3324
|
+
});
|
|
3325
|
+
const state = store.getState();
|
|
3326
|
+
return {
|
|
3327
|
+
isConnected: state.tabs.has(tab.id) && state.tabs.get(tab.id)?.state === "connected",
|
|
3328
|
+
state
|
|
3329
|
+
};
|
|
3330
|
+
}
|
|
3331
|
+
async function disconnectEverything() {
|
|
3332
|
+
tabGroupQueue = tabGroupQueue.then(async () => {
|
|
3333
|
+
const { tabs } = store.getState();
|
|
3334
|
+
for (const tabId of tabs.keys()) await disconnectTab(tabId);
|
|
3335
|
+
});
|
|
3336
|
+
await tabGroupQueue;
|
|
3337
|
+
}
|
|
3338
|
+
async function resetDebugger() {
|
|
3339
|
+
let targets = await chrome.debugger.getTargets();
|
|
3340
|
+
targets = targets.filter((x) => x.tabId && x.attached);
|
|
3341
|
+
logger.log(`found ${targets.length} existing debugger targets. detaching them before background script starts`);
|
|
3342
|
+
for (const target of targets) await chrome.debugger.detach({ tabId: target.tabId });
|
|
3343
|
+
}
|
|
3344
|
+
var OUR_EXTENSION_IDS = ["jfeammnjpkecdekppnclgkkffahnhfhe", "pebbngnfojnignonigcnkdilknapkgid"];
|
|
3345
|
+
function isRestrictedUrl(url) {
|
|
3346
|
+
if (!url) return false;
|
|
3347
|
+
if (url.startsWith("chrome-extension://")) {
|
|
3348
|
+
const extensionId = url.replace("chrome-extension://", "").split("/")[0];
|
|
3349
|
+
return !OUR_EXTENSION_IDS.includes(extensionId);
|
|
3350
|
+
}
|
|
3351
|
+
return [
|
|
3352
|
+
"chrome://",
|
|
3353
|
+
"devtools://",
|
|
3354
|
+
"edge://",
|
|
3355
|
+
"https://chrome.google.com/",
|
|
3356
|
+
"https://chromewebstore.google.com/"
|
|
3357
|
+
].some((prefix) => url.startsWith(prefix));
|
|
3358
|
+
}
|
|
3359
|
+
var icons = {
|
|
3360
|
+
connected: {
|
|
3361
|
+
path: {
|
|
3362
|
+
"16": "/icons/icon-green-16.png",
|
|
3363
|
+
"32": "/icons/icon-green-32.png",
|
|
3364
|
+
"48": "/icons/icon-green-48.png",
|
|
3365
|
+
"128": "/icons/icon-green-128.png"
|
|
3366
|
+
},
|
|
3367
|
+
title: "Connected - Click to disconnect",
|
|
3368
|
+
badgeText: "",
|
|
3369
|
+
badgeColor: [
|
|
3370
|
+
64,
|
|
3371
|
+
64,
|
|
3372
|
+
64,
|
|
3373
|
+
255
|
|
3374
|
+
]
|
|
3375
|
+
},
|
|
3376
|
+
connecting: {
|
|
3377
|
+
path: {
|
|
3378
|
+
"16": "/icons/icon-gray-16.png",
|
|
3379
|
+
"32": "/icons/icon-gray-32.png",
|
|
3380
|
+
"48": "/icons/icon-gray-48.png",
|
|
3381
|
+
"128": "/icons/icon-gray-128.png"
|
|
3382
|
+
},
|
|
3383
|
+
title: "Waiting for MCP WS server...",
|
|
3384
|
+
badgeText: "...",
|
|
3385
|
+
badgeColor: [
|
|
3386
|
+
64,
|
|
3387
|
+
64,
|
|
3388
|
+
64,
|
|
3389
|
+
255
|
|
3390
|
+
]
|
|
3391
|
+
},
|
|
3392
|
+
idle: {
|
|
3393
|
+
path: {
|
|
3394
|
+
"16": "/icons/icon-black-16.png",
|
|
3395
|
+
"32": "/icons/icon-black-32.png",
|
|
3396
|
+
"48": "/icons/icon-black-48.png",
|
|
3397
|
+
"128": "/icons/icon-black-128.png"
|
|
3398
|
+
},
|
|
3399
|
+
title: "Click to attach debugger",
|
|
3400
|
+
badgeText: "",
|
|
3401
|
+
badgeColor: [
|
|
3402
|
+
64,
|
|
3403
|
+
64,
|
|
3404
|
+
64,
|
|
3405
|
+
255
|
|
3406
|
+
]
|
|
3407
|
+
},
|
|
3408
|
+
restricted: {
|
|
3409
|
+
path: {
|
|
3410
|
+
"16": "/icons/icon-gray-16.png",
|
|
3411
|
+
"32": "/icons/icon-gray-32.png",
|
|
3412
|
+
"48": "/icons/icon-gray-48.png",
|
|
3413
|
+
"128": "/icons/icon-gray-128.png"
|
|
3414
|
+
},
|
|
3415
|
+
title: "Cannot attach to this page",
|
|
3416
|
+
badgeText: "",
|
|
3417
|
+
badgeColor: [
|
|
3418
|
+
64,
|
|
3419
|
+
64,
|
|
3420
|
+
64,
|
|
3421
|
+
255
|
|
3422
|
+
]
|
|
3423
|
+
},
|
|
3424
|
+
extensionReplaced: {
|
|
3425
|
+
path: {
|
|
3426
|
+
"16": "/icons/icon-gray-16.png",
|
|
3427
|
+
"32": "/icons/icon-gray-32.png",
|
|
3428
|
+
"48": "/icons/icon-gray-48.png",
|
|
3429
|
+
"128": "/icons/icon-gray-128.png"
|
|
3430
|
+
},
|
|
3431
|
+
title: "Another Tabwright extension connected - Click to retry",
|
|
3432
|
+
badgeText: "!",
|
|
3433
|
+
badgeColor: [
|
|
3434
|
+
220,
|
|
3435
|
+
38,
|
|
3436
|
+
38,
|
|
3437
|
+
255
|
|
3438
|
+
]
|
|
3439
|
+
},
|
|
3440
|
+
relayWarning: {
|
|
3441
|
+
path: {
|
|
3442
|
+
"16": "/icons/icon-gray-16.png",
|
|
3443
|
+
"32": "/icons/icon-gray-32.png",
|
|
3444
|
+
"48": "/icons/icon-gray-48.png",
|
|
3445
|
+
"128": "/icons/icon-gray-128.png"
|
|
3446
|
+
},
|
|
3447
|
+
title: relayIssueText({ issue: "unavailable" }),
|
|
3448
|
+
badgeText: "!",
|
|
3449
|
+
badgeColor: [
|
|
3450
|
+
245,
|
|
3451
|
+
158,
|
|
3452
|
+
11,
|
|
3453
|
+
255
|
|
3454
|
+
]
|
|
3455
|
+
},
|
|
3456
|
+
relayReviewDegraded: {
|
|
3457
|
+
path: {
|
|
3458
|
+
"16": "/icons/icon-green-16.png",
|
|
3459
|
+
"32": "/icons/icon-green-32.png",
|
|
3460
|
+
"48": "/icons/icon-green-48.png",
|
|
3461
|
+
"128": "/icons/icon-green-128.png"
|
|
3462
|
+
},
|
|
3463
|
+
title: relayReviewIssueText({ issue: "unavailable" }),
|
|
3464
|
+
badgeText: "!",
|
|
3465
|
+
badgeColor: [
|
|
3466
|
+
245,
|
|
3467
|
+
158,
|
|
3468
|
+
11,
|
|
3469
|
+
255
|
|
3470
|
+
]
|
|
3471
|
+
},
|
|
3472
|
+
tabError: {
|
|
3473
|
+
path: {
|
|
3474
|
+
"16": "/icons/icon-gray-16.png",
|
|
3475
|
+
"32": "/icons/icon-gray-32.png",
|
|
3476
|
+
"48": "/icons/icon-gray-48.png",
|
|
3477
|
+
"128": "/icons/icon-gray-128.png"
|
|
3478
|
+
},
|
|
3479
|
+
title: "Error",
|
|
3480
|
+
badgeText: "!",
|
|
3481
|
+
badgeColor: [
|
|
3482
|
+
220,
|
|
3483
|
+
38,
|
|
3484
|
+
38,
|
|
3485
|
+
255
|
|
3486
|
+
]
|
|
3487
|
+
}
|
|
3488
|
+
};
|
|
3489
|
+
async function updateIcons() {
|
|
3490
|
+
const { connectionState, relayReviewState, tabs, errorText } = store.getState();
|
|
3491
|
+
const connectedCount = Array.from(tabs.values()).filter((t) => t.state === "connected").length;
|
|
3492
|
+
const allTabs = await chrome.tabs.query({});
|
|
3493
|
+
const tabUrlMap = new Map(allTabs.map((tab) => [tab.id, tab.url]));
|
|
3494
|
+
const allTabIds = [void 0, ...allTabs.map((tab) => tab.id).filter((id) => id !== void 0)];
|
|
3495
|
+
for (const tabId of allTabIds) {
|
|
3496
|
+
const tabInfo = tabId !== void 0 ? tabs.get(tabId) : void 0;
|
|
3497
|
+
const tabUrl = tabId !== void 0 ? tabUrlMap.get(tabId) : void 0;
|
|
3498
|
+
const iconConfig = (() => {
|
|
3499
|
+
if (connectionState === "extension-replaced") return icons.extensionReplaced;
|
|
3500
|
+
if (connectionState === "relay-warning") return icons.relayWarning;
|
|
3501
|
+
if (connectionState === "connected" && relayReviewState.status === "degraded") return icons.relayReviewDegraded;
|
|
3502
|
+
if (tabId !== void 0 && isRestrictedUrl(tabUrl)) return icons.restricted;
|
|
3503
|
+
if (tabInfo?.state === "error") return icons.tabError;
|
|
3504
|
+
if (tabInfo?.state === "connecting") return icons.connecting;
|
|
3505
|
+
if (tabInfo?.state === "connected") return icons.connected;
|
|
3506
|
+
return icons.idle;
|
|
3507
|
+
})();
|
|
3508
|
+
const title = (() => {
|
|
3509
|
+
if (connectionState === "extension-replaced" && errorText) return errorText;
|
|
3510
|
+
if (connectionState === "relay-warning" && errorText) return errorText;
|
|
3511
|
+
if (connectionState === "connected" && relayReviewState.status === "degraded") return relayReviewState.errorText;
|
|
3512
|
+
if (tabInfo?.errorText) return tabInfo.errorText;
|
|
3513
|
+
return iconConfig.title;
|
|
3514
|
+
})();
|
|
3515
|
+
const badgeText = (() => {
|
|
3516
|
+
if (iconConfig === icons.connected || iconConfig === icons.idle || iconConfig === icons.restricted) return connectedCount > 0 ? String(connectedCount) : "";
|
|
3517
|
+
return iconConfig.badgeText;
|
|
3518
|
+
})();
|
|
3519
|
+
chrome.action.setIcon({
|
|
3520
|
+
tabId,
|
|
3521
|
+
path: iconConfig.path
|
|
3522
|
+
});
|
|
3523
|
+
chrome.action.setTitle({
|
|
3524
|
+
tabId,
|
|
3525
|
+
title
|
|
3526
|
+
});
|
|
3527
|
+
if (iconConfig.badgeColor) chrome.action.setBadgeBackgroundColor({
|
|
3528
|
+
tabId,
|
|
3529
|
+
color: iconConfig.badgeColor
|
|
3530
|
+
});
|
|
3531
|
+
chrome.action.setBadgeText({
|
|
3532
|
+
tabId,
|
|
3533
|
+
text: badgeText
|
|
3534
|
+
});
|
|
3535
|
+
}
|
|
3536
|
+
}
|
|
3537
|
+
async function onTabRemoved(tabId) {
|
|
3538
|
+
popupSourceTabMap.delete(tabId);
|
|
3539
|
+
const { tabs } = store.getState();
|
|
3540
|
+
if (!tabs.has(tabId)) return;
|
|
3541
|
+
logger.debug(`Connected tab ${tabId} was closed, disconnecting`);
|
|
3542
|
+
await disconnectTab(tabId);
|
|
3543
|
+
}
|
|
3544
|
+
async function onTabActivated(activeInfo) {
|
|
3545
|
+
store.setState({
|
|
3546
|
+
currentTabId: activeInfo.tabId,
|
|
3547
|
+
preferredWindowId: activeInfo.windowId
|
|
3548
|
+
});
|
|
3549
|
+
}
|
|
3550
|
+
async function onActionClicked(tab) {
|
|
3551
|
+
if (!tab.id) {
|
|
3552
|
+
logger.debug("No tab ID available");
|
|
3553
|
+
return;
|
|
3554
|
+
}
|
|
3555
|
+
if (tab.windowId !== void 0) store.setState({
|
|
3556
|
+
currentTabId: tab.id,
|
|
3557
|
+
preferredWindowId: tab.windowId
|
|
3558
|
+
});
|
|
3559
|
+
if (isRestrictedUrl(tab.url)) {
|
|
3560
|
+
logger.debug("Cannot attach to restricted URL:", tab.url);
|
|
3561
|
+
return;
|
|
3562
|
+
}
|
|
3563
|
+
const { tabs, connectionState } = store.getState();
|
|
3564
|
+
const tabInfo = tabs.get(tab.id);
|
|
3565
|
+
if (connectionState === "extension-replaced") {
|
|
3566
|
+
logger.debug("Clearing extension-replaced state, attempting to reconnect");
|
|
3567
|
+
store.setState({
|
|
3568
|
+
connectionState: "idle",
|
|
3569
|
+
errorText: void 0
|
|
3570
|
+
});
|
|
3571
|
+
await connectTab(tab.id);
|
|
3572
|
+
return;
|
|
3573
|
+
}
|
|
3574
|
+
if (connectionState === "relay-warning") {
|
|
3575
|
+
logger.debug("Clearing relay-warning state, attempting to reconnect");
|
|
3576
|
+
store.setState({
|
|
3577
|
+
connectionState: "idle",
|
|
3578
|
+
errorText: void 0
|
|
3579
|
+
});
|
|
3580
|
+
await connectTab(tab.id);
|
|
3581
|
+
return;
|
|
3582
|
+
}
|
|
3583
|
+
if (tabInfo?.state === "error") {
|
|
3584
|
+
logger.debug("Tab has error - disconnecting to clear state");
|
|
3585
|
+
await disconnectTab(tab.id);
|
|
3586
|
+
return;
|
|
3587
|
+
}
|
|
3588
|
+
if (tabInfo?.state === "connecting") {
|
|
3589
|
+
logger.debug("Tab is already connecting, ignoring click");
|
|
3590
|
+
return;
|
|
3591
|
+
}
|
|
3592
|
+
if (tabInfo?.state === "connected") await disconnectTab(tab.id);
|
|
3593
|
+
else await connectTab(tab.id);
|
|
3594
|
+
}
|
|
3595
|
+
resetDebugger();
|
|
3596
|
+
connectionManager.maintainLoop();
|
|
3597
|
+
chrome.contextMenus.remove("playwriter-pin-element").catch(() => {}).finally(() => {
|
|
3598
|
+
chrome.contextMenus?.create({
|
|
3599
|
+
id: "playwriter-pin-element",
|
|
3600
|
+
title: "Copy Tabwright Element Reference",
|
|
3601
|
+
contexts: ["all"],
|
|
3602
|
+
visible: false
|
|
3603
|
+
});
|
|
3604
|
+
});
|
|
3605
|
+
chrome.contextMenus.remove("playwriter-copy-react-source").catch(() => {}).finally(() => {
|
|
3606
|
+
chrome.contextMenus?.create({
|
|
3607
|
+
id: "playwriter-copy-react-source",
|
|
3608
|
+
title: "Copy React Component Source Path",
|
|
3609
|
+
contexts: ["all"],
|
|
3610
|
+
visible: false
|
|
3611
|
+
});
|
|
3612
|
+
});
|
|
3613
|
+
function updateContextMenuVisibility() {
|
|
3614
|
+
const { currentTabId, tabs } = store.getState();
|
|
3615
|
+
const isConnected = currentTabId !== void 0 && tabs.get(currentTabId)?.state === "connected";
|
|
3616
|
+
chrome.contextMenus?.update("playwriter-pin-element", { visible: isConnected });
|
|
3617
|
+
chrome.contextMenus?.update("playwriter-copy-react-source", { visible: isConnected });
|
|
3618
|
+
}
|
|
3619
|
+
function buildPinnedElementInspectionCode(options) {
|
|
3620
|
+
return `inspectPinnedElement(${JSON.stringify(options.url).replace(/'/g, "\\u0027")},"globalThis.${options.pinName}")`;
|
|
3621
|
+
}
|
|
3622
|
+
chrome.runtime.onInstalled.addListener((details) => {});
|
|
3623
|
+
function serializeTabs(tabs) {
|
|
3624
|
+
return JSON.stringify(Array.from(tabs.entries()));
|
|
3625
|
+
}
|
|
3626
|
+
store.subscribe((state, prevState) => {
|
|
3627
|
+
logger.log(state);
|
|
3628
|
+
updateIcons();
|
|
3629
|
+
updateContextMenuVisibility();
|
|
3630
|
+
if (serializeTabs(state.tabs) !== serializeTabs(prevState.tabs)) tabGroupQueue = tabGroupQueue.then(syncTabGroup).catch((e) => {
|
|
3631
|
+
logger.debug("syncTabGroup error:", e);
|
|
3632
|
+
});
|
|
3633
|
+
});
|
|
3634
|
+
logger.debug(`Using relay host: ${RELAY_HOST}, port: ${RELAY_PORT}`);
|
|
3635
|
+
var lastMemoryUsage = 0;
|
|
3636
|
+
var lastMemoryCheck = Date.now();
|
|
3637
|
+
var MEMORY_WARNING_THRESHOLD = 50 * 1024 * 1024;
|
|
3638
|
+
var MEMORY_CRITICAL_THRESHOLD = 100 * 1024 * 1024;
|
|
3639
|
+
var MEMORY_GROWTH_THRESHOLD = 10 * 1024 * 1024;
|
|
3640
|
+
function checkMemory() {
|
|
3641
|
+
try {
|
|
3642
|
+
const memory = performance.memory;
|
|
3643
|
+
if (!memory) return;
|
|
3644
|
+
const used = memory.usedJSHeapSize;
|
|
3645
|
+
const total = memory.totalJSHeapSize;
|
|
3646
|
+
const limit = memory.jsHeapSizeLimit;
|
|
3647
|
+
const now = Date.now();
|
|
3648
|
+
const timeDelta = now - lastMemoryCheck;
|
|
3649
|
+
const memoryDelta = used - lastMemoryUsage;
|
|
3650
|
+
const formatMB = (bytes) => (bytes / 1024 / 1024).toFixed(2) + "MB";
|
|
3651
|
+
const growthRate = timeDelta > 0 ? memoryDelta / timeDelta * 1e3 : 0;
|
|
3652
|
+
if (used > MEMORY_CRITICAL_THRESHOLD) logger.error(`MEMORY CRITICAL: used=${formatMB(used)} total=${formatMB(total)} limit=${formatMB(limit)} growth=${formatMB(memoryDelta)} rate=${formatMB(growthRate)}/s`);
|
|
3653
|
+
else if (used > MEMORY_WARNING_THRESHOLD) logger.warn(`MEMORY WARNING: used=${formatMB(used)} total=${formatMB(total)} limit=${formatMB(limit)} growth=${formatMB(memoryDelta)} rate=${formatMB(growthRate)}/s`);
|
|
3654
|
+
else if (memoryDelta > MEMORY_GROWTH_THRESHOLD && timeDelta < 6e4) logger.warn(`MEMORY SPIKE: grew ${formatMB(memoryDelta)} in ${(timeDelta / 1e3).toFixed(1)}s (used=${formatMB(used)})`);
|
|
3655
|
+
lastMemoryUsage = used;
|
|
3656
|
+
lastMemoryCheck = now;
|
|
3657
|
+
} catch (e) {}
|
|
3658
|
+
}
|
|
3659
|
+
setInterval(checkMemory, 5e3);
|
|
3660
|
+
checkMemory();
|
|
3661
|
+
chrome.tabs.onRemoved.addListener(onTabRemoved);
|
|
3662
|
+
chrome.tabs.onActivated.addListener(onTabActivated);
|
|
3663
|
+
chrome.action.onClicked.addListener(onActionClicked);
|
|
3664
|
+
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
|
|
3665
|
+
updateIcons();
|
|
3666
|
+
if (changeInfo.groupId !== void 0) tabGroupQueue = tabGroupQueue.then(async () => {
|
|
3667
|
+
const groupId = (await chrome.tabGroups.query({ title: TAB_GROUP_TITLE }))[0]?.id;
|
|
3668
|
+
if (groupId === void 0) return;
|
|
3669
|
+
const { tabs } = store.getState();
|
|
3670
|
+
if (changeInfo.groupId === groupId) {
|
|
3671
|
+
if (!tabs.has(tabId) && !isRestrictedUrl(tab.url)) {
|
|
3672
|
+
logger.debug("Tab manually added to Tabwright group:", tabId);
|
|
3673
|
+
await connectTab(tabId);
|
|
3674
|
+
}
|
|
3675
|
+
} else if (tabs.has(tabId)) {
|
|
3676
|
+
if (tabs.get(tabId)?.state === "connecting") {
|
|
3677
|
+
logger.debug("Tab removed from group while connecting, ignoring:", tabId);
|
|
3678
|
+
return;
|
|
3679
|
+
}
|
|
3680
|
+
logger.debug("Tab manually removed from Tabwright group:", tabId);
|
|
3681
|
+
await disconnectTab(tabId);
|
|
3682
|
+
}
|
|
3683
|
+
}).catch((e) => {
|
|
3684
|
+
logger.debug("onTabUpdated handler error:", e);
|
|
3685
|
+
});
|
|
3686
|
+
});
|
|
3687
|
+
var popupSourceTabMap = /* @__PURE__ */ new Map();
|
|
3688
|
+
chrome.webNavigation.onCreatedNavigationTarget.addListener((details) => {
|
|
3689
|
+
popupSourceTabMap.set(details.tabId, details.sourceTabId);
|
|
3690
|
+
setTimeout(() => {
|
|
3691
|
+
popupSourceTabMap.delete(details.tabId);
|
|
3692
|
+
}, 1e4);
|
|
3693
|
+
});
|
|
3694
|
+
chrome.windows.onCreated.addListener(async (popupWindow) => {
|
|
3695
|
+
if (popupWindow.type !== "popup" || popupWindow.id === void 0) return;
|
|
3696
|
+
try {
|
|
3697
|
+
let popupTabs = [];
|
|
3698
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
3699
|
+
popupTabs = await chrome.tabs.query({ windowId: popupWindow.id });
|
|
3700
|
+
if (popupTabs.length > 0) break;
|
|
3701
|
+
await sleep(20);
|
|
3702
|
+
}
|
|
3703
|
+
const tabIds = popupTabs.map((t) => t.id).filter((id) => {
|
|
3704
|
+
return id !== void 0;
|
|
3705
|
+
});
|
|
3706
|
+
if (tabIds.length === 0) {
|
|
3707
|
+
logger.debug(`Popup window ${popupWindow.id} has no tabs after retry, skipping`);
|
|
3708
|
+
return;
|
|
3709
|
+
}
|
|
3710
|
+
const { tabs: connectedTabs } = store.getState();
|
|
3711
|
+
let sourceTabId;
|
|
3712
|
+
for (const tabId of tabIds) {
|
|
3713
|
+
const candidate = popupSourceTabMap.get(tabId);
|
|
3714
|
+
if (candidate !== void 0 && connectedTabs.has(candidate)) {
|
|
3715
|
+
sourceTabId = candidate;
|
|
3716
|
+
break;
|
|
3717
|
+
}
|
|
3718
|
+
}
|
|
3719
|
+
for (const tabId of tabIds) popupSourceTabMap.delete(tabId);
|
|
3720
|
+
if (sourceTabId === void 0) {
|
|
3721
|
+
logger.debug(`Popup window ${popupWindow.id} not opened by a Tabwright-connected tab, leaving alone (tabs=${JSON.stringify(tabIds)})`);
|
|
3722
|
+
return;
|
|
3723
|
+
}
|
|
3724
|
+
let destinationWindowId;
|
|
3725
|
+
try {
|
|
3726
|
+
const sourceTab = await chrome.tabs.get(sourceTabId);
|
|
3727
|
+
if (sourceTab.windowId === void 0) {
|
|
3728
|
+
const focused = await chrome.windows.getLastFocused({ populate: false });
|
|
3729
|
+
if (focused.id === void 0 || focused.id === popupWindow.id) return;
|
|
3730
|
+
destinationWindowId = focused.id;
|
|
3731
|
+
} else destinationWindowId = sourceTab.windowId;
|
|
3732
|
+
} catch (e) {
|
|
3733
|
+
logger.debug(`Source tab ${sourceTabId} no longer exists, skipping relocation:`, e);
|
|
3734
|
+
return;
|
|
3735
|
+
}
|
|
3736
|
+
logger.debug(`Relocating ${tabIds.length} popup tab(s) from window ${popupWindow.id} into source window ${destinationWindowId} (sourceTabId=${sourceTabId})`);
|
|
3737
|
+
await chrome.tabs.move(tabIds, {
|
|
3738
|
+
windowId: destinationWindowId,
|
|
3739
|
+
index: -1
|
|
3740
|
+
});
|
|
3741
|
+
try {
|
|
3742
|
+
await chrome.windows.remove(popupWindow.id);
|
|
3743
|
+
} catch {}
|
|
3744
|
+
for (const tabId of tabIds) {
|
|
3745
|
+
if (connectedTabs.has(tabId)) continue;
|
|
3746
|
+
try {
|
|
3747
|
+
await connectTab(tabId);
|
|
3748
|
+
} catch (e) {
|
|
3749
|
+
logger.warn(`Failed to auto-connect relocated popup tab ${tabId}:`, e);
|
|
3750
|
+
}
|
|
3751
|
+
}
|
|
3752
|
+
} catch (e) {
|
|
3753
|
+
logger.warn("Failed to relocate popup window:", e);
|
|
3754
|
+
}
|
|
3755
|
+
});
|
|
3756
|
+
chrome.contextMenus?.onClicked.addListener(async (info, tab) => {
|
|
3757
|
+
if (!tab?.id) return;
|
|
3758
|
+
const tabInfo = store.getState().tabs.get(tab.id);
|
|
3759
|
+
if (!tabInfo || tabInfo.state !== "connected") {
|
|
3760
|
+
logger.debug("Tab not connected, ignoring");
|
|
3761
|
+
return;
|
|
3762
|
+
}
|
|
3763
|
+
const debuggee = { tabId: tab.id };
|
|
3764
|
+
if (info.menuItemId === "playwriter-pin-element") try {
|
|
3765
|
+
const jsAllocatePin = js`
|
|
3766
|
+
(function() {
|
|
3767
|
+
window.__playwriterPinCount = (window.__playwriterPinCount || 0) + 1;
|
|
3768
|
+
return window.__playwriterPinCount;
|
|
3769
|
+
})()
|
|
3770
|
+
`;
|
|
3771
|
+
const name = `playwriterPinnedElem${(await chrome.debugger.sendCommand(debuggee, "Runtime.evaluate", {
|
|
3772
|
+
expression: jsAllocatePin,
|
|
3773
|
+
returnByValue: true
|
|
3774
|
+
})).result?.value ?? 1}`;
|
|
3775
|
+
const jsAssignPin = js`
|
|
3776
|
+
if (window.__playwriter_lastRightClicked) {
|
|
3777
|
+
window.${name} = window.__playwriter_lastRightClicked;
|
|
3778
|
+
'${name}';
|
|
3779
|
+
} else {
|
|
3780
|
+
throw new Error('No element was right-clicked');
|
|
3781
|
+
}
|
|
3782
|
+
`;
|
|
3783
|
+
const result = await chrome.debugger.sendCommand(debuggee, "Runtime.evaluate", {
|
|
3784
|
+
expression: jsAssignPin,
|
|
3785
|
+
returnByValue: true
|
|
3786
|
+
});
|
|
3787
|
+
if (result.exceptionDetails) {
|
|
3788
|
+
logger.error("Failed to pin element:", result.exceptionDetails.text);
|
|
3789
|
+
return;
|
|
3790
|
+
}
|
|
3791
|
+
const clipboardText = "tabwright -e '" + buildPinnedElementInspectionCode({
|
|
3792
|
+
pinName: name,
|
|
3793
|
+
url: tab.url || ""
|
|
3794
|
+
}) + "'";
|
|
3795
|
+
const jsPinFlashAndCopy = js`
|
|
3796
|
+
(() => {
|
|
3797
|
+
const el = window.${name};
|
|
3798
|
+
if (!el) return;
|
|
3799
|
+
const orig = el.getAttribute('style') || '';
|
|
3800
|
+
el.setAttribute('style', orig + '; outline: 3px solid #22c55e !important; outline-offset: 2px !important; box-shadow: 0 0 0 3px #22c55e !important;');
|
|
3801
|
+
setTimeout(() => el.setAttribute('style', orig), 300);
|
|
3802
|
+
return navigator.clipboard.writeText(${JSON.stringify(clipboardText)});
|
|
3803
|
+
})()
|
|
3804
|
+
`;
|
|
3805
|
+
await chrome.debugger.sendCommand(debuggee, "Runtime.evaluate", {
|
|
3806
|
+
expression: jsPinFlashAndCopy,
|
|
3807
|
+
awaitPromise: true,
|
|
3808
|
+
userGesture: true
|
|
3809
|
+
});
|
|
3810
|
+
logger.debug("Pinned element as:", name);
|
|
3811
|
+
} catch (error) {
|
|
3812
|
+
logger.error("Failed to pin element:", error.message);
|
|
3813
|
+
}
|
|
3814
|
+
if (info.menuItemId === "playwriter-copy-react-source") try {
|
|
3815
|
+
if (!(await chrome.debugger.sendCommand(debuggee, "Runtime.evaluate", {
|
|
3816
|
+
expression: "!!globalThis.__bippy",
|
|
3817
|
+
returnByValue: true
|
|
3818
|
+
})).result?.value) await chrome.debugger.sendCommand(debuggee, "Runtime.evaluate", { expression: bippy_default });
|
|
3819
|
+
const jsResolveSource = js`
|
|
3820
|
+
(async () => {
|
|
3821
|
+
const el = window.__playwriter_lastRightClicked;
|
|
3822
|
+
if (!el) return JSON.stringify({ error: 'No element was right-clicked' });
|
|
3823
|
+
|
|
3824
|
+
const bippy = globalThis.__bippy;
|
|
3825
|
+
if (!bippy) return JSON.stringify({ error: 'bippy not loaded' });
|
|
3826
|
+
|
|
3827
|
+
// bippy.normalizeFileName strips "/app-pages-browser/" but not the parenthesized
|
|
3828
|
+
// form "/(app-pages-browser)/" that Next.js webpack actually uses. This regex
|
|
3829
|
+
// strips all Next.js webpack layer prefixes: (app-pages-browser), (ssr), (rsc),
|
|
3830
|
+
// (action-browser), (pages-dir-browser), (pages-dir-edge), (pages-dir-node).
|
|
3831
|
+
// Also strips leading "./" that often follows the layer prefix.
|
|
3832
|
+
const cleanFileName = (name) => {
|
|
3833
|
+
let f = bippy.normalizeFileName(name);
|
|
3834
|
+
f = f.replace(/^\/?\\([-\\w]+\\)\\//, '');
|
|
3835
|
+
f = f.replace(/^\\.[\\/]/, '');
|
|
3836
|
+
return f;
|
|
3837
|
+
};
|
|
3838
|
+
|
|
3839
|
+
let fiber;
|
|
3840
|
+
try { fiber = bippy.getFiberFromHostInstance(el); } catch {}
|
|
3841
|
+
if (!fiber) return JSON.stringify({ error: 'No React fiber found. Is this a React app?' });
|
|
3842
|
+
|
|
3843
|
+
// Walk up to find nearest composite fiber with source info
|
|
3844
|
+
let current = fiber;
|
|
3845
|
+
for (let i = 0; i < 50 && current; i++) {
|
|
3846
|
+
try {
|
|
3847
|
+
if (bippy.isCompositeFiber(current)) {
|
|
3848
|
+
const source = await bippy.getSource(current);
|
|
3849
|
+
if (source && source.fileName && bippy.isSourceFile(source.fileName)) {
|
|
3850
|
+
return JSON.stringify({
|
|
3851
|
+
fileName: cleanFileName(source.fileName),
|
|
3852
|
+
lineNumber: source.lineNumber || null,
|
|
3853
|
+
columnNumber: source.columnNumber || null,
|
|
3854
|
+
componentName: source.functionName || bippy.getDisplayName(current.type) || null,
|
|
3855
|
+
});
|
|
3856
|
+
}
|
|
3857
|
+
// Try owner stack as fallback for this fiber
|
|
3858
|
+
const ownerStack = await bippy.getOwnerStack(current);
|
|
3859
|
+
for (const frame of ownerStack) {
|
|
3860
|
+
if (frame.fileName && bippy.isSourceFile(frame.fileName)) {
|
|
3861
|
+
return JSON.stringify({
|
|
3862
|
+
fileName: cleanFileName(frame.fileName),
|
|
3863
|
+
lineNumber: frame.lineNumber || null,
|
|
3864
|
+
columnNumber: frame.columnNumber || null,
|
|
3865
|
+
componentName: frame.functionName || bippy.getDisplayName(current.type) || null,
|
|
3866
|
+
});
|
|
3867
|
+
}
|
|
3868
|
+
}
|
|
3869
|
+
}
|
|
3870
|
+
} catch {}
|
|
3871
|
+
current = current.return;
|
|
3872
|
+
}
|
|
3873
|
+
return JSON.stringify({ error: 'No React source location found. Is this a dev build with source maps?' });
|
|
3874
|
+
})()
|
|
3875
|
+
`;
|
|
3876
|
+
const sourceResult = await chrome.debugger.sendCommand(debuggee, "Runtime.evaluate", {
|
|
3877
|
+
expression: jsResolveSource,
|
|
3878
|
+
returnByValue: true,
|
|
3879
|
+
awaitPromise: true
|
|
3880
|
+
});
|
|
3881
|
+
if (sourceResult.exceptionDetails) {
|
|
3882
|
+
logger.error("Failed to get React source:", sourceResult.exceptionDetails.text);
|
|
3883
|
+
return;
|
|
3884
|
+
}
|
|
3885
|
+
const parsed = JSON.parse(sourceResult.result?.value || "{}");
|
|
3886
|
+
if (!parsed.fileName && !parsed.error) parsed.error = "React source result missing fileName";
|
|
3887
|
+
if (parsed.error) {
|
|
3888
|
+
const jsFlashRed = js`
|
|
3889
|
+
(() => {
|
|
3890
|
+
const el = window.__playwriter_lastRightClicked;
|
|
3891
|
+
if (!el) return;
|
|
3892
|
+
const orig = el.getAttribute('style') || '';
|
|
3893
|
+
el.setAttribute('style', orig + '; outline: 3px solid #ef4444 !important; outline-offset: 2px !important;');
|
|
3894
|
+
setTimeout(() => el.setAttribute('style', orig), 600);
|
|
3895
|
+
})()
|
|
3896
|
+
`;
|
|
3897
|
+
await chrome.debugger.sendCommand(debuggee, "Runtime.evaluate", { expression: jsFlashRed });
|
|
3898
|
+
logger.debug("React source not found:", parsed.error);
|
|
3899
|
+
return;
|
|
3900
|
+
}
|
|
3901
|
+
const clipboardText = (() => {
|
|
3902
|
+
if (parsed.lineNumber) return `${parsed.fileName}:${parsed.lineNumber}`;
|
|
3903
|
+
return parsed.fileName;
|
|
3904
|
+
})();
|
|
3905
|
+
const jsFlashGreenAndCopy = js`
|
|
3906
|
+
(() => {
|
|
3907
|
+
const el = window.__playwriter_lastRightClicked;
|
|
3908
|
+
if (!el) return;
|
|
3909
|
+
const orig = el.getAttribute('style') || '';
|
|
3910
|
+
el.setAttribute('style', orig + '; outline: 3px solid #22c55e !important; outline-offset: 2px !important; box-shadow: 0 0 0 3px #22c55e !important;');
|
|
3911
|
+
setTimeout(() => el.setAttribute('style', orig), 300);
|
|
3912
|
+
return navigator.clipboard.writeText(${JSON.stringify(clipboardText)});
|
|
3913
|
+
})()
|
|
3914
|
+
`;
|
|
3915
|
+
await chrome.debugger.sendCommand(debuggee, "Runtime.evaluate", {
|
|
3916
|
+
expression: jsFlashGreenAndCopy,
|
|
3917
|
+
awaitPromise: true,
|
|
3918
|
+
userGesture: true
|
|
3919
|
+
});
|
|
3920
|
+
logger.debug("Copied React source path:", clipboardText, "component:", parsed.componentName);
|
|
3921
|
+
} catch (error) {
|
|
3922
|
+
logger.error("Failed to copy React source:", error.message);
|
|
3923
|
+
}
|
|
3924
|
+
});
|
|
3925
|
+
updateIcons();
|
|
3926
|
+
chrome.runtime.onConnect.addListener((port) => {
|
|
3927
|
+
if (port.name !== "playwriter-toolbar-recording") return;
|
|
3928
|
+
port.onMessage.addListener((message) => {
|
|
3929
|
+
if (!isToolbarRecordingPortMessage(message)) {
|
|
3930
|
+
logger.debug("Ignoring invalid toolbar recording port message");
|
|
3931
|
+
return;
|
|
3932
|
+
}
|
|
3933
|
+
handleToolbarRecordingPortMessage({
|
|
3934
|
+
port,
|
|
3935
|
+
message
|
|
3936
|
+
});
|
|
3937
|
+
});
|
|
3938
|
+
});
|
|
3939
|
+
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|
3940
|
+
if (isToolbarRecordingMessage(message)) {
|
|
3941
|
+
logger.debug("Toolbar runtime message received:", message.action, "tabId:", sender.tab?.id);
|
|
3942
|
+
(message.action === "playwriterToolbarToggleRecording" ? toggleToolbarRecording(sender) : getToolbarRecordingStatus(sender)).then((result) => {
|
|
3943
|
+
logger.debug("Toolbar runtime message response:", message.action, result);
|
|
3944
|
+
sendResponse(result);
|
|
3945
|
+
}).catch((error) => {
|
|
3946
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
3947
|
+
logger.debug("Toolbar runtime message failed:", message.action, errorMessage);
|
|
3948
|
+
sendResponse({
|
|
3949
|
+
success: false,
|
|
3950
|
+
error: errorMessage,
|
|
3951
|
+
isRecording: false
|
|
3952
|
+
});
|
|
3953
|
+
});
|
|
3954
|
+
return true;
|
|
3955
|
+
}
|
|
3956
|
+
if (isRrwebEventBatchMessage(message)) {
|
|
3957
|
+
const tabId = sender.tab?.id;
|
|
3958
|
+
if (!tabId) {
|
|
3959
|
+
logger.debug("Ignoring rrweb event batch without sender tab");
|
|
3960
|
+
return false;
|
|
3961
|
+
}
|
|
3962
|
+
if (connectionManager.ws?.readyState === WebSocket.OPEN) sendMessage({
|
|
3963
|
+
method: "rrwebRecordingData",
|
|
3964
|
+
params: {
|
|
3965
|
+
tabId,
|
|
3966
|
+
events: message.events,
|
|
3967
|
+
final: message.final
|
|
3968
|
+
}
|
|
3969
|
+
});
|
|
3970
|
+
else logger.debug(`Dropping rrweb event batch for tab ${tabId} because WebSocket is not ready`);
|
|
3971
|
+
return false;
|
|
3972
|
+
}
|
|
3973
|
+
if (isRrwebCancelledMessage(message)) {
|
|
3974
|
+
const tabId = sender.tab?.id;
|
|
3975
|
+
if (!tabId) {
|
|
3976
|
+
logger.debug("Ignoring rrweb cancellation without sender tab");
|
|
3977
|
+
return false;
|
|
3978
|
+
}
|
|
3979
|
+
if (connectionManager.ws?.readyState === WebSocket.OPEN) sendMessage({
|
|
3980
|
+
method: "rrwebRecordingCancelled",
|
|
3981
|
+
params: { tabId }
|
|
3982
|
+
});
|
|
3983
|
+
return false;
|
|
3984
|
+
}
|
|
3985
|
+
return false;
|
|
3986
|
+
});
|
|
3987
|
+
chrome.webNavigation.onDOMContentLoaded.addListener((details) => {
|
|
3988
|
+
const { tabs } = store.getState();
|
|
3989
|
+
const tabInfo = tabs.get(details.tabId);
|
|
3990
|
+
if (!tabInfo || tabInfo.state !== "connected") return;
|
|
3991
|
+
if (details.frameId === 0) injectToolbar(details.tabId);
|
|
3992
|
+
resumeRrwebRecordingForNavigation(details.tabId);
|
|
3993
|
+
});
|
|
3994
|
+
//#endregion
|