From 595af2d55fe1e92ccdf147f1e747414e400323b6 Mon Sep 17 00:00:00 2001 From: dvcolomban Date: Tue, 8 Sep 2026 14:58:50 +0200 Subject: [PATCH 1/6] feat: extract broker orchestration and Devframe integration --- ARCHITECTURE.md | 23 +- README.md | 2 + docs/benchmarks/2026-09-08-latency.csv | 1201 +++++++++++++++++ docs/benchmarks/2026-09-08-latency.md | 57 + docs/native-mvp.md | 12 +- docs/releasing.md | 2 +- docs/research/devframe-toast-removal.md | 40 + eslint.config.ts | 1 + examples/devframe/README.md | 21 +- examples/devframe/devframe.ts | 57 +- examples/devframe/devtools.ts | 13 + examples/devframe/extension/build.ts | 16 + examples/devframe/extension/popup.html | 44 + examples/devframe/extension/popup.ts | 60 + examples/devframe/extension/worker.ts | 85 ++ examples/devframe/host.ts | 71 + examples/devframe/package.json | 10 +- examples/devframe/smoke.ts | 139 +- knip.jsonc | 4 +- package.json | 3 + packages/broker/LICENSE | 21 + packages/broker/README.md | 32 + packages/broker/package.json | 62 + packages/broker/src/config.ts | 68 + packages/broker/src/contract.ts | 148 ++ packages/broker/src/error.ts | 12 + packages/broker/src/identity-store.ts | 127 ++ packages/broker/src/index.ts | 5 + .../broker/src/provider-authentication.ts | 128 ++ packages/broker/src/runtime.ts | 756 +++++++++++ packages/broker/src/session-client.ts | 158 +++ packages/broker/test/identity-store.test.ts | 101 ++ .../test/provider-authentication.test.ts | 64 + packages/broker/test/runtime.test.ts | 146 ++ packages/broker/tsconfig.json | 15 + packages/broker/tsdown.config.ts | 3 + packages/broker/vitest.config.ts | 3 + packages/core/package.json | 4 + packages/core/src/authentication.ts | 277 ++++ packages/core/src/authority.ts | 2 + packages/core/src/broker.ts | 12 + packages/core/src/client-target-connection.ts | 14 +- packages/core/src/grant-request.ts | 47 +- packages/core/test/grant-request.test.ts | 41 + packages/core/test/target-directory.test.ts | 27 + packages/core/tsdown.config.ts | 1 + packages/devframe/LICENSE | 21 + packages/devframe/README.md | 52 + packages/devframe/package.json | 76 ++ packages/devframe/src/client.ts | 251 ++++ packages/devframe/src/index.ts | 4 + packages/devframe/src/page-script.ts | 79 ++ packages/devframe/src/panel-definition.ts | 112 ++ packages/devframe/src/panel-view.ts | 89 ++ packages/devframe/src/panel.ts | 139 ++ packages/devframe/src/service.ts | 249 ++++ packages/devframe/src/wire.ts | 47 + packages/devframe/test/page-script.test.ts | 51 + packages/devframe/test/panel-view.test.ts | 38 + packages/devframe/test/panel.test.ts | 31 + packages/devframe/test/service.test.ts | 173 +++ packages/devframe/tsconfig.json | 15 + packages/devframe/tsdown.config.ts | 3 + packages/devframe/vite.config.ts | 18 + packages/devframe/vitest.config.ts | 3 + packages/extension/README.md | 12 + packages/extension/package.json | 11 +- packages/extension/src/agent-recovery.ts | 12 +- .../extension/src/child-session-router.ts | 27 +- packages/extension/src/chrome.ts | 496 +++++++ packages/extension/src/index.ts | 1 + packages/extension/src/notifications.ts | 205 +++ packages/extension/src/pairing-store.ts | 15 + packages/extension/src/provider-connection.ts | 21 + .../extension/src/selected-tab-publisher.ts | 37 +- .../extension/test/agent-recovery.test.ts | 40 + .../test/child-session-router.test.ts | 28 + packages/extension/test/notifications.test.ts | 75 + .../test/selected-tab-publisher.test.ts | 52 +- packages/extension/tsdown.config.ts | 2 + packages/mcp/src/index.ts | 200 ++- packages/mcp/test/locator-session.test.ts | 6 + packages/mcp/test/mvp-session.test.ts | 166 ++- packages/mcp/test/tool-session.test.ts | 1 + packages/websocket/src/authentication.ts | 276 +--- .../websocket/test/authentication.test.ts | 4 +- pnpm-lock.yaml | 507 +++++-- pnpm-workspace.yaml | 12 +- .../authenticated-agent-websocket.test.ts | 16 + tests/e2e/README.md | 13 + tests/e2e/devframe-native.test.ts | 200 +++ tests/e2e/fixtures/devframe-service-worker.ts | 53 + tests/e2e/fixtures/workflow-benchmark.ts | 64 + tests/e2e/native-agent-benchmark.test.ts | 35 +- tests/e2e/native-agent-loop.test.ts | 13 +- tsconfig.json | 2 + tsconfig.scripts.json | 1 + 97 files changed, 7589 insertions(+), 600 deletions(-) create mode 100644 docs/benchmarks/2026-09-08-latency.csv create mode 100644 docs/benchmarks/2026-09-08-latency.md create mode 100644 docs/research/devframe-toast-removal.md create mode 100644 examples/devframe/devtools.ts create mode 100644 examples/devframe/extension/build.ts create mode 100644 examples/devframe/extension/popup.html create mode 100644 examples/devframe/extension/popup.ts create mode 100644 examples/devframe/extension/worker.ts create mode 100644 examples/devframe/host.ts create mode 100644 packages/broker/LICENSE create mode 100644 packages/broker/README.md create mode 100644 packages/broker/package.json create mode 100644 packages/broker/src/config.ts create mode 100644 packages/broker/src/contract.ts create mode 100644 packages/broker/src/error.ts create mode 100644 packages/broker/src/identity-store.ts create mode 100644 packages/broker/src/index.ts create mode 100644 packages/broker/src/provider-authentication.ts create mode 100644 packages/broker/src/runtime.ts create mode 100644 packages/broker/src/session-client.ts create mode 100644 packages/broker/test/identity-store.test.ts create mode 100644 packages/broker/test/provider-authentication.test.ts create mode 100644 packages/broker/test/runtime.test.ts create mode 100644 packages/broker/tsconfig.json create mode 100644 packages/broker/tsdown.config.ts create mode 100644 packages/broker/vitest.config.ts create mode 100644 packages/core/src/authentication.ts create mode 100644 packages/devframe/LICENSE create mode 100644 packages/devframe/README.md create mode 100644 packages/devframe/package.json create mode 100644 packages/devframe/src/client.ts create mode 100644 packages/devframe/src/index.ts create mode 100644 packages/devframe/src/page-script.ts create mode 100644 packages/devframe/src/panel-definition.ts create mode 100644 packages/devframe/src/panel-view.ts create mode 100644 packages/devframe/src/panel.ts create mode 100644 packages/devframe/src/service.ts create mode 100644 packages/devframe/src/wire.ts create mode 100644 packages/devframe/test/page-script.test.ts create mode 100644 packages/devframe/test/panel-view.test.ts create mode 100644 packages/devframe/test/panel.test.ts create mode 100644 packages/devframe/test/service.test.ts create mode 100644 packages/devframe/tsconfig.json create mode 100644 packages/devframe/tsdown.config.ts create mode 100644 packages/devframe/vite.config.ts create mode 100644 packages/devframe/vitest.config.ts create mode 100644 packages/extension/src/chrome.ts create mode 100644 packages/extension/src/notifications.ts create mode 100644 packages/extension/src/provider-connection.ts create mode 100644 packages/extension/test/notifications.test.ts create mode 100644 tests/e2e/devframe-native.test.ts create mode 100644 tests/e2e/fixtures/devframe-service-worker.ts create mode 100644 tests/e2e/fixtures/workflow-benchmark.ts diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index e184347..54c9fe0 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -55,6 +55,24 @@ or resume-window expiry deletes the authority record and ends the logical sessio Session `metadata` is optional JSON-compatible data. CDB validates and stores it but never interprets it as identity, authority, or policy. +### Optional host orchestration + +`@dvcol/cdb-broker` composes the core stores, pairing, logical sessions, grant coordinator and +per-principal tool sessions. `defineBroker` is declarative; `createBroker` creates the runtime. +Management state is a projection of these components. Persistent identity storage preserves the +broker ID and existing provider pairings at a host-selected location. + +Navigation presets belong to this optional package, outside the protocol kernel. Each approval binds +the selected preset to a principal and target. Disallowed presets fail without downgrading. Target +generation renewal invalidates document-bound references independently of the navigation policy. + +`@dvcol/cdb-devframe` installs this runtime as a context-lifetime service. Provider messages, pairing +proofs and operation cancellation use the host's authenticated Devframe RPC peer. CDB channel closure +does not close that shared connection. Hosts forward peer lifecycle callbacks and dispose the service +on startup failure and shutdown. The optional panel consumes an existing management client and never +instantiates a broker. Registry aggregation, tool ownership/routing, MCP lifecycle and WebMCP +publication remain with the embedding application. + ### Grant request coordinator `createGrantRequestCoordinator` stores the authenticated logical session, principal, requested @@ -72,8 +90,9 @@ for the contract and runnable public example. ### Extension helpers `@dvcol/cdb-extension` provides publication and recovery mechanics that remain useful to a browser -extension but do not import Chrome APIs. The extension host supplies the Chrome adapter and user -approval policy. The separate `presentation` entry is an opt-in content-script helper. It translates +extension but do not import Chrome APIs. The optional `@dvcol/cdb-extension/chrome` entry composes +Chrome debugger, tab, navigation, alarm and storage bindings. The embedding application owns +permissions, installation, tab-selection policy and final approval trust. The separate `presentation` entry is an opt-in content-script helper. It translates successful CDP pointer commands into sanitized visual events and renders an isolated pointer plus a temporary favicon. The host owns installation, messaging, current grant state, and navigation reinjection. diff --git a/README.md b/README.md index 6fc72f0..9f3c268 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,8 @@ See [ARCHITECTURE.md](./ARCHITECTURE.md) for the protocol and failure model. See | ---------------------- | ---------------------------------------------------------------------------------------- | | `@dvcol/cdb` | Broker, client facade, protocol types, target authorization, leases, and command routing | | `@dvcol/cdb-birpc` | RPC transport adapter | +| `@dvcol/cdb-broker` | Shared pairing, grant, recovery and per-principal tool orchestration for hosts | +| `@dvcol/cdb-devframe` | Devframe service, shared provider RPC transport, and mountable browser-control panel | | `@dvcol/cdb-extension` | Browser-extension helpers for publication, recovery, and opt-in control presentation | | `@dvcol/cdb-mcp` | Principal-scoped semantic and raw MCP tool sessions over a CDB client | | `@dvcol/cdb-automation-playwright` | Experimental Playwright semantic provider over a broker-authorized CDB executor | diff --git a/docs/benchmarks/2026-09-08-latency.csv b/docs/benchmarks/2026-09-08-latency.csv new file mode 100644 index 0000000..bc6cb74 --- /dev/null +++ b/docs/benchmarks/2026-09-08-latency.csv @@ -0,0 +1,1201 @@ +run,tool,milliseconds,argument_characters,response_characters +devframe-before,browser.fill,665.914,118,56 +devframe-before,browser.click,642.185,97,57 +devframe-before,browser.snapshot,584.808,18,452 +devframe-before,browser.batch,1858.653,261,605 +devframe-before,browser.fill,655.935,118,56 +devframe-before,browser.click,667.392,97,57 +devframe-before,browser.snapshot,581.378,18,456 +devframe-before,browser.batch,1834.693,261,605 +devframe-before,browser.fill,650.006,118,56 +devframe-before,browser.click,651.773,97,57 +devframe-before,browser.snapshot,606.705,18,456 +devframe-before,browser.batch,1874.813,261,605 +devframe-before,browser.fill,650.686,118,56 +devframe-before,browser.click,658.995,97,57 +devframe-before,browser.snapshot,565.023,18,456 +devframe-before,browser.batch,1868.965,261,605 +devframe-before,browser.fill,701.848,118,56 +devframe-before,browser.click,784.83,97,57 +devframe-before,browser.snapshot,628.898,18,456 +devframe-before,browser.batch,2370.466,261,605 +devframe-before,browser.fill,1422.107,118,56 +devframe-before,browser.click,1199.71,97,57 +devframe-before,browser.snapshot,1102.314,18,456 +devframe-before,browser.batch,2795.679,261,605 +devframe-before,browser.fill,740.032,118,56 +devframe-before,browser.click,651.123,97,57 +devframe-before,browser.snapshot,551.803,18,456 +devframe-before,browser.batch,1856.037,261,605 +devframe-before,browser.fill,647.157,118,56 +devframe-before,browser.click,665.51,97,57 +devframe-before,browser.snapshot,607.204,18,456 +devframe-before,browser.batch,1927.29,261,605 +devframe-before,browser.fill,682.502,118,56 +devframe-before,browser.click,658.755,97,57 +devframe-before,browser.snapshot,591.102,18,456 +devframe-before,browser.batch,1914.958,261,605 +devframe-before,browser.fill,719.497,118,56 +devframe-before,browser.click,701.231,97,57 +devframe-before,browser.snapshot,930.418,18,457 +devframe-before,browser.batch,1855.581,261,610 +devframe-before,browser.fill,652.525,118,56 +devframe-before,browser.click,650.04,97,57 +devframe-before,browser.snapshot,558.767,18,461 +devframe-before,browser.batch,1807.236,261,610 +devframe-before,browser.fill,639.931,118,56 +devframe-before,browser.click,635.772,97,57 +devframe-before,browser.snapshot,574.127,18,461 +devframe-before,browser.batch,1808.154,261,610 +devframe-before,browser.fill,649.482,118,56 +devframe-before,browser.click,668.471,97,57 +devframe-before,browser.snapshot,565.056,18,461 +devframe-before,browser.batch,1827.637,261,610 +devframe-before,browser.fill,709.049,118,56 +devframe-before,browser.click,812.586,97,57 +devframe-before,browser.snapshot,692.224,18,461 +devframe-before,browser.batch,1882.954,261,610 +devframe-before,browser.fill,670.176,118,56 +devframe-before,browser.click,674.767,97,57 +devframe-before,browser.snapshot,579.444,18,461 +devframe-before,browser.batch,1798.696,261,610 +devframe-before,browser.fill,650.399,118,56 +devframe-before,browser.click,649.178,97,57 +devframe-before,browser.snapshot,576.029,18,461 +devframe-before,browser.batch,1800.113,261,610 +devframe-before,browser.fill,661.626,118,56 +devframe-before,browser.click,635.029,97,57 +devframe-before,browser.snapshot,570.537,18,461 +devframe-before,browser.batch,1822.384,261,610 +devframe-before,browser.fill,636.096,118,56 +devframe-before,browser.click,636.827,97,57 +devframe-before,browser.snapshot,579.231,18,461 +devframe-before,browser.batch,1804.186,261,610 +devframe-before,browser.fill,665.651,118,56 +devframe-before,browser.click,644.665,97,57 +devframe-before,browser.snapshot,557.239,18,461 +devframe-before,browser.batch,1907.898,261,610 +devframe-before,browser.fill,659.914,118,56 +devframe-before,browser.click,668.325,97,57 +devframe-before,browser.snapshot,568.779,18,461 +devframe-before,browser.batch,1811.382,261,610 +devframe-before,browser.fill,635.574,118,56 +devframe-before,browser.click,648.259,97,57 +devframe-before,browser.snapshot,599.448,18,461 +devframe-before,browser.batch,1817.285,261,610 +devframe-before,browser.fill,650.087,118,56 +devframe-before,browser.click,702.499,97,57 +devframe-before,browser.snapshot,556.187,18,461 +devframe-before,browser.batch,1836.006,261,610 +devframe-before,browser.fill,645.846,118,56 +devframe-before,browser.click,681.035,97,57 +devframe-before,browser.snapshot,617.855,18,461 +devframe-before,browser.batch,2031.881,261,610 +devframe-before,browser.fill,793.003,118,56 +devframe-before,browser.click,717.571,97,57 +devframe-before,browser.snapshot,760.318,18,461 +devframe-before,browser.batch,2161.035,261,610 +devframe-before,browser.fill,779.759,118,56 +devframe-before,browser.click,706.716,97,57 +devframe-before,browser.snapshot,605.787,18,461 +devframe-before,browser.batch,1877.26,261,610 +devframe-before,browser.fill,643.858,118,56 +devframe-before,browser.click,630.375,97,57 +devframe-before,browser.snapshot,576.75,18,461 +devframe-before,browser.batch,1807.053,261,610 +devframe-before,browser.fill,648.588,118,56 +devframe-before,browser.click,635.006,97,57 +devframe-before,browser.snapshot,554.175,18,461 +devframe-before,browser.batch,1791.192,261,610 +devframe-before,browser.fill,649.989,118,56 +devframe-before,browser.click,641.189,97,57 +devframe-before,browser.snapshot,565.795,18,461 +devframe-before,browser.batch,1798.55,261,610 +devframe-before,browser.fill,743.351,118,56 +devframe-before,browser.click,647.954,97,57 +devframe-before,browser.snapshot,562.255,18,461 +devframe-before,browser.batch,1803.327,261,610 +devframe-before,browser.fill,637.416,118,56 +devframe-before,browser.click,674.908,97,57 +devframe-before,browser.snapshot,605.911,18,461 +devframe-before,browser.batch,1978.871,261,610 +devframe-final,browser.fill,365.665,118,56 +devframe-final,browser.click,426.405,97,57 +devframe-final,browser.snapshot,856.457,18,452 +devframe-final,browser.batch,1174.22,261,605 +devframe-final,browser.fill,412.736,118,56 +devframe-final,browser.click,415.409,97,57 +devframe-final,browser.snapshot,683.858,18,456 +devframe-final,browser.batch,1337.171,261,605 +devframe-final,browser.fill,418.923,118,56 +devframe-final,browser.click,424.173,97,57 +devframe-final,browser.snapshot,610.804,18,456 +devframe-final,browser.batch,1314.38,261,605 +devframe-final,browser.fill,374.73,118,56 +devframe-final,browser.click,400.033,97,57 +devframe-final,browser.snapshot,633.617,18,456 +devframe-final,browser.batch,1277.608,261,605 +devframe-final,browser.fill,363.072,118,56 +devframe-final,browser.click,353.837,97,57 +devframe-final,browser.snapshot,641.252,18,456 +devframe-final,browser.batch,1223.789,261,605 +devframe-final,browser.fill,378.88,118,56 +devframe-final,browser.click,335.317,97,57 +devframe-final,browser.snapshot,745.524,18,456 +devframe-final,browser.batch,1229.394,261,605 +devframe-final,browser.fill,320.707,118,56 +devframe-final,browser.click,1033.579,97,57 +devframe-final,browser.snapshot,1693.964,18,456 +devframe-final,browser.batch,1187.673,261,605 +devframe-final,browser.fill,433.147,118,56 +devframe-final,browser.click,382.342,97,57 +devframe-final,browser.snapshot,592.484,18,456 +devframe-final,browser.batch,1417.023,261,605 +devframe-final,browser.fill,351.966,118,56 +devframe-final,browser.click,396.387,97,57 +devframe-final,browser.snapshot,675.106,18,456 +devframe-final,browser.batch,1299.463,261,605 +devframe-final,browser.fill,386.744,118,56 +devframe-final,browser.click,401.219,97,57 +devframe-final,browser.snapshot,634.678,18,457 +devframe-final,browser.batch,1423.854,261,610 +devframe-final,browser.fill,369.354,118,56 +devframe-final,browser.click,418.955,97,57 +devframe-final,browser.snapshot,615.119,18,461 +devframe-final,browser.batch,1324.838,261,610 +devframe-final,browser.fill,410.813,118,56 +devframe-final,browser.click,363.263,97,57 +devframe-final,browser.snapshot,667.424,18,461 +devframe-final,browser.batch,1227.627,261,610 +devframe-final,browser.fill,385.919,118,56 +devframe-final,browser.click,414.459,97,57 +devframe-final,browser.snapshot,648.423,18,461 +devframe-final,browser.batch,1231.09,261,610 +devframe-final,browser.fill,366.375,118,56 +devframe-final,browser.click,311.194,97,57 +devframe-final,browser.snapshot,645.656,18,461 +devframe-final,browser.batch,1201.598,261,610 +devframe-final,browser.fill,365.5,118,56 +devframe-final,browser.click,395.054,97,57 +devframe-final,browser.snapshot,591.751,18,461 +devframe-final,browser.batch,1332.65,261,610 +devframe-final,browser.fill,300.009,118,56 +devframe-final,browser.click,317.512,97,57 +devframe-final,browser.snapshot,934.723,18,461 +devframe-final,browser.batch,1895.619,261,610 +devframe-final,browser.fill,340.665,118,56 +devframe-final,browser.click,293.87,97,57 +devframe-final,browser.snapshot,731.419,18,461 +devframe-final,browser.batch,1222.864,261,610 +devframe-final,browser.fill,397.495,118,56 +devframe-final,browser.click,429.829,97,57 +devframe-final,browser.snapshot,660.538,18,461 +devframe-final,browser.batch,1381.368,261,610 +devframe-final,browser.fill,407.527,118,56 +devframe-final,browser.click,329.529,97,57 +devframe-final,browser.snapshot,738.213,18,461 +devframe-final,browser.batch,1184.988,261,610 +devframe-final,browser.fill,376.748,118,56 +devframe-final,browser.click,387.433,97,57 +devframe-final,browser.snapshot,598.81,18,461 +devframe-final,browser.batch,1331.361,261,610 +devframe-final,browser.fill,214.33,118,56 +devframe-final,browser.click,414.329,97,57 +devframe-final,browser.snapshot,671.419,18,461 +devframe-final,browser.batch,1306.107,261,610 +devframe-final,browser.fill,418.951,118,56 +devframe-final,browser.click,413.182,97,57 +devframe-final,browser.snapshot,625.544,18,461 +devframe-final,browser.batch,1255.71,261,610 +devframe-final,browser.fill,325.483,118,56 +devframe-final,browser.click,407.23,97,57 +devframe-final,browser.snapshot,578.919,18,461 +devframe-final,browser.batch,1973.472,261,610 +devframe-final,browser.fill,807.452,118,56 +devframe-final,browser.click,445.087,97,57 +devframe-final,browser.snapshot,872.154,18,461 +devframe-final,browser.batch,1235.236,261,610 +devframe-final,browser.fill,354.445,118,56 +devframe-final,browser.click,271.557,97,57 +devframe-final,browser.snapshot,706.537,18,461 +devframe-final,browser.batch,1338.806,261,610 +devframe-final,browser.fill,290.277,118,56 +devframe-final,browser.click,298.019,97,57 +devframe-final,browser.snapshot,692.617,18,461 +devframe-final,browser.batch,1262.667,261,610 +devframe-final,browser.fill,429.212,118,56 +devframe-final,browser.click,360.508,97,57 +devframe-final,browser.snapshot,638.915,18,461 +devframe-final,browser.batch,1230.06,261,610 +devframe-final,browser.fill,379.49,118,56 +devframe-final,browser.click,387.013,97,57 +devframe-final,browser.snapshot,677.798,18,461 +devframe-final,browser.batch,1190.268,261,610 +devframe-final,browser.fill,340.557,118,56 +devframe-final,browser.click,338.321,97,57 +devframe-final,browser.snapshot,905.419,18,461 +devframe-final,browser.batch,1827.455,261,610 +devframe-final,browser.fill,364.02,118,56 +devframe-final,browser.click,425.151,97,57 +devframe-final,browser.snapshot,641.693,18,461 +devframe-final,browser.batch,1304.13,261,610 +devframe-final,browser.click:blocked,2063.87,97,197 +devframe-final,browser.click:blocked,2001.917,97,197 +devframe-final,browser.click:blocked,2002.676,97,197 +devframe-final,browser.click:blocked,2267.219,97,197 +devframe-final,browser.click:blocked,2026.696,97,197 +devframe-final,browser.click:blocked,2022.197,97,197 +devframe-final,browser.click:blocked,2003.358,97,197 +devframe-final,browser.click:blocked,2000.568,97,197 +devframe-final,browser.click:blocked,2002.436,97,197 +devframe-final,browser.click:blocked,2002.609,97,197 +devframe-final,browser.click:blocked,2002.927,97,197 +devframe-final,browser.click:blocked,2010.17,97,197 +devframe-final,browser.click:blocked,2010.846,97,197 +devframe-final,browser.click:blocked,2130.67,97,197 +devframe-final,browser.click:blocked,2001.124,97,197 +devframe-final,browser.click:blocked,2022.038,97,197 +devframe-final,browser.click:blocked,2002.463,97,197 +devframe-final,browser.click:blocked,2001.258,97,197 +devframe-final,browser.click:blocked,1999.786,97,197 +devframe-final,browser.click:blocked,2001.313,97,197 +devframe-final,browser.click:blocked,2003.145,97,197 +devframe-final,browser.click:blocked,2034.358,97,197 +devframe-final,browser.click:blocked,2003.095,97,197 +devframe-final,browser.click:blocked,2004.31,97,197 +devframe-final,browser.click:blocked,2101.897,97,197 +devframe-final,browser.click:blocked,2002.523,97,197 +devframe-final,browser.click:blocked,2000.263,97,197 +devframe-final,browser.click:blocked,2002.981,97,197 +devframe-final,browser.click:blocked,2047.744,97,197 +devframe-final,browser.click:blocked,2000.552,97,197 +websocket-before,browser.snapshot,669.488,18,461 +websocket-before,browser.fill,107.358,63,56 +websocket-before,browser.click,98.717,30,57 +websocket-before,browser.snapshot,648.694,18,465 +websocket-before,browser.fill,104.245,64,56 +websocket-before,browser.click,101.4,30,57 +websocket-before,browser.snapshot,634.346,18,465 +websocket-before,browser.fill,107.394,64,56 +websocket-before,browser.click,95.947,30,57 +websocket-before,browser.snapshot,645.912,18,465 +websocket-before,browser.fill,99.365,64,56 +websocket-before,browser.click,95.047,30,57 +websocket-before,browser.snapshot,628.92,18,465 +websocket-before,browser.fill,103.562,64,56 +websocket-before,browser.click,93.565,30,57 +websocket-before,browser.snapshot,586.077,18,465 +websocket-before,browser.fill,102.316,64,56 +websocket-before,browser.click,91.576,30,57 +websocket-before,browser.snapshot,587.977,18,465 +websocket-before,browser.fill,99.923,64,56 +websocket-before,browser.click,93.552,30,57 +websocket-before,browser.snapshot,591.653,18,465 +websocket-before,browser.fill,104.3,64,56 +websocket-before,browser.click,91.37,30,57 +websocket-before,browser.snapshot,586.024,18,465 +websocket-before,browser.fill,97.763,64,56 +websocket-before,browser.click,91.134,30,57 +websocket-before,browser.snapshot,602.56,18,465 +websocket-before,browser.fill,100.16,64,56 +websocket-before,browser.click,92.146,30,57 +websocket-before,browser.snapshot,617.31,18,465 +websocket-before,browser.fill,99.967,64,56 +websocket-before,browser.click,92.836,30,57 +websocket-before,browser.snapshot,641.543,18,465 +websocket-before,browser.fill,103.994,64,56 +websocket-before,browser.click,93.359,30,57 +websocket-before,browser.snapshot,685.116,18,465 +websocket-before,browser.fill,100.713,64,56 +websocket-before,browser.click,97.235,30,57 +websocket-before,browser.snapshot,736.993,18,465 +websocket-before,browser.fill,174.623,64,56 +websocket-before,browser.click,107.565,30,57 +websocket-before,browser.snapshot,664.391,18,465 +websocket-before,browser.fill,123.743,64,56 +websocket-before,browser.click,126.464,30,57 +websocket-before,browser.snapshot,651.708,18,465 +websocket-before,browser.fill,121.566,64,56 +websocket-before,browser.click,104.143,30,57 +websocket-before,browser.snapshot,652.87,18,465 +websocket-before,browser.fill,108.478,64,56 +websocket-before,browser.click,105.452,30,57 +websocket-before,browser.snapshot,664.924,18,465 +websocket-before,browser.fill,108.928,64,56 +websocket-before,browser.click,106.008,30,57 +websocket-before,browser.snapshot,644.588,18,466 +websocket-before,browser.fill,104.343,64,56 +websocket-before,browser.click,91.799,31,57 +websocket-before,browser.snapshot,578.418,18,470 +websocket-before,browser.fill,107.828,65,56 +websocket-before,browser.click,97.319,31,57 +websocket-before,browser.snapshot,588.974,18,470 +websocket-before,browser.fill,98.441,65,56 +websocket-before,browser.click,89.004,31,57 +websocket-before,browser.snapshot,579.904,18,470 +websocket-before,browser.fill,100.574,65,56 +websocket-before,browser.click,88.702,31,57 +websocket-before,browser.snapshot,582.565,18,470 +websocket-before,browser.fill,108.908,65,56 +websocket-before,browser.click,94.634,31,57 +websocket-before,browser.snapshot,577.285,18,470 +websocket-before,browser.fill,98.635,65,56 +websocket-before,browser.click,90.2,31,57 +websocket-before,browser.snapshot,585.437,18,470 +websocket-before,browser.fill,100.935,65,56 +websocket-before,browser.click,89.988,31,57 +websocket-before,browser.snapshot,586.583,18,470 +websocket-before,browser.fill,100.97,65,56 +websocket-before,browser.click,91.652,31,57 +websocket-before,browser.snapshot,594.287,18,470 +websocket-before,browser.fill,101.197,65,56 +websocket-before,browser.click,92.215,31,57 +websocket-before,browser.snapshot,614.849,18,470 +websocket-before,browser.fill,98.747,65,56 +websocket-before,browser.click,89.656,31,57 +websocket-before,browser.snapshot,618.94,18,470 +websocket-before,browser.fill,101.782,65,56 +websocket-before,browser.click,92.166,31,57 +websocket-before,browser.snapshot,729.736,18,470 +websocket-before,browser.fill,121.325,65,56 +websocket-before,browser.click,112.373,31,57 +websocket-intermediate,browser.snapshot,608.936,18,461 +websocket-intermediate,browser.fill,103.588,63,56 +websocket-intermediate,browser.click,98.135,30,57 +websocket-intermediate,browser.snapshot,594.151,18,465 +websocket-intermediate,browser.fill,100.639,64,56 +websocket-intermediate,browser.click,89.73,30,57 +websocket-intermediate,browser.snapshot,603.3,18,465 +websocket-intermediate,browser.fill,106.552,64,56 +websocket-intermediate,browser.click,100.377,30,57 +websocket-intermediate,browser.snapshot,588.123,18,465 +websocket-intermediate,browser.fill,101.041,64,56 +websocket-intermediate,browser.click,92.876,30,57 +websocket-intermediate,browser.snapshot,590.964,18,465 +websocket-intermediate,browser.fill,98.536,64,56 +websocket-intermediate,browser.click,91.955,30,57 +websocket-intermediate,browser.snapshot,584.465,18,465 +websocket-intermediate,browser.fill,98.874,64,56 +websocket-intermediate,browser.click,91.799,30,57 +websocket-intermediate,browser.snapshot,587.74,18,465 +websocket-intermediate,browser.fill,96.755,64,56 +websocket-intermediate,browser.click,91.935,30,57 +websocket-intermediate,browser.snapshot,590.873,18,465 +websocket-intermediate,browser.fill,99.99,64,56 +websocket-intermediate,browser.click,90.84,30,57 +websocket-intermediate,browser.snapshot,584.226,18,465 +websocket-intermediate,browser.fill,98.558,64,56 +websocket-intermediate,browser.click,89.69,30,57 +websocket-intermediate,browser.snapshot,600.072,18,465 +websocket-intermediate,browser.fill,100.185,64,56 +websocket-intermediate,browser.click,88.941,30,57 +websocket-intermediate,browser.snapshot,589.91,18,465 +websocket-intermediate,browser.fill,97.86,64,56 +websocket-intermediate,browser.click,90.107,30,57 +websocket-intermediate,browser.snapshot,583.771,18,465 +websocket-intermediate,browser.fill,103.508,64,56 +websocket-intermediate,browser.click,99.841,30,57 +websocket-intermediate,browser.snapshot,594.442,18,465 +websocket-intermediate,browser.fill,106.894,64,56 +websocket-intermediate,browser.click,92.404,30,57 +websocket-intermediate,browser.snapshot,594.619,18,465 +websocket-intermediate,browser.fill,95.915,64,56 +websocket-intermediate,browser.click,89.827,30,57 +websocket-intermediate,browser.snapshot,598.961,18,465 +websocket-intermediate,browser.fill,128.0,64,56 +websocket-intermediate,browser.click,95.073,30,57 +websocket-intermediate,browser.snapshot,629.819,18,465 +websocket-intermediate,browser.fill,120.957,64,56 +websocket-intermediate,browser.click,119.898,30,57 +websocket-intermediate,browser.snapshot,662.34,18,465 +websocket-intermediate,browser.fill,96.935,64,56 +websocket-intermediate,browser.click,91.573,30,57 +websocket-intermediate,browser.snapshot,629.71,18,465 +websocket-intermediate,browser.fill,98.913,64,56 +websocket-intermediate,browser.click,88.664,30,57 +websocket-intermediate,browser.snapshot,609.257,18,466 +websocket-intermediate,browser.fill,97.821,64,56 +websocket-intermediate,browser.click,90.31,31,57 +websocket-intermediate,browser.snapshot,612.338,18,470 +websocket-intermediate,browser.fill,101.064,65,56 +websocket-intermediate,browser.click,93.912,31,57 +websocket-intermediate,browser.snapshot,657.423,18,470 +websocket-intermediate,browser.fill,102.622,65,56 +websocket-intermediate,browser.click,92.293,31,57 +websocket-intermediate,browser.snapshot,637.513,18,470 +websocket-intermediate,browser.fill,96.8,65,56 +websocket-intermediate,browser.click,87.089,31,57 +websocket-intermediate,browser.snapshot,640.743,18,470 +websocket-intermediate,browser.fill,104.685,65,56 +websocket-intermediate,browser.click,98.108,31,57 +websocket-intermediate,browser.snapshot,606.495,18,470 +websocket-intermediate,browser.fill,99.052,65,56 +websocket-intermediate,browser.click,87.919,31,57 +websocket-intermediate,browser.snapshot,608.234,18,470 +websocket-intermediate,browser.fill,97.604,65,56 +websocket-intermediate,browser.click,90.315,31,57 +websocket-intermediate,browser.snapshot,583.847,18,470 +websocket-intermediate,browser.fill,98.28,65,56 +websocket-intermediate,browser.click,89.727,31,57 +websocket-intermediate,browser.snapshot,598.92,18,470 +websocket-intermediate,browser.fill,98.668,65,56 +websocket-intermediate,browser.click,91.766,31,57 +websocket-intermediate,browser.snapshot,593.021,18,470 +websocket-intermediate,browser.fill,97.96,65,56 +websocket-intermediate,browser.click,90.371,31,57 +websocket-intermediate,browser.snapshot,637.53,18,470 +websocket-intermediate,browser.fill,99.277,65,56 +websocket-intermediate,browser.click,90.054,31,57 +websocket-intermediate,browser.snapshot,602.92,18,470 +websocket-intermediate,browser.fill,102.884,65,56 +websocket-intermediate,browser.click,90.275,31,57 +websocket-final,browser.snapshot,664.745,18,461 +websocket-final,browser.fill,105.674,63,56 +websocket-final,browser.click,94.057,30,57 +websocket-final,browser.snapshot,624.873,18,465 +websocket-final,browser.fill,101.705,64,56 +websocket-final,browser.click,94.87,30,57 +websocket-final,browser.snapshot,629.386,18,465 +websocket-final,browser.fill,100.401,64,56 +websocket-final,browser.click,90.052,30,57 +websocket-final,browser.snapshot,615.24,18,465 +websocket-final,browser.fill,101.273,64,56 +websocket-final,browser.click,125.812,30,57 +websocket-final,browser.snapshot,652.561,18,465 +websocket-final,browser.fill,98.598,64,56 +websocket-final,browser.click,93.108,30,57 +websocket-final,browser.snapshot,615.302,18,465 +websocket-final,browser.fill,97.775,64,56 +websocket-final,browser.click,93.095,30,57 +websocket-final,browser.snapshot,631.475,18,465 +websocket-final,browser.fill,104.903,64,56 +websocket-final,browser.click,93.588,30,57 +websocket-final,browser.snapshot,628.054,18,465 +websocket-final,browser.fill,98.46,64,56 +websocket-final,browser.click,91.984,30,57 +websocket-final,browser.snapshot,599.163,18,465 +websocket-final,browser.fill,99.428,64,56 +websocket-final,browser.click,90.135,30,57 +websocket-final,browser.snapshot,600.259,18,465 +websocket-final,browser.fill,100.452,64,56 +websocket-final,browser.click,91.013,30,57 +websocket-final,browser.snapshot,604.582,18,465 +websocket-final,browser.fill,99.086,64,56 +websocket-final,browser.click,94.497,30,57 +websocket-final,browser.snapshot,626.666,18,465 +websocket-final,browser.fill,99.997,64,56 +websocket-final,browser.click,94.632,30,57 +websocket-final,browser.snapshot,625.802,18,465 +websocket-final,browser.fill,99.643,64,56 +websocket-final,browser.click,90.132,30,57 +websocket-final,browser.snapshot,612.176,18,465 +websocket-final,browser.fill,97.543,64,56 +websocket-final,browser.click,90.904,30,57 +websocket-final,browser.snapshot,609.463,18,465 +websocket-final,browser.fill,97.819,64,56 +websocket-final,browser.click,90.599,30,57 +websocket-final,browser.snapshot,611.608,18,465 +websocket-final,browser.fill,107.402,64,56 +websocket-final,browser.click,98.54,30,57 +websocket-final,browser.snapshot,1157.887,18,465 +websocket-final,browser.fill,225.363,64,56 +websocket-final,browser.click,169.617,30,57 +websocket-final,browser.snapshot,1418.676,18,465 +websocket-final,browser.fill,538.377,64,56 +websocket-final,browser.click,272.395,30,57 +websocket-final,browser.snapshot,1261.849,18,466 +websocket-final,browser.fill,186.248,64,56 +websocket-final,browser.click,161.84,31,57 +websocket-final,browser.snapshot,1940.601,18,470 +websocket-final,browser.fill,235.379,65,56 +websocket-final,browser.click,291.711,31,57 +websocket-final,browser.snapshot,1876.544,18,470 +websocket-final,browser.fill,400.25,65,56 +websocket-final,browser.click,149.508,31,57 +websocket-final,browser.snapshot,1871.929,18,470 +websocket-final,browser.fill,211.004,65,56 +websocket-final,browser.click,104.619,31,57 +websocket-final,browser.snapshot,1205.999,18,470 +websocket-final,browser.fill,169.634,65,56 +websocket-final,browser.click,142.133,31,57 +websocket-final,browser.snapshot,1651.754,18,470 +websocket-final,browser.fill,168.634,65,56 +websocket-final,browser.click,114.817,31,57 +websocket-final,browser.snapshot,669.3,18,470 +websocket-final,browser.fill,106.903,65,56 +websocket-final,browser.click,91.782,31,57 +websocket-final,browser.snapshot,736.557,18,470 +websocket-final,browser.fill,185.907,65,56 +websocket-final,browser.click,256.563,31,57 +websocket-final,browser.snapshot,1579.727,18,470 +websocket-final,browser.fill,234.339,65,56 +websocket-final,browser.click,355.891,31,57 +websocket-final,browser.snapshot,970.507,18,470 +websocket-final,browser.fill,104.105,65,56 +websocket-final,browser.click,96.146,31,57 +websocket-final,browser.snapshot,617.855,18,470 +websocket-final,browser.fill,99.622,65,56 +websocket-final,browser.click,96.835,31,57 +websocket-final,browser.snapshot,596.08,18,470 +websocket-final,browser.fill,97.688,65,56 +websocket-final,browser.click,90.159,31,57 +websocket-validation-validation,browser.snapshot,1182.863,18,461 +websocket-validation-validation,browser.fill,114.374,63,56 +websocket-validation-validation,browser.click,111.908,30,57 +websocket-validation-validation,browser.snapshot,700.686,18,465 +websocket-validation-validation,browser.fill,149.448,64,56 +websocket-validation-validation,browser.click,147.34,30,57 +websocket-validation-validation,browser.snapshot,740.642,18,465 +websocket-validation-validation,browser.fill,124.896,64,56 +websocket-validation-validation,browser.click,104.523,30,57 +websocket-validation-validation,browser.snapshot,661.463,18,465 +websocket-validation-validation,browser.fill,105.836,64,56 +websocket-validation-validation,browser.click,103.252,30,57 +websocket-validation-validation,browser.snapshot,662.27,18,465 +websocket-validation-validation,browser.fill,117.871,64,56 +websocket-validation-validation,browser.click,105.21,30,57 +websocket-validation-validation,browser.snapshot,751.368,18,465 +websocket-validation-validation,browser.fill,143.406,64,56 +websocket-validation-validation,browser.click,102.786,30,57 +websocket-validation-validation,browser.snapshot,807.09,18,465 +websocket-validation-validation,browser.fill,103.368,64,56 +websocket-validation-validation,browser.click,97.203,30,57 +websocket-validation-validation,browser.snapshot,651.861,18,465 +websocket-validation-validation,browser.fill,108.163,64,56 +websocket-validation-validation,browser.click,98.198,30,57 +websocket-validation-validation,browser.snapshot,620.001,18,465 +websocket-validation-validation,browser.fill,127.217,64,56 +websocket-validation-validation,browser.click,97.072,30,57 +websocket-validation-validation,browser.snapshot,627.498,18,465 +websocket-validation-validation,browser.fill,99.381,64,56 +websocket-validation-validation,browser.click,96.191,30,57 +websocket-validation-validation,browser.snapshot,663.068,18,465 +websocket-validation-validation,browser.fill,172.38,64,56 +websocket-validation-validation,browser.click,123.333,30,57 +websocket-validation-validation,browser.snapshot,664.126,18,465 +websocket-validation-validation,browser.fill,126.783,64,56 +websocket-validation-validation,browser.click,101.684,30,57 +websocket-validation-validation,browser.snapshot,649.343,18,465 +websocket-validation-validation,browser.fill,112.777,64,56 +websocket-validation-validation,browser.click,107.175,30,57 +websocket-validation-validation,browser.snapshot,1017.371,18,465 +websocket-validation-validation,browser.fill,128.802,64,56 +websocket-validation-validation,browser.click,101.155,30,57 +websocket-validation-validation,browser.snapshot,771.506,18,465 +websocket-validation-validation,browser.fill,103.969,64,56 +websocket-validation-validation,browser.click,95.207,30,57 +websocket-validation-validation,browser.snapshot,648.612,18,465 +websocket-validation-validation,browser.fill,103.409,64,56 +websocket-validation-validation,browser.click,97.664,30,57 +websocket-validation-validation,browser.snapshot,732.147,18,465 +websocket-validation-validation,browser.fill,180.856,64,56 +websocket-validation-validation,browser.click,169.003,30,57 +websocket-validation-validation,browser.snapshot,838.509,18,465 +websocket-validation-validation,browser.fill,107.561,64,56 +websocket-validation-validation,browser.click,96.55,30,57 +websocket-validation-validation,browser.snapshot,672.017,18,466 +websocket-validation-validation,browser.fill,136.635,64,56 +websocket-validation-validation,browser.click,103.038,31,57 +websocket-validation-validation,browser.snapshot,698.326,18,470 +websocket-validation-validation,browser.fill,209.049,65,56 +websocket-validation-validation,browser.click,134.582,31,57 +websocket-validation-validation,browser.snapshot,666.06,18,470 +websocket-validation-validation,browser.fill,299.16,65,56 +websocket-validation-validation,browser.click,113.401,31,57 +websocket-validation-validation,browser.snapshot,750.514,18,470 +websocket-validation-validation,browser.fill,212.186,65,56 +websocket-validation-validation,browser.click,118.544,31,57 +websocket-validation-validation,browser.snapshot,1289.783,18,470 +websocket-validation-validation,browser.fill,119.847,65,56 +websocket-validation-validation,browser.click,127.474,31,57 +websocket-validation-validation,browser.snapshot,622.654,18,470 +websocket-validation-validation,browser.fill,134.033,65,56 +websocket-validation-validation,browser.click,92.134,31,57 +websocket-validation-validation,browser.snapshot,601.283,18,470 +websocket-validation-validation,browser.fill,130.64,65,56 +websocket-validation-validation,browser.click,99.477,31,57 +websocket-validation-validation,browser.snapshot,601.336,18,470 +websocket-validation-validation,browser.fill,103.909,65,56 +websocket-validation-validation,browser.click,102.546,31,57 +websocket-validation-validation,browser.snapshot,704.16,18,470 +websocket-validation-validation,browser.fill,103.194,65,56 +websocket-validation-validation,browser.click,92.169,31,57 +websocket-validation-validation,browser.snapshot,637.63,18,470 +websocket-validation-validation,browser.fill,109.829,65,56 +websocket-validation-validation,browser.click,99.667,31,57 +websocket-validation-validation,browser.snapshot,639.144,18,470 +websocket-validation-validation,browser.fill,106.618,65,56 +websocket-validation-validation,browser.click,92.268,31,57 +websocket-validation-validation,browser.snapshot,643.621,18,470 +websocket-validation-validation,browser.fill,135.937,65,56 +websocket-validation-validation,browser.click,96.254,31,57 +websocket-validation-profile,browser.snapshot,671.33,18,461 +websocket-validation-profile,browser.fill,107.326,63,56 +websocket-validation-profile,browser.click,92.857,30,57 +websocket-validation-profile,browser.snapshot,627.918,18,465 +websocket-validation-profile,browser.fill,109.345,64,56 +websocket-validation-profile,browser.click,103.387,30,57 +websocket-validation-profile,browser.snapshot,877.921,18,465 +websocket-validation-profile,browser.fill,118.389,64,56 +websocket-validation-profile,browser.click,92.472,30,57 +websocket-validation-profile,browser.snapshot,641.717,18,465 +websocket-validation-profile,browser.fill,127.872,64,56 +websocket-validation-profile,browser.click,94.146,30,57 +websocket-validation-profile,browser.snapshot,614.898,18,465 +websocket-validation-profile,browser.fill,106.751,64,56 +websocket-validation-profile,browser.click,94.677,30,57 +websocket-validation-profile,browser.snapshot,673.926,18,465 +websocket-validation-profile,browser.fill,109.068,64,56 +websocket-validation-profile,browser.click,93.791,30,57 +websocket-validation-profile,browser.snapshot,647.503,18,465 +websocket-validation-profile,browser.fill,99.951,64,56 +websocket-validation-profile,browser.click,90.514,30,57 +websocket-validation-profile,browser.snapshot,607.553,18,465 +websocket-validation-profile,browser.fill,98.977,64,56 +websocket-validation-profile,browser.click,89.501,30,57 +websocket-validation-profile,browser.snapshot,602.448,18,465 +websocket-validation-profile,browser.fill,95.923,64,56 +websocket-validation-profile,browser.click,91.362,30,57 +websocket-validation-profile,browser.snapshot,603.778,18,465 +websocket-validation-profile,browser.fill,102.73,64,56 +websocket-validation-profile,browser.click,90.205,30,57 +websocket-validation-profile,browser.snapshot,640.702,18,465 +websocket-validation-profile,browser.fill,100.103,64,56 +websocket-validation-profile,browser.click,95.62,30,57 +websocket-validation-profile,browser.snapshot,693.598,18,465 +websocket-validation-profile,browser.fill,130.695,64,56 +websocket-validation-profile,browser.click,107.693,30,57 +websocket-validation-profile,browser.snapshot,637.21,18,465 +websocket-validation-profile,browser.fill,96.849,64,56 +websocket-validation-profile,browser.click,89.902,30,57 +websocket-validation-profile,browser.snapshot,632.534,18,465 +websocket-validation-profile,browser.fill,99.77,64,56 +websocket-validation-profile,browser.click,92.564,30,57 +websocket-validation-profile,browser.snapshot,692.932,18,465 +websocket-validation-profile,browser.fill,115.088,64,56 +websocket-validation-profile,browser.click,136.562,30,57 +websocket-validation-profile,browser.snapshot,649.236,18,465 +websocket-validation-profile,browser.fill,101.412,64,56 +websocket-validation-profile,browser.click,117.074,30,57 +websocket-validation-profile,browser.snapshot,625.749,18,465 +websocket-validation-profile,browser.fill,105.055,64,56 +websocket-validation-profile,browser.click,91.302,30,57 +websocket-validation-profile,browser.snapshot,621.003,18,465 +websocket-validation-profile,browser.fill,111.529,64,56 +websocket-validation-profile,browser.click,89.884,30,57 +websocket-validation-profile,browser.snapshot,791.033,18,466 +websocket-validation-profile,browser.fill,113.189,64,56 +websocket-validation-profile,browser.click,90.055,31,57 +websocket-validation-profile,browser.snapshot,604.588,18,470 +websocket-validation-profile,browser.fill,100.27,65,56 +websocket-validation-profile,browser.click,101.367,31,57 +websocket-validation-profile,browser.snapshot,627.667,18,470 +websocket-validation-profile,browser.fill,124.06,65,56 +websocket-validation-profile,browser.click,109.44,31,57 +websocket-validation-profile,browser.snapshot,631.615,18,470 +websocket-validation-profile,browser.fill,104.461,65,56 +websocket-validation-profile,browser.click,92.172,31,57 +websocket-validation-profile,browser.snapshot,617.246,18,470 +websocket-validation-profile,browser.fill,97.765,65,56 +websocket-validation-profile,browser.click,89.745,31,57 +websocket-validation-profile,browser.snapshot,593.626,18,470 +websocket-validation-profile,browser.fill,98.506,65,56 +websocket-validation-profile,browser.click,101.134,31,57 +websocket-validation-profile,browser.snapshot,654.203,18,470 +websocket-validation-profile,browser.fill,107.819,65,56 +websocket-validation-profile,browser.click,92.541,31,57 +websocket-validation-profile,browser.snapshot,700.239,18,470 +websocket-validation-profile,browser.fill,123.17,65,56 +websocket-validation-profile,browser.click,97.625,31,57 +websocket-validation-profile,browser.snapshot,674.439,18,470 +websocket-validation-profile,browser.fill,117.989,65,56 +websocket-validation-profile,browser.click,92.777,31,57 +websocket-validation-profile,browser.snapshot,629.703,18,470 +websocket-validation-profile,browser.fill,100.347,65,56 +websocket-validation-profile,browser.click,99.607,31,57 +websocket-validation-profile,browser.snapshot,616.109,18,470 +websocket-validation-profile,browser.fill,97.113,65,56 +websocket-validation-profile,browser.click,88.521,31,57 +websocket-validation-profile,browser.snapshot,602.571,18,470 +websocket-validation-profile,browser.fill,98.539,65,56 +websocket-validation-profile,browser.click,92.199,31,57 +websocket-validation-repeat,browser.snapshot,689.002,18,461 +websocket-validation-repeat,browser.fill,103.37,63,56 +websocket-validation-repeat,browser.click,92.261,30,57 +websocket-validation-repeat,browser.snapshot,642.689,18,465 +websocket-validation-repeat,browser.fill,101.016,64,56 +websocket-validation-repeat,browser.click,121.676,30,57 +websocket-validation-repeat,browser.snapshot,879.289,18,465 +websocket-validation-repeat,browser.fill,102.015,64,56 +websocket-validation-repeat,browser.click,93.136,30,57 +websocket-validation-repeat,browser.snapshot,701.735,18,465 +websocket-validation-repeat,browser.fill,99.535,64,56 +websocket-validation-repeat,browser.click,91.496,30,57 +websocket-validation-repeat,browser.snapshot,624.829,18,465 +websocket-validation-repeat,browser.fill,105.156,64,56 +websocket-validation-repeat,browser.click,93.65,30,57 +websocket-validation-repeat,browser.snapshot,677.641,18,465 +websocket-validation-repeat,browser.fill,100.008,64,56 +websocket-validation-repeat,browser.click,123.94,30,57 +websocket-validation-repeat,browser.snapshot,690.193,18,465 +websocket-validation-repeat,browser.fill,110.545,64,56 +websocket-validation-repeat,browser.click,93.448,30,57 +websocket-validation-repeat,browser.snapshot,646.878,18,465 +websocket-validation-repeat,browser.fill,100.988,64,56 +websocket-validation-repeat,browser.click,92.194,30,57 +websocket-validation-repeat,browser.snapshot,635.47,18,465 +websocket-validation-repeat,browser.fill,101.433,64,56 +websocket-validation-repeat,browser.click,91.104,30,57 +websocket-validation-repeat,browser.snapshot,632.336,18,465 +websocket-validation-repeat,browser.fill,97.851,64,56 +websocket-validation-repeat,browser.click,90.533,30,57 +websocket-validation-repeat,browser.snapshot,601.599,18,465 +websocket-validation-repeat,browser.fill,97.693,64,56 +websocket-validation-repeat,browser.click,90.213,30,57 +websocket-validation-repeat,browser.snapshot,627.206,18,465 +websocket-validation-repeat,browser.fill,108.616,64,56 +websocket-validation-repeat,browser.click,92.066,30,57 +websocket-validation-repeat,browser.snapshot,650.785,18,465 +websocket-validation-repeat,browser.fill,98.612,64,56 +websocket-validation-repeat,browser.click,91.203,30,57 +websocket-validation-repeat,browser.snapshot,632.07,18,465 +websocket-validation-repeat,browser.fill,94.721,64,56 +websocket-validation-repeat,browser.click,87.656,30,57 +websocket-validation-repeat,browser.snapshot,617.111,18,465 +websocket-validation-repeat,browser.fill,101.577,64,56 +websocket-validation-repeat,browser.click,96.785,30,57 +websocket-validation-repeat,browser.snapshot,615.053,18,465 +websocket-validation-repeat,browser.fill,99.802,64,56 +websocket-validation-repeat,browser.click,90.256,30,57 +websocket-validation-repeat,browser.snapshot,608.767,18,465 +websocket-validation-repeat,browser.fill,100.073,64,56 +websocket-validation-repeat,browser.click,92.157,30,57 +websocket-validation-repeat,browser.snapshot,635.634,18,465 +websocket-validation-repeat,browser.fill,100.064,64,56 +websocket-validation-repeat,browser.click,90.31,30,57 +websocket-validation-repeat,browser.snapshot,614.558,18,466 +websocket-validation-repeat,browser.fill,96.245,64,56 +websocket-validation-repeat,browser.click,91.895,31,57 +websocket-validation-repeat,browser.snapshot,629.242,18,470 +websocket-validation-repeat,browser.fill,97.473,65,56 +websocket-validation-repeat,browser.click,90.146,31,57 +websocket-validation-repeat,browser.snapshot,615.515,18,470 +websocket-validation-repeat,browser.fill,96.739,65,56 +websocket-validation-repeat,browser.click,89.612,31,57 +websocket-validation-repeat,browser.snapshot,645.705,18,470 +websocket-validation-repeat,browser.fill,111.366,65,56 +websocket-validation-repeat,browser.click,93.142,31,57 +websocket-validation-repeat,browser.snapshot,644.829,18,470 +websocket-validation-repeat,browser.fill,97.446,65,56 +websocket-validation-repeat,browser.click,90.334,31,57 +websocket-validation-repeat,browser.snapshot,613.952,18,470 +websocket-validation-repeat,browser.fill,96.737,65,56 +websocket-validation-repeat,browser.click,91.277,31,57 +websocket-validation-repeat,browser.snapshot,623.556,18,470 +websocket-validation-repeat,browser.fill,97.326,65,56 +websocket-validation-repeat,browser.click,88.265,31,57 +websocket-validation-repeat,browser.snapshot,645.855,18,470 +websocket-validation-repeat,browser.fill,95.207,65,56 +websocket-validation-repeat,browser.click,89.18,31,57 +websocket-validation-repeat,browser.snapshot,679.68,18,470 +websocket-validation-repeat,browser.fill,104.128,65,56 +websocket-validation-repeat,browser.click,94.426,31,57 +websocket-validation-repeat,browser.snapshot,646.129,18,470 +websocket-validation-repeat,browser.fill,97.047,65,56 +websocket-validation-repeat,browser.click,87.523,31,57 +websocket-validation-repeat,browser.snapshot,620.01,18,470 +websocket-validation-repeat,browser.fill,97.241,65,56 +websocket-validation-repeat,browser.click,90.514,31,57 +websocket-validation-repeat,browser.snapshot,634.757,18,470 +websocket-validation-repeat,browser.fill,97.123,65,56 +websocket-validation-repeat,browser.click,91.166,31,57 +websocket-validation-final,browser.snapshot,609.194,18,461 +websocket-validation-final,browser.fill,100.946,63,56 +websocket-validation-final,browser.click,94.02,30,57 +websocket-validation-final,browser.snapshot,605.819,18,465 +websocket-validation-final,browser.fill,99.498,64,56 +websocket-validation-final,browser.click,91.49,30,57 +websocket-validation-final,browser.snapshot,602.858,18,465 +websocket-validation-final,browser.fill,98.328,64,56 +websocket-validation-final,browser.click,90.682,30,57 +websocket-validation-final,browser.snapshot,587.73,18,465 +websocket-validation-final,browser.fill,111.221,64,56 +websocket-validation-final,browser.click,91.895,30,57 +websocket-validation-final,browser.snapshot,627.105,18,465 +websocket-validation-final,browser.fill,101.407,64,56 +websocket-validation-final,browser.click,90.306,30,57 +websocket-validation-final,browser.snapshot,634.053,18,465 +websocket-validation-final,browser.fill,100.227,64,56 +websocket-validation-final,browser.click,96.394,30,57 +websocket-validation-final,browser.snapshot,636.025,18,465 +websocket-validation-final,browser.fill,99.626,64,56 +websocket-validation-final,browser.click,91.48,30,57 +websocket-validation-final,browser.snapshot,580.655,18,465 +websocket-validation-final,browser.fill,95.952,64,56 +websocket-validation-final,browser.click,90.212,30,57 +websocket-validation-final,browser.snapshot,612.975,18,465 +websocket-validation-final,browser.fill,105.298,64,56 +websocket-validation-final,browser.click,91.447,30,57 +websocket-validation-final,browser.snapshot,603.648,18,465 +websocket-validation-final,browser.fill,99.626,64,56 +websocket-validation-final,browser.click,89.335,30,57 +websocket-validation-final,browser.snapshot,596.111,18,465 +websocket-validation-final,browser.fill,94.555,64,56 +websocket-validation-final,browser.click,87.943,30,57 +websocket-validation-final,browser.snapshot,588.854,18,465 +websocket-validation-final,browser.fill,98.507,64,56 +websocket-validation-final,browser.click,91.054,30,57 +websocket-validation-final,browser.snapshot,588.151,18,465 +websocket-validation-final,browser.fill,94.622,64,56 +websocket-validation-final,browser.click,89.397,30,57 +websocket-validation-final,browser.snapshot,611.732,18,465 +websocket-validation-final,browser.fill,95.015,64,56 +websocket-validation-final,browser.click,85.09,30,57 +websocket-validation-final,browser.snapshot,625.254,18,465 +websocket-validation-final,browser.fill,98.26,64,56 +websocket-validation-final,browser.click,88.361,30,57 +websocket-validation-final,browser.snapshot,626.148,18,465 +websocket-validation-final,browser.fill,95.427,64,56 +websocket-validation-final,browser.click,89.357,30,57 +websocket-validation-final,browser.snapshot,614.862,18,465 +websocket-validation-final,browser.fill,97.836,64,56 +websocket-validation-final,browser.click,89.167,30,57 +websocket-validation-final,browser.snapshot,620.83,18,465 +websocket-validation-final,browser.fill,93.296,64,56 +websocket-validation-final,browser.click,86.383,30,57 +websocket-validation-final,browser.snapshot,711.974,18,466 +websocket-validation-final,browser.fill,131.279,64,56 +websocket-validation-final,browser.click,106.474,31,57 +websocket-validation-final,browser.snapshot,671.268,18,470 +websocket-validation-final,browser.fill,146.599,65,56 +websocket-validation-final,browser.click,102.216,31,57 +websocket-validation-final,browser.snapshot,693.128,18,470 +websocket-validation-final,browser.fill,109.694,65,56 +websocket-validation-final,browser.click,97.09,31,57 +websocket-validation-final,browser.snapshot,841.208,18,470 +websocket-validation-final,browser.fill,98.365,65,56 +websocket-validation-final,browser.click,99.011,31,57 +websocket-validation-final,browser.snapshot,634.851,18,470 +websocket-validation-final,browser.fill,99.78,65,56 +websocket-validation-final,browser.click,92.592,31,57 +websocket-validation-final,browser.snapshot,697.512,18,470 +websocket-validation-final,browser.fill,130.975,65,56 +websocket-validation-final,browser.click,92.891,31,57 +websocket-validation-final,browser.snapshot,681.105,18,470 +websocket-validation-final,browser.fill,105.034,65,56 +websocket-validation-final,browser.click,104.193,31,57 +websocket-validation-final,browser.snapshot,613.601,18,470 +websocket-validation-final,browser.fill,103.605,65,56 +websocket-validation-final,browser.click,108.998,31,57 +websocket-validation-final,browser.snapshot,708.145,18,470 +websocket-validation-final,browser.fill,98.159,65,56 +websocket-validation-final,browser.click,92.185,31,57 +websocket-validation-final,browser.snapshot,692.54,18,470 +websocket-validation-final,browser.fill,108.658,65,56 +websocket-validation-final,browser.click,111.316,31,57 +websocket-validation-final,browser.snapshot,670.807,18,470 +websocket-validation-final,browser.fill,101.042,65,56 +websocket-validation-final,browser.click,91.422,31,57 +websocket-validation-final,browser.snapshot,660.968,18,470 +websocket-validation-final,browser.fill,100.358,65,56 +websocket-validation-final,browser.click,95.742,31,57 +devframe-validation-validation,browser.fill,340.097,118,56 +devframe-validation-validation,browser.click,292.802,97,57 +devframe-validation-validation,browser.snapshot,632.067,18,452 +devframe-validation-validation,browser.batch,1186.612,261,605 +devframe-validation-validation,browser.fill,383.29,118,56 +devframe-validation-validation,browser.click,317.468,97,57 +devframe-validation-validation,browser.snapshot,608.528,18,456 +devframe-validation-validation,browser.batch,1281.218,261,605 +devframe-validation-validation,browser.fill,387.383,118,56 +devframe-validation-validation,browser.click,266.776,97,57 +devframe-validation-validation,browser.snapshot,671.853,18,456 +devframe-validation-validation,browser.batch,1310.824,261,605 +devframe-validation-validation,browser.fill,325.141,118,56 +devframe-validation-validation,browser.click,342.566,97,57 +devframe-validation-validation,browser.snapshot,593.86,18,456 +devframe-validation-validation,browser.batch,1265.797,261,605 +devframe-validation-validation,browser.fill,407.939,118,56 +devframe-validation-validation,browser.click,320.753,97,57 +devframe-validation-validation,browser.snapshot,598.263,18,456 +devframe-validation-validation,browser.batch,1317.02,261,605 +devframe-validation-validation,browser.fill,397.547,118,56 +devframe-validation-validation,browser.click,305.95,97,57 +devframe-validation-validation,browser.snapshot,613.472,18,456 +devframe-validation-validation,browser.batch,1341.585,261,605 +devframe-validation-validation,browser.fill,278.219,118,56 +devframe-validation-validation,browser.click,275.59,97,57 +devframe-validation-validation,browser.snapshot,612.154,18,456 +devframe-validation-validation,browser.batch,1224.37,261,605 +devframe-validation-validation,browser.fill,383.537,118,56 +devframe-validation-validation,browser.click,330.406,97,57 +devframe-validation-validation,browser.snapshot,609.647,18,456 +devframe-validation-validation,browser.batch,1165.287,261,605 +devframe-validation-validation,browser.fill,429.27,118,56 +devframe-validation-validation,browser.click,363.652,97,57 +devframe-validation-validation,browser.snapshot,633.367,18,456 +devframe-validation-validation,browser.batch,1259.317,261,605 +devframe-validation-validation,browser.fill,338.517,118,56 +devframe-validation-validation,browser.click,288.884,97,57 +devframe-validation-validation,browser.snapshot,667.674,18,457 +devframe-validation-validation,browser.batch,1207.499,261,610 +devframe-validation-validation,browser.fill,371.855,118,56 +devframe-validation-validation,browser.click,331.782,97,57 +devframe-validation-validation,browser.snapshot,752.394,18,461 +devframe-validation-validation,browser.batch,1076.647,261,610 +devframe-validation-validation,browser.fill,426.872,118,56 +devframe-validation-validation,browser.click,411.911,97,57 +devframe-validation-validation,browser.snapshot,738.02,18,461 +devframe-validation-validation,browser.batch,1041.727,261,610 +devframe-validation-validation,browser.fill,370.228,118,56 +devframe-validation-validation,browser.click,361.279,97,57 +devframe-validation-validation,browser.snapshot,563.757,18,461 +devframe-validation-validation,browser.batch,1283.173,261,610 +devframe-validation-validation,browser.fill,319.799,118,56 +devframe-validation-validation,browser.click,378.422,97,57 +devframe-validation-validation,browser.snapshot,666.585,18,461 +devframe-validation-validation,browser.batch,1152.538,261,610 +devframe-validation-validation,browser.fill,308.421,118,56 +devframe-validation-validation,browser.click,332.601,97,57 +devframe-validation-validation,browser.snapshot,592.524,18,461 +devframe-validation-validation,browser.batch,1288.587,261,610 +devframe-validation-validation,browser.fill,401.692,118,56 +devframe-validation-validation,browser.click,422.479,97,57 +devframe-validation-validation,browser.snapshot,613.033,18,461 +devframe-validation-validation,browser.batch,1320.636,261,610 +devframe-validation-validation,browser.fill,369.6,118,56 +devframe-validation-validation,browser.click,347.167,97,57 +devframe-validation-validation,browser.snapshot,583.995,18,461 +devframe-validation-validation,browser.batch,1166.723,261,610 +devframe-validation-validation,browser.fill,361.417,118,56 +devframe-validation-validation,browser.click,406.104,97,57 +devframe-validation-validation,browser.snapshot,614.672,18,461 +devframe-validation-validation,browser.batch,1284.271,261,610 +devframe-validation-validation,browser.fill,333.812,118,56 +devframe-validation-validation,browser.click,326.341,97,57 +devframe-validation-validation,browser.snapshot,673.167,18,461 +devframe-validation-validation,browser.batch,1239.59,261,610 +devframe-validation-validation,browser.fill,351.025,118,56 +devframe-validation-validation,browser.click,342.679,97,57 +devframe-validation-validation,browser.snapshot,590.104,18,461 +devframe-validation-validation,browser.batch,1642.983,261,610 +devframe-validation-validation,browser.fill,323.922,118,56 +devframe-validation-validation,browser.click,298.95,97,57 +devframe-validation-validation,browser.snapshot,583.05,18,461 +devframe-validation-validation,browser.batch,1359.275,261,610 +devframe-validation-validation,browser.fill,355.355,118,56 +devframe-validation-validation,browser.click,297.134,97,57 +devframe-validation-validation,browser.snapshot,628.134,18,461 +devframe-validation-validation,browser.batch,1149.58,261,610 +devframe-validation-validation,browser.fill,372.307,118,56 +devframe-validation-validation,browser.click,356.803,97,57 +devframe-validation-validation,browser.snapshot,588.413,18,461 +devframe-validation-validation,browser.batch,1121.753,261,610 +devframe-validation-validation,browser.fill,347.528,118,56 +devframe-validation-validation,browser.click,275.648,97,57 +devframe-validation-validation,browser.snapshot,581.245,18,461 +devframe-validation-validation,browser.batch,1269.639,261,610 +devframe-validation-validation,browser.fill,335.269,118,56 +devframe-validation-validation,browser.click,409.154,97,57 +devframe-validation-validation,browser.snapshot,658.343,18,461 +devframe-validation-validation,browser.batch,1427.485,261,610 +devframe-validation-validation,browser.fill,298.9,118,56 +devframe-validation-validation,browser.click,306.002,97,57 +devframe-validation-validation,browser.snapshot,664.216,18,461 +devframe-validation-validation,browser.batch,1245.854,261,610 +devframe-validation-validation,browser.fill,374.639,118,56 +devframe-validation-validation,browser.click,329.531,97,57 +devframe-validation-validation,browser.snapshot,822.374,18,461 +devframe-validation-validation,browser.batch,1109.942,261,610 +devframe-validation-validation,browser.fill,335.336,118,56 +devframe-validation-validation,browser.click,425.028,97,57 +devframe-validation-validation,browser.snapshot,597.196,18,461 +devframe-validation-validation,browser.batch,1226.926,261,610 +devframe-validation-validation,browser.fill,380.779,118,56 +devframe-validation-validation,browser.click,389.456,97,57 +devframe-validation-validation,browser.snapshot,603.882,18,461 +devframe-validation-validation,browser.batch,1306.936,261,610 +devframe-validation-validation,browser.fill,426.236,118,56 +devframe-validation-validation,browser.click,337.037,97,57 +devframe-validation-validation,browser.snapshot,588.49,18,461 +devframe-validation-validation,browser.batch,1303.217,261,610 +devframe-validation-validation,browser.click:blocked,2002.87,97,197 +devframe-validation-validation,browser.click:blocked,2001.755,97,197 +devframe-validation-validation,browser.click:blocked,2001.964,97,197 +devframe-validation-validation,browser.click:blocked,2002.237,97,197 +devframe-validation-validation,browser.click:blocked,2000.503,97,197 +devframe-validation-validation,browser.click:blocked,2002.435,97,197 +devframe-validation-validation,browser.click:blocked,2000.854,97,197 +devframe-validation-validation,browser.click:blocked,2000.979,97,197 +devframe-validation-validation,browser.click:blocked,2002.857,97,197 +devframe-validation-validation,browser.click:blocked,2001.402,97,197 +devframe-validation-validation,browser.click:blocked,2002.378,97,197 +devframe-validation-validation,browser.click:blocked,2003.021,97,197 +devframe-validation-validation,browser.click:blocked,2024.507,97,197 +devframe-validation-validation,browser.click:blocked,2000.284,97,197 +devframe-validation-validation,browser.click:blocked,2001.654,97,197 +devframe-validation-validation,browser.click:blocked,2002.467,97,197 +devframe-validation-validation,browser.click:blocked,2000.535,97,197 +devframe-validation-validation,browser.click:blocked,2002.296,97,197 +devframe-validation-validation,browser.click:blocked,2013.454,97,197 +devframe-validation-validation,browser.click:blocked,2001.919,97,197 +devframe-validation-validation,browser.click:blocked,2030.31,97,197 +devframe-validation-validation,browser.click:blocked,2003.089,97,197 +devframe-validation-validation,browser.click:blocked,2001.517,97,197 +devframe-validation-validation,browser.click:blocked,2056.295,97,197 +devframe-validation-validation,browser.click:blocked,2000.825,97,197 +devframe-validation-validation,browser.click:blocked,2001.54,97,197 +devframe-validation-validation,browser.click:blocked,2001.676,97,197 +devframe-validation-validation,browser.click:blocked,2001.146,97,197 +devframe-validation-validation,browser.click:blocked,2059.054,97,197 +devframe-validation-validation,browser.click:blocked,2001.048,97,197 +devframe-validation-final,browser.fill,334.525,118,56 +devframe-validation-final,browser.click,319.495,97,57 +devframe-validation-final,browser.snapshot,587.946,18,452 +devframe-validation-final,browser.batch,1076.829,261,605 +devframe-validation-final,browser.fill,286.73,118,56 +devframe-validation-final,browser.click,288.48,97,57 +devframe-validation-final,browser.snapshot,592.177,18,456 +devframe-validation-final,browser.batch,1079.096,261,605 +devframe-validation-final,browser.fill,273.888,118,56 +devframe-validation-final,browser.click,312.221,97,57 +devframe-validation-final,browser.snapshot,611.19,18,456 +devframe-validation-final,browser.batch,1099.432,261,605 +devframe-validation-final,browser.fill,297.266,118,56 +devframe-validation-final,browser.click,263.375,97,57 +devframe-validation-final,browser.snapshot,645.465,18,456 +devframe-validation-final,browser.batch,1196.065,261,605 +devframe-validation-final,browser.fill,333.928,118,56 +devframe-validation-final,browser.click,306.397,97,57 +devframe-validation-final,browser.snapshot,695.502,18,456 +devframe-validation-final,browser.batch,1358.076,261,605 +devframe-validation-final,browser.fill,362.02,118,56 +devframe-validation-final,browser.click,347.609,97,57 +devframe-validation-final,browser.snapshot,624.534,18,456 +devframe-validation-final,browser.batch,1092.474,261,605 +devframe-validation-final,browser.fill,311.23,118,56 +devframe-validation-final,browser.click,274.077,97,57 +devframe-validation-final,browser.snapshot,590.014,18,456 +devframe-validation-final,browser.batch,1100.753,261,605 +devframe-validation-final,browser.fill,284.211,118,56 +devframe-validation-final,browser.click,305.432,97,57 +devframe-validation-final,browser.snapshot,633.038,18,456 +devframe-validation-final,browser.batch,1169.554,261,605 +devframe-validation-final,browser.fill,443.609,118,56 +devframe-validation-final,browser.click,401.158,97,57 +devframe-validation-final,browser.snapshot,851.832,18,456 +devframe-validation-final,browser.batch,1134.599,261,605 +devframe-validation-final,browser.fill,306.488,118,56 +devframe-validation-final,browser.click,279.557,97,57 +devframe-validation-final,browser.snapshot,711.726,18,457 +devframe-validation-final,browser.batch,1240.372,261,610 +devframe-validation-final,browser.fill,370.35,118,56 +devframe-validation-final,browser.click,291.191,97,57 +devframe-validation-final,browser.snapshot,655.373,18,461 +devframe-validation-final,browser.batch,1242.185,261,610 +devframe-validation-final,browser.fill,287.914,118,56 +devframe-validation-final,browser.click,325.876,97,57 +devframe-validation-final,browser.snapshot,623.183,18,461 +devframe-validation-final,browser.batch,1184.104,261,610 +devframe-validation-final,browser.fill,323.203,118,56 +devframe-validation-final,browser.click,341.68,97,57 +devframe-validation-final,browser.snapshot,642.44,18,461 +devframe-validation-final,browser.batch,1077.787,261,610 +devframe-validation-final,browser.fill,313.542,118,56 +devframe-validation-final,browser.click,268.053,97,57 +devframe-validation-final,browser.snapshot,606.167,18,461 +devframe-validation-final,browser.batch,1199.638,261,610 +devframe-validation-final,browser.fill,270.38,118,56 +devframe-validation-final,browser.click,289.297,97,57 +devframe-validation-final,browser.snapshot,627.305,18,461 +devframe-validation-final,browser.batch,1159.873,261,610 +devframe-validation-final,browser.fill,327.307,118,56 +devframe-validation-final,browser.click,283.73,97,57 +devframe-validation-final,browser.snapshot,672.138,18,461 +devframe-validation-final,browser.batch,1088.664,261,610 +devframe-validation-final,browser.fill,317.571,118,56 +devframe-validation-final,browser.click,279.713,97,57 +devframe-validation-final,browser.snapshot,635.194,18,461 +devframe-validation-final,browser.batch,1116.706,261,610 +devframe-validation-final,browser.fill,307.252,118,56 +devframe-validation-final,browser.click,345.578,97,57 +devframe-validation-final,browser.snapshot,625.879,18,461 +devframe-validation-final,browser.batch,1116.924,261,610 +devframe-validation-final,browser.fill,271.904,118,56 +devframe-validation-final,browser.click,310.017,97,57 +devframe-validation-final,browser.snapshot,553.541,18,461 +devframe-validation-final,browser.batch,1072.454,261,610 +devframe-validation-final,browser.fill,288.724,118,56 +devframe-validation-final,browser.click,299.014,97,57 +devframe-validation-final,browser.snapshot,583.66,18,461 +devframe-validation-final,browser.batch,1118.419,261,610 +devframe-validation-final,browser.fill,308.042,118,56 +devframe-validation-final,browser.click,296.496,97,57 +devframe-validation-final,browser.snapshot,606.585,18,461 +devframe-validation-final,browser.batch,1021.443,261,610 +devframe-validation-final,browser.fill,300.439,118,56 +devframe-validation-final,browser.click,273.459,97,57 +devframe-validation-final,browser.snapshot,655.412,18,461 +devframe-validation-final,browser.batch,1094.311,261,610 +devframe-validation-final,browser.fill,282.885,118,56 +devframe-validation-final,browser.click,324.183,97,57 +devframe-validation-final,browser.snapshot,994.853,18,461 +devframe-validation-final,browser.batch,2307.807,261,610 +devframe-validation-final,browser.fill,389.936,118,56 +devframe-validation-final,browser.click,537.056,97,57 +devframe-validation-final,browser.snapshot,1387.117,18,461 +devframe-validation-final,browser.batch,1814.76,261,610 +devframe-validation-final,browser.fill,790.579,118,56 +devframe-validation-final,browser.click,263.882,97,57 +devframe-validation-final,browser.snapshot,627.45,18,461 +devframe-validation-final,browser.batch,1196.755,261,610 +devframe-validation-final,browser.fill,302.862,118,56 +devframe-validation-final,browser.click,299.992,97,57 +devframe-validation-final,browser.snapshot,770.81,18,461 +devframe-validation-final,browser.batch,1209.729,261,610 +devframe-validation-final,browser.fill,313.774,118,56 +devframe-validation-final,browser.click,242.085,97,57 +devframe-validation-final,browser.snapshot,586.381,18,461 +devframe-validation-final,browser.batch,1101.918,261,610 +devframe-validation-final,browser.fill,318.166,118,56 +devframe-validation-final,browser.click,329.305,97,57 +devframe-validation-final,browser.snapshot,587.955,18,461 +devframe-validation-final,browser.batch,1055.779,261,610 +devframe-validation-final,browser.fill,281.699,118,56 +devframe-validation-final,browser.click,279.296,97,57 +devframe-validation-final,browser.snapshot,567.7,18,461 +devframe-validation-final,browser.batch,1056.206,261,610 +devframe-validation-final,browser.fill,326.245,118,56 +devframe-validation-final,browser.click,300.213,97,57 +devframe-validation-final,browser.snapshot,619.546,18,461 +devframe-validation-final,browser.batch,1288.024,261,610 +devframe-validation-final,browser.click:blocked,2001.495,97,197 +devframe-validation-final,browser.click:blocked,2002.566,97,197 +devframe-validation-final,browser.click:blocked,2002.679,97,197 +devframe-validation-final,browser.click:blocked,2001.582,97,197 +devframe-validation-final,browser.click:blocked,2002.806,97,197 +devframe-validation-final,browser.click:blocked,2001.927,97,197 +devframe-validation-final,browser.click:blocked,2002.821,97,197 +devframe-validation-final,browser.click:blocked,2002.086,97,197 +devframe-validation-final,browser.click:blocked,2001.052,97,197 +devframe-validation-final,browser.click:blocked,2003.153,97,197 +devframe-validation-final,browser.click:blocked,2039.416,97,197 +devframe-validation-final,browser.click:blocked,2000.925,97,197 +devframe-validation-final,browser.click:blocked,2002.806,97,197 +devframe-validation-final,browser.click:blocked,2001.481,97,197 +devframe-validation-final,browser.click:blocked,2000.973,97,197 +devframe-validation-final,browser.click:blocked,2001.387,97,197 +devframe-validation-final,browser.click:blocked,2002.231,97,197 +devframe-validation-final,browser.click:blocked,2002.097,97,197 +devframe-validation-final,browser.click:blocked,2001.894,97,197 +devframe-validation-final,browser.click:blocked,2002.91,97,197 +devframe-validation-final,browser.click:blocked,2001.65,97,197 +devframe-validation-final,browser.click:blocked,2001.797,97,197 +devframe-validation-final,browser.click:blocked,2000.906,97,197 +devframe-validation-final,browser.click:blocked,2002.241,97,197 +devframe-validation-final,browser.click:blocked,2010.802,97,197 +devframe-validation-final,browser.click:blocked,2002.194,97,197 +devframe-validation-final,browser.click:blocked,2002.11,97,197 +devframe-validation-final,browser.click:blocked,2001.157,97,197 +devframe-validation-final,browser.click:blocked,2000.562,97,197 +devframe-validation-final,browser.click:blocked,2001.68,97,197 diff --git a/docs/benchmarks/2026-09-08-latency.md b/docs/benchmarks/2026-09-08-latency.md new file mode 100644 index 0000000..80eaba1 --- /dev/null +++ b/docs/benchmarks/2026-09-08-latency.md @@ -0,0 +1,57 @@ +# Native action deadlines and discovery measurements + +Recorded on 8 September 2026 with Chromium 151.0.7922.34, Node 24.20.0, macOS arm64, Apple M3 Pro (11 logical processors). The normal public fixture contains 10,000 DOM nodes, 200 levels, 20 closed shadow roots and three cross-origin frame levels. Each reported scenario has 30 warm samples. Browser startup, fixture loading and model time are excluded; CDB processing and readiness checks are included. [Raw samples](./2026-09-08-latency.csv) retain the failed performance run as well as successful runs. + +## Devframe RPC workflow + +Warmup: one fill, one click, then one fill/click batch with observation. Each measured iteration uses exact role/name locators for fill and click, an individual compact snapshot, then the same fill/click batch with `observe: true`. Persistent-overlay samples are collected separately afterward. + +| Scenario | Before p95 | Final p95 | Target | +| --- | ---: | ---: | ---: | +| Fill | 793 ms | 433 ms | 500 ms | +| Click | 813 ms | 445 ms | 500 ms | +| Compact snapshot | 930 ms | 935 ms | 1,000 ms | +| Fill/click batch + observation | 2,370 ms | 1,896 ms | — | +| Individual fill + click + snapshot | — | 2,125 ms | — | +| Covered click | Not sampled | 2,131 ms | 2,000 ms input deadline | + +All 30 covered clicks returned the covered-element error without input. The deadline bounds CDB execution; transport delivery and event-loop scheduling add observable overhead. Shorter blocked waits are not counted as faster successful actions. + +A separate five-sample phase probe before optimization averaged 611 ms discovery, 80 ms actionability, 10 ms input and 3 ms verification. Final successful actions averaged 247, 92, 16 and 2 ms respectively. Discovery now uses filtered accessibility queries at session roots and full trees for in-process child frames. After failed readiness it refreshes the full AX view before renewing the locator. No DOM state is cached across actions. Ancestor-frame and fresh geometry/hit checks remain enabled. + +Obtaining query roots through `Accessibility.getRootAXNode` avoids `DOM.getDocument` initialization and its unnecessary DOM event burst. The existing first-use batch and scrolling-replacement Chromium regressions exercise these cases. Final fill/click diagnostics average 47/43 CDP commands, 13,521/12,451 serialized request characters, and 21,341/18,312 response-envelope characters. Full diagnostic traces remain opt-in through `cdb.mcp.action`. + +## WebSocket reference workflow + +This harness uses fresh element references rather than role/name locators. Its results cannot isolate a transport regression against the Devframe harness. Warmup and sample counts are unchanged between runs. + +| Run | Action p95 | Snapshot p95 | Action errors | Gate | +| --- | ---: | ---: | ---: | --- | +| Before | 122 ms | 730 ms | 0 | Pass | +| Intermediate | 107 ms | 657 ms | 0 | Pass | +| Final | 292 ms | 1,877 ms | 0 | **Snapshot gate failed** | + +The final WebSocket snapshot run exceeds the one-second target. The variation is unresolved; the earlier passing run does not establish acceptance for the final run. The native behavioral suite passes all 18 scenarios, and the separate Devframe run passes its action and snapshot targets. + +## Follow-up validation + +The same revision and normal fixture were rerun sequentially later on 8 September, with 30 warm samples per scenario and no concurrent test suites or builds. The first rerun reproduced the snapshot failure. Subsequent runs added timing at the actual client command, artifact HTTP read, and Chrome debugger boundaries without changing production behavior. All samples, including the failed rerun, are retained in the CSV. + +| Run | Action p95 | Snapshot p95 | Gate | +| --- | ---: | ---: | --- | +| WebSocket reproduction | 181 ms | 1,183 ms | Snapshot failed | +| WebSocket command/artifact probe | 124 ms | 791 ms | Pass | +| WebSocket confirmation | 111 ms | 702 ms | Pass | +| WebSocket final, including Chrome timing | 111 ms | 712 ms | Pass | +| Devframe confirmation, fill/click | 427/422 ms | 752 ms | Pass | +| Devframe final explicit gate, fill/click | 444/401 ms | 995 ms | Pass | + +OS load averages during the failing reproduction rose from approximately 15.2/9.2/10.5 to 18.8/11.1/11.2. Process inspection showed GitKraken and filesystem activity alongside Chromium. The final WebSocket run recorded 5.8/7.8/9.6 at the start and 5.7/7.6/9.4 at the end. Devframe's final run recorded 5.8/7.4/9.3 and 6.5/7.4/9.0. This association supports host contention as a contributor, but does not isolate it as the sole cause of the earlier variation. + +Final WebSocket snapshots averaged 642 ms and transferred about 5.6 MB of accessibility artifacts. Each of four AX command round trips averaged 145 ms; direct Chrome debugger execution averaged 67 ms, including the warmup. Artifact HTTP reads totaled 11 ms per snapshot. The command-time difference includes provider/broker serialization, transport and processing; it is not a network-only measurement. These measurements do not justify changing artifact delivery or weakening snapshot completeness. + +The final gates pass in this recorded environment, with no production optimization applied during follow-up. Devframe's 995 ms snapshot p95 is close to the budget and should not be described as having substantial performance margin. Its final batch p95 was 1,815 ms; covered clicks returned the readiness error without input at 2,011 ms p95. The benchmark now exposes an explicit Devframe performance gate and both harnesses record machine load for future comparisons. + +## Agent payload costs + +The final Devframe catalogue is 75,906 serialized characters. Individual fill/click/snapshot arguments and responses total approximately 805 characters per workflow; the batch with observation totals 869. At characters divided by four, these are approximately 19,000 catalogue tokens, 201 individual-workflow tokens and 217 batch-workflow tokens. These are estimates, not measurements from a model tokenizer. The batch trades about 16 estimated payload tokens for one agent round trip instead of three and a shared lease. Observation remains opt-in. diff --git a/docs/native-mvp.md b/docs/native-mvp.md index 01d9368..97da4e9 100644 --- a/docs/native-mvp.md +++ b/docs/native-mvp.md @@ -16,6 +16,14 @@ Debug evaluation, raw leases, artifact tools, and network/console diagnostics re Default screenshots return MCP image content and release temporary artifacts internally. Debug sessions retain the inline-or-artifact result for explicit inspection. +## Element action deadlines + +Element actions default to a two-second total deadline. Hosts can change `timing.defaultInputActionTimeoutMilliseconds`; callers can supply `timeoutMilliseconds` for an action that intentionally waits longer. Discovery, locator renewal, lease acquisition, readiness, dispatch and verification consume the same budget. Navigation, snapshots, explicit waits and approval retain their existing deadlines. + +Before input, timeout preserves the last readiness error when available. Once input may have been dispatched, timeout or cancellation returns `MCP_ACTION_OUTCOME_UNKNOWN`. Observe the control before deciding on another action; CDB never automatically replays uncertain input. A lease arriving after cancellation is released without issuing commands. + +For local diagnosis, subscribe to Node's `cdb.mcp.action` diagnostics channel. It reports wall-clock phase durations, CDP method durations and counts, serialized command/response sizes and readiness retry counts. It includes no DOM content or command arguments and adds nothing to normal agent responses. Artifact command sizes describe the envelope; benchmark response sizes include the returned observation. Measurements are emitted only to subscribers. + ## Batches `browser.batch` runs ordered element actions on one target under one exclusive lease. The default total deadline is 30 seconds, with at most 20 actions. The first catalogue contains click, fill, type, press, check, uncheck, focus, hover and scroll-into-view. Navigation commands are excluded. @@ -31,7 +39,9 @@ Default screenshots return MCP image content and release temporary artifacts int } ``` -Each step calls the same implementation as its individual tool. Success returns `completed` entries with zero-based indices and action names. `observe` adds a final compact snapshot. Failures use the standard `code`, `message`, `retryable` and `details` error envelope. `details.completed` records successful steps, `details.failedStep` identifies the failed step, and `details.uncertain:true` means input may have occurred. A final observation failure uses `details.phase: "observation"`. Cancellation, target authority replacement, or document replacement stops subsequent steps. Completed actions are not rolled back or replayed. If final observation fails, completed actions remain recorded. +Each step calls the same implementation as its individual tool and receives the lesser of the remaining batch time and its action budget. `actionTimeoutMilliseconds` changes the per-step budget, which defaults to two seconds; the overall batch remains capped at 30 seconds. Success returns `completed` entries with zero-based indices and action names. `observe` adds a final compact snapshot. Failures use the standard `code`, `message`, `retryable` and `details` error envelope. `details.completed` records successful steps, `details.failedStep` identifies the failed step, and `details.uncertain:true` means input may have occurred. A final observation failure uses `details.phase: "observation"`. Cancellation, target authority replacement, or document replacement stops subsequent steps. Completed actions are not rolled back or replayed. If final observation fails, completed actions remain recorded. + +For a known sequence, send one batch with `observe: true` instead of separate action calls followed by a snapshot. This reduces agent round trips and shares one lease. Observation remains opt-in; use individual tools when the next action depends on inspecting the preceding result. ## Browser behavior and limits diff --git a/docs/releasing.md b/docs/releasing.md index c6fe766..68eb204 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -1,6 +1,6 @@ # Releasing CDB packages -CDB publishes its six public packages on one synchronized version. A manually dispatched GitHub workflow creates the version commit and tag, publishes the immutable tagged commit, and then creates one GitHub Release. +CDB publishes its eight public packages on one synchronized version. A manually dispatched GitHub workflow creates the version commit and tag, publishes the immutable tagged commit, and then creates one GitHub Release. ## Responsibilities diff --git a/docs/research/devframe-toast-removal.md b/docs/research/devframe-toast-removal.md new file mode 100644 index 0000000..d283f3b --- /dev/null +++ b/docs/research/devframe-toast-removal.md @@ -0,0 +1,40 @@ +# Devframe 0.9.10 toast removal + +Status: reproduced during browser validation on 8 September 2026. This is an unresolved presentation limitation of the pinned host UI. + +## Public reproduction + +Mount a page script in a Devframe 0.9.10 host using its embedded hub UI. The following synthetic script needs no browser provider, grant, application data, or CDB authority: + +```ts +interface NotificationContext { + messages: { + info: ( + message: string, + options: { id: string; notify: boolean; autoDismiss: false }, + ) => Promise<{ dismiss: () => Promise }>; + }; +} + +export default async function reproduceToastRemoval(context: NotificationContext): Promise { + const notification = await context.messages.info('Synthetic pending request', { + id: 'synthetic-request-removal', + notify: true, + autoDismiss: false, + }); + await new Promise(resolve => setTimeout(resolve, 3_000)); + await notification.dismiss(); +} +``` + +Expected: the notification disappears from both the message list and the visible toast list after three seconds. + +Observed: the message entry disappears, while its toast remains visible. Repeating request/approval/revocation sequences leaves obsolete request and active-control toasts. + +## Boundary and regression coverage + +Inspection of the installed `@devframes/hub-ui` 0.9.10 embedded bundle shows that `src/client/state/messages.ts` deletes `removedIds` from the message map without removing their entries from `src/client/state/toasts.ts`. The page script uses the supported handle's `dismiss()` method, which removes the host message over RPC. + +CDB's `packages/devframe/test/page-script.test.ts` verifies that ended requests unregister their commands and dismiss their host messages. That test establishes CDB cleanup, not the host's visible toast behavior. The reproduction above remains a manual browser regression until the host UI is fixed; it is not a passing automated acceptance test. + +Broker grants, pending requests, and the mounted CDB panel update correctly. Obsolete toasts must not be treated as current authority. Keep the existing dependency pins; resolve this through the host UI separately, without patching installed dependencies or introducing a second notification renderer in CDB. diff --git a/eslint.config.ts b/eslint.config.ts index 6f394ca..7267dd6 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -40,6 +40,7 @@ export default defineTypescriptConfig( 'packages/*/test/**/*.ts', 'packages/*/tsdown.config.ts', 'packages/*/vitest.config.ts', + 'packages/*/vite.config.ts', 'tests/browser/**/*.ts', 'tests/e2e/**/*.ts', 'tests/fixtures/**/*.ts', diff --git a/examples/devframe/README.md b/examples/devframe/README.md index 35b5db8..0559a4f 100644 --- a/examples/devframe/README.md +++ b/examples/devframe/README.md @@ -1,7 +1,20 @@ -# Experimental Devframe MCP architecture +# Devframe browser-control example -This private example uses upstream Devframe, its Vite integration, and its route-based MCP server as an alternative MCP architecture. It composes directly with `@dvcol/cdb` broker/client primitives; it does not import `@dvcol/cdb-mcp` or `@dvcol/cdb-birpc`. +This example runs a CDB broker as a Devframe service, serves the public management panel, and exposes native browser tools through an MCP stdio server owned by the example. The extension sends provider messages over its existing authenticated Devframe RPC peer. Final approval comes from the extension popup. -The only agent-enabled capability is a read-only target count. It deliberately omits pairing credentials, raw target identifiers, and raw CDP access. Its smoke flow starts Vite, discovers Devframe's side-car MCP endpoint from `__connection.json`, establishes an MCP session with a loopback Origin, calls the safe summary tool, and closes the Vite and broker lifecycles. +The supported baseline is Devframe 0.9.10, DevTools Kit 0.6.1 and Vite 8.1.5. Upgrading DevTools/Vite is separate from this integration. -Run `pnpm --filter @chrome-debugger-bridge-example/devframe smoke`. This is experimental because Devframe's agent-native MCP surface is upstream experimental. +1. Build the affected packages: `pnpm exec turbo run build --filter @dvcol/cdb-devframe...`. +2. Build the extension: `pnpm --filter @chrome-debugger-bridge-example/devframe build:extension`. +3. Load `examples/devframe/dist/extension` unpacked in Chrome and copy its extension ID. +4. Configure an MCP stdio command running `node examples/devframe/host.ts` from the repository. Set `CDB_EXTENSION_ORIGIN=chrome-extension://`. The host uses port 58920 by default; `CDB_EXAMPLE_PORT` changes the example's one Devframe port. +5. Open the management panel at the host URL. Connect the extension popup to the same host and authenticate with the Devframe code printed in the host's stderr log for that connection. Keep the inspected application tab active when approving. +6. Ask the agent for `browser.request_access`, review its requested level/navigation policy in the extension popup, and approve the current tab, group or window. Run `browser.snapshot` and `browser.batch` from that same agent session. + +The example's identity file lives under `examples/devframe/dist/identity`. Preserve it and the extension's storage to retain pairing across restarts. The standalone panel requests review; the popup performs the final approval and validates the selected Chrome scope. + +`pnpm --filter @chrome-debugger-bridge-example/devframe smoke` checks the served panel, page-script assets and native catalogue without a browser. The focused `tests/e2e/devframe-native.test.ts` exercises real Chromium, closed shadow roots, nested cross-origin frames, overlapping grants, live group membership and provider recovery through Devframe RPC and native MCP. + +The example owns its single stdio principal and process lifecycle. An application with a registry should keep its own catalogue aggregation, authenticated principal mapping and registry-only WebMCP exposure. + +`devtools.ts` demonstrates mounting the same panel through DevTools Kit's existing `createPluginFromDevframe` adapter. Pass the host's existing broker client and call the returned `dispose` hook during application shutdown. diff --git a/examples/devframe/devframe.ts b/examples/devframe/devframe.ts index 51c779d..3c0380e 100644 --- a/examples/devframe/devframe.ts +++ b/examples/devframe/devframe.ts @@ -1,29 +1,36 @@ -import type { EmbeddedChromeDebuggerBridge } from '@dvcol/cdb'; +import type { BrokerDefinition } from '@dvcol/cdb-broker'; +import type { CdbDevframeService } from '@dvcol/cdb-devframe'; +import type { DevframeDefinition } from 'devframe'; -import { defineDevframe } from 'devframe/types'; +import { createCdbPanel, createCdbService, getCdbService } from '@dvcol/cdb-devframe'; -/** Creates an experimental Devframe surface over safe bridge target summaries only. */ -export function createDevframeDefinition(bridge: EmbeddedChromeDebuggerBridge): ReturnType { - return defineDevframe({ - description: 'Experimental Devframe target-summary integration for CDB.', - homepage: 'https://github.com/dvcol/chrome-debugger-bridge', - icon: 'ph:bug-duotone', - id: 'cdb-devframe', - name: 'CDB Devframe', - packageName: '@chrome-debugger-bridge-example/devframe', - setup(context) { - context.agent.registerTool({ - description: 'Summarize the number of currently available Chrome Debugger Bridge targets without exposing target identifiers.', - handler: async () => { - const targets = await bridge.client.listTargets(); - return { - markdown: `Available targets: ${targets.filter(target => target.availability === 'available').length}.`, - }; - }, - id: 'cdb-devframe:target-summary', - safety: 'read', - }); +/** The example owns service lifecycle; mounting its panel does not create another broker. */ +export function createDevframeExample(broker: BrokerDefinition = {}): { readonly definition: DevframeDefinition; readonly service: CdbDevframeService; dispose: () => Promise } { + let service: CdbDevframeService | undefined; + const panel = createCdbPanel({ client() { + if (service === undefined) throw new Error('The example broker is not ready.'); + const runtime = service.broker; + return { ...runtime, watch(listener) { + listener(runtime.snapshot()); + return runtime.subscribe(listener); + } }; + } }); + return { + definition: { + ...panel.definition, + services: [createCdbService({ broker })], + async setup(context) { + service = getCdbService(context); + await panel.definition.setup(context); + }, }, - version: '0.0.0', - }); + get service(): CdbDevframeService { + if (service === undefined) throw new Error('The example broker is not ready.'); + return service; + }, + async dispose() { + panel.dispose(); + await service?.dispose(); + }, + }; } diff --git a/examples/devframe/devtools.ts b/examples/devframe/devtools.ts new file mode 100644 index 0000000..61cda5a --- /dev/null +++ b/examples/devframe/devtools.ts @@ -0,0 +1,13 @@ +import type { BrowserControlPanelClient } from '@dvcol/cdb-devframe/panel'; + +import { createCdbPanel } from '@dvcol/cdb-devframe'; +import { createPluginFromDevframe } from '@vitejs/devtools-kit/node'; + +/** The embedding application supplies its existing broker client and owns plugin teardown. */ +export function createBrowserControlDevtoolsPlugin(client: BrowserControlPanelClient): { + readonly plugin: ReturnType; + dispose: () => void; +} { + const panel = createCdbPanel({ client: () => client }); + return { plugin: createPluginFromDevframe(panel.definition), dispose: panel.dispose }; +} diff --git a/examples/devframe/extension/build.ts b/examples/devframe/extension/build.ts new file mode 100644 index 0000000..79d3fc8 --- /dev/null +++ b/examples/devframe/extension/build.ts @@ -0,0 +1,16 @@ +import { copyFile, mkdir, writeFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { build } from 'vite'; + +async function main(): Promise { + const outDir = join(import.meta.dirname, '../dist/extension'); + for (const entry of ['worker', 'popup']) { + await build({ configFile: false, logLevel: 'silent', build: { outDir, emptyOutDir: entry === 'worker', lib: { entry: join(import.meta.dirname, `${entry}.ts`), formats: ['es'], fileName: () => `${entry}.js` } } }); + } + await mkdir(outDir, { recursive: true }); + await copyFile(join(import.meta.dirname, 'popup.html'), join(outDir, 'popup.html')); + await writeFile(join(outDir, 'manifest.json'), JSON.stringify({ manifest_version: 3, name: 'CDB Devframe example', version: '0.1.0', action: { default_popup: 'popup.html' }, background: { service_worker: 'worker.js', type: 'module' }, permissions: ['debugger', 'tabs', 'tabGroups', 'webNavigation', 'storage', 'alarms'], host_permissions: ['http://127.0.0.1/*'] } satisfies chrome.runtime.ManifestV3, null, 2)); +} + +void main(); diff --git a/examples/devframe/extension/popup.html b/examples/devframe/extension/popup.html new file mode 100644 index 0000000..c64a0eb --- /dev/null +++ b/examples/devframe/extension/popup.html @@ -0,0 +1,44 @@ + + + + + Browser access + + + +

Browser access

+ +

+ +
+ + + diff --git a/examples/devframe/extension/popup.ts b/examples/devframe/extension/popup.ts new file mode 100644 index 0000000..51f20ee --- /dev/null +++ b/examples/devframe/extension/popup.ts @@ -0,0 +1,60 @@ +import type { BrokerState } from '@dvcol/cdb-broker/contract'; +import type { TabScopeSelector } from '@dvcol/cdb-extension'; + +const status = document.querySelector('#status')!; +const requests = document.querySelector('#requests')!; +let busy = false; +async function send(message: object): Promise { + const result = await chrome.runtime.sendMessage(message); + if (!result.ok) throw new Error(result.error); + return result.value; +} +async function perform(action: () => Promise): Promise { + busy = true; + try { + await action(); + status.textContent = 'Done.'; + } catch (error) { + status.textContent = error instanceof Error ? error.message : String(error); + } finally { + busy = false; + } +} +document.querySelector('#connect')!.onclick = () => { + void perform(async () => send({ kind: 'connect', baseURL: document.querySelector('#host')!.value, code: document.querySelector('#code')!.value })); +}; +async function refresh(): Promise { + if (busy) return; + const result = await send<{ state?: BrokerState; failure?: string }>({ kind: 'state' }); + if (result.failure !== undefined) status.textContent = result.failure; + requests.replaceChildren(); + for (const request of result.state?.requests ?? []) { + const section = document.createElement('section'); + const description = document.createElement('p'); + description.textContent = `${request.principalLabel} requests ${request.level} access (${request.navigation}).`; + const approve = document.createElement('button'); + approve.textContent = `Allow ${request.level}`; + approve.onclick = () => void perform(async () => { + const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); + if (tab?.id === undefined) throw new Error('Select a browser tab first.'); + const scope = document.querySelector('#scope')!.value; + const selector: TabScopeSelector = scope === 'group' ? { kind: 'group', groupId: tab.groupId } : scope === 'window' ? { kind: 'window', windowId: tab.windowId } : { kind: 'explicit-tabs', tabIds: [tab.id] }; + await send({ kind: 'approve', requestId: request.id, selector }); + }); + section.append(description, approve); + requests.append(section); + } + for (const scope of result.state?.scopes ?? []) { + const revoke = document.createElement('button'); + revoke.textContent = `Stop ${scope.principalLabel}`; + revoke.onclick = () => void perform(async () => send({ kind: 'revoke', requestId: scope.id })); + requests.append(revoke); + } +} +void refresh().catch((error) => { + status.textContent = String(error); +}); +const interval = setInterval(() => void refresh().catch((error) => { + status.textContent = String(error); +}), 1_000); +window.addEventListener('pagehide', () => clearInterval(interval), { once: true }); diff --git a/examples/devframe/extension/worker.ts b/examples/devframe/extension/worker.ts new file mode 100644 index 0000000..6373912 --- /dev/null +++ b/examples/devframe/extension/worker.ts @@ -0,0 +1,85 @@ +import type { BrokerState } from '@dvcol/cdb-broker/contract'; +import type { TabScopeSelector } from '@dvcol/cdb-extension'; + +import { createCdbClient } from '@dvcol/cdb-devframe/client'; +import { createIndexedDbPairingStore } from '@dvcol/cdb-extension'; +import { createChromeProvider, getChromeProviderIdentity } from '@dvcol/cdb-extension/chrome'; +import { connectDevframe } from 'devframe/client'; +import { DEVFRAME_WS_ROUTE } from 'devframe/constants'; + +let provider: ReturnType> | undefined; +let closeConnection: (() => Promise) | undefined; +let state: BrokerState | undefined; +let failure: string | undefined; +const popupUrl = chrome.runtime.getURL('popup.html'); +function trusted(sender: chrome.runtime.MessageSender): boolean { + return sender.id === chrome.runtime.id && sender.tab === undefined && sender.url === popupUrl; +} +async function connect(baseURL: string, code: string): Promise { + await provider?.dispose(); + await closeConnection?.(); + let connection: Awaited> | undefined; + let client: ReturnType | undefined; + let stopObserving: (() => void) | undefined; + const disposeConnection = async (): Promise => { + stopObserving?.(); + client?.disconnected(); + try { + await client?.dispose(); + } finally { + connection?.close?.(); + } + client = undefined; + connection = undefined; + }; + closeConnection = disposeConnection; + async function currentClient(): Promise> { + if (client !== undefined && connection?.status === 'connected') return client; + await disposeConnection(); + const candidate = await connectDevframe({ baseURL, simpleAuth: false, connectionMeta: { backend: 'websocket', websocket: { path: DEVFRAME_WS_ROUTE } } }); + try { + if (code.length > 0) await candidate.requestTrustWithCode(code); + await candidate.ensureTrusted(); + code = ''; + connection = candidate; + client = createCdbClient(candidate); + const current = client; + stopObserving = candidate.events.on('connection:status', (status) => { + if (status === 'disconnected') current.disconnected(); + }); + return current; + } catch (error) { + candidate.close?.(); + throw error; + } + } + await currentClient(); + const registration = { id: 'example-chrome', instanceId: await getChromeProviderIdentity('cdb-example-installation'), maximumLevel: 'debug' as const, name: 'Example Chrome extension', version: chrome.runtime.getManifest().version }; + provider = createChromeProvider({ + connect: async () => (await currentClient()).connectProvider({ registration, pairingKey: baseURL, pairingStore: createIndexedDbPairingStore(), confirmPairing: () => true }), + maximumLevel: registration.maximumLevel, + authorizeApproval: (_request, _selector, sender) => trusted(sender), + recoveryStorageKey: 'cdb-example-recovery', + recoveryAlarmName: 'cdb-example-heartbeat', + onState(value) { + state = value; + failure = undefined; + }, + onError(error) { + failure = error instanceof Error ? error.message : String(error); + }, + }); + provider.start(); +} +chrome.runtime.onMessage.addListener((message: { kind: string; baseURL?: string; code?: string; requestId?: string; selector?: TabScopeSelector }, sender, reply) => { + if (!trusted(sender)) return false; + void (async () => { + if (message.kind === 'state') return { state, failure }; + if (message.kind === 'connect' && message.baseURL !== undefined) return connect(message.baseURL, message.code ?? ''); + if (message.kind === 'approve' && message.requestId !== undefined && message.selector !== undefined && provider !== undefined) + return provider.approve(message.requestId, message.selector, sender); + if (message.kind === 'revoke' && message.requestId !== undefined && provider !== undefined) return provider.revoke(message.requestId); + throw new Error('Unsupported example request.'); + })().then(value => reply({ ok: true, value }), error => reply({ ok: false, error: error instanceof Error ? error.message : String(error) })); + return true; +}); diff --git a/examples/devframe/host.ts b/examples/devframe/host.ts new file mode 100644 index 0000000..57a8a1f --- /dev/null +++ b/examples/devframe/host.ts @@ -0,0 +1,71 @@ +import { join } from 'node:path'; +import { styleText } from 'node:util'; + +import { createFileBrokerIdentityStore, defineBroker } from '@dvcol/cdb-broker'; +import { Server } from '@modelcontextprotocol/server'; +import { StdioServerTransport } from '@modelcontextprotocol/server/stdio'; +import { createDevServer } from 'devframe/adapters/dev'; +import { getTempAuthCode } from 'devframe/node/auth'; + +import { createDevframeExample } from './devframe.ts'; + +async function main(): Promise { + const example = createDevframeExample(defineBroker({ + identityStore: await createFileBrokerIdentityStore(join(import.meta.dirname, 'dist', 'identity')), + navigation: { default: 'same-origin', allowed: ['same-origin', 'follow-tab'] }, + })); + const extensionOrigin = process.env.CDB_EXTENSION_ORIGIN; + const server = await createDevServer(example.definition, { + host: '127.0.0.1', + port: Number(process.env.CDB_EXAMPLE_PORT ?? 58920), + mcp: false, + openBrowser: false, + ...(extensionOrigin === undefined ? {} : { allowedOrigins: [extensionOrigin] }), + onPeerConnect: (connection, session) => { + example.service.onPeerConnect(connection, session); + console.error(styleText('yellow', '🔑 [cdb-example]'), 'Devframe authentication code:', getTempAuthCode()); + }, + onPeerDisconnect: (connection) => { + void example.service.onPeerDisconnect(connection); + }, + }).catch(async (error) => { + await example.dispose(); + throw error; + }); + const broker = example.service.broker; + const agent = { id: 'example-stdio-agent', label: 'Example MCP agent' }; + const mcp = new Server({ name: 'cdb-devframe-example', version: example.definition.version }, { capabilities: { tools: {} } }); + mcp.setRequestHandler('tools/list', async () => ({ tools: broker.tools.map(tool => ({ ...tool, inputSchema: { ...tool.inputSchema, type: 'object' as const } })) })); + mcp.setRequestHandler('tools/call', async (request, context) => { + try { + const value = await broker.invoke(agent, request.params.name, request.params.arguments, { signal: context.mcpReq.signal }); + return { content: [{ type: 'text' as const, text: typeof value === 'string' ? value : JSON.stringify(value) }] }; + } catch (error) { + return { isError: true, content: [{ type: 'text' as const, text: JSON.stringify({ ...(error as object), message: error instanceof Error ? error.message : String(error) }) }] }; + } + }); + let closing = false; + async function close(): Promise { + if (closing) return; + closing = true; + await example.dispose(); + await server.close(); + await mcp.close(); + } + mcp.onclose = () => { + void close(); + }; + process.once('SIGINT', () => { + void close(); + }); + process.once('SIGTERM', () => { + void close(); + }); + await mcp.connect(new StdioServerTransport()).catch(async (error) => { + await close(); + throw error; + }); + console.error(styleText('cyan', '🌐 [cdb-example]'), `Panel and provider RPC: http://127.0.0.1:${server.port}/`); +} + +void main(); diff --git a/examples/devframe/package.json b/examples/devframe/package.json index 77abc75..5a8f33d 100644 --- a/examples/devframe/package.json +++ b/examples/devframe/package.json @@ -4,11 +4,15 @@ "version": "0.0.0", "private": true, "scripts": { - "smoke": "node smoke.ts" + "smoke": "node smoke.ts", + "start": "node host.ts", + "build:extension": "node extension/build.ts" }, "dependencies": { - "@dvcol/cdb": "workspace:*", - "@modelcontextprotocol/client": "catalog:", + "@dvcol/cdb-broker": "workspace:*", + "@dvcol/cdb-devframe": "workspace:*", + "@dvcol/cdb-extension": "workspace:*", + "@modelcontextprotocol/server": "catalog:", "@vitejs/devtools-kit": "catalog:devtools", "devframe": "catalog:devtools", "vite": "catalog:devtools" diff --git a/examples/devframe/smoke.ts b/examples/devframe/smoke.ts index 83b713a..423f63c 100644 --- a/examples/devframe/smoke.ts +++ b/examples/devframe/smoke.ts @@ -1,125 +1,40 @@ -import type { PublishedTarget } from '@dvcol/cdb'; - import assert from 'node:assert/strict'; -import { createServer as createHttpServer } from 'node:http'; - -import { createEmbeddedChromeDebuggerBridge } from '@dvcol/cdb'; -import { createPluginFromDevframe } from '@vitejs/devtools-kit/node'; -import { viteDevBridge } from 'devframe/helpers/vite'; -import { createServer } from 'vite'; -import { createDevframeDefinition } from './devframe.ts'; +import { createDevServer } from 'devframe/adapters/dev'; -const targetSummaryPattern = /Available targets: 1/u; -const target: PublishedTarget = { - availability: 'available', - capabilities: { level: 'inspect' }, - generation: 1, - id: 'e5f7a25e-810e-41a7-97d0-ae4636c5e4e5', - scopeId: '76f667f1-cf48-4664-9c41-ffab0ed11b55', - type: 'page', -}; +import { createDevframeExample } from './devframe.ts'; -function requireRecord(value: unknown): Record { - assert.ok(typeof value === 'object' && value !== null && !Array.isArray(value)); - return value as Record; -} - -async function postMcp(endpoint: string, request: Record, sessionId?: string | null): Promise<{ body: Record; sessionId: string | null }> { - const response = await fetch(endpoint, { - body: JSON.stringify(request), - headers: { - 'Accept': 'application/json, text/event-stream', - 'Content-Type': 'application/json', - 'Origin': 'http://127.0.0.1', - ...(sessionId == null ? {} : { 'Mcp-Session-Id': sessionId }), - }, - method: 'POST', - }); - assert.equal(response.ok, true); - const responseText = await response.text(); - const data = responseText.split('\n').find(line => line.startsWith('data: ')); - if (data === undefined) throw new Error(`Expected an MCP JSON response, received ${responseText}.`); - const body = requireRecord(JSON.parse(data.slice('data: '.length)) as unknown); - return { body, sessionId: response.headers.get('Mcp-Session-Id') }; -} - -async function reserveLoopbackPort(): Promise { - const server = createHttpServer(); - await new Promise((resolve, reject) => { - server.listen(0, '127.0.0.1', resolve); - server.once('error', reject); - }); - const address = server.address(); - if (address === null || typeof address === 'string') throw new Error('Expected reserved TCP address.'); - await new Promise((resolve, reject) => server.close(error => error === undefined ? resolve() : reject(error))); - return address.port; -} +const panelTitlePattern = /Browser control/u; +const pageScriptPattern = /cdb:review-request/u; async function main(): Promise { - const bridge = createEmbeddedChromeDebuggerBridge(); - bridge.broker.publishTarget(target); - const devframe = createDevframeDefinition(bridge); - const devframePort = await reserveLoopbackPort(); - const viteServer = await createServer({ - configFile: false, - plugins: [ - createPluginFromDevframe(devframe), - viteDevBridge(devframe, { - auth: false, - base: '/__cdb-devframe/', - devMiddleware: { host: '127.0.0.1', port: devframePort }, - mcp: true, - }), - ], - server: { host: '127.0.0.1', port: 0 }, - }); - + const example = createDevframeExample(); try { - await viteServer.listen(); - const viteAddress = viteServer.httpServer?.address(); - if (viteAddress === null || viteAddress === undefined || typeof viteAddress === 'string') throw new Error('Expected Vite TCP address.'); - const connectionResponse = await fetch(`http://127.0.0.1:${viteAddress.port}/__cdb-devframe/__connection.json`); - assert.equal(connectionResponse.ok, true); - const connection = requireRecord(await connectionResponse.json() as unknown); - const mcp = requireRecord(connection.mcp); - assert.ok(typeof mcp.port === 'number'); - assert.ok(typeof mcp.path === 'string'); - - const endpoint = `http://127.0.0.1:${mcp.port}${mcp.path}`; - let initialized; + const server = await createDevServer(example.definition, { + port: 0, + host: '127.0.0.1', + auth: false, + mcp: false, + openBrowser: false, + onPeerConnect: (connection, session) => example.service.onPeerConnect(connection, session), + onPeerDisconnect: (connection) => { + void example.service.onPeerDisconnect(connection); + }, + }); try { - initialized = await postMcp(endpoint, { - id: 1, - jsonrpc: '2.0', - method: 'initialize', - params: { - capabilities: {}, - clientInfo: { name: 'cdb-devframe-example', version: '0.0.0' }, - protocolVersion: '2026-07-28', - }, - }); - } catch (error) { - throw new Error(`Unable to initialize Devframe MCP endpoint ${endpoint}.`, { cause: error }); + const page = await fetch(`http://127.0.0.1:${server.port}/`); + assert.equal(page.ok, true); + assert.match(await page.text(), panelTitlePattern); + const script = await fetch(`http://127.0.0.1:${server.port}/page-script.js`); + assert.equal(script.ok, true); + assert.match(await script.text(), pageScriptPattern); + assert.ok(example.service.broker.tools.some(tool => tool.name === 'browser.batch')); + assert.deepEqual(example.service.broker.snapshot().providers, []); + } finally { + await server.close(); } - assert.equal(typeof initialized.sessionId, 'string'); - const tools = await postMcp(endpoint, { id: 2, jsonrpc: '2.0', method: 'tools/list', params: {} }, initialized.sessionId); - const toolValues = requireRecord(tools.body.result).tools; - assert.ok(Array.isArray(toolValues)); - const availableTools = toolValues.map((value: unknown) => requireRecord(value)); - const summaryTool = availableTools.find(tool => typeof tool.name === 'string' && tool.name.includes('target-summary')); - assert.ok(summaryTool); - assert.equal(requireRecord(summaryTool.annotations).readOnlyHint, true); - assert.equal(availableTools.some(tool => typeof tool.name === 'string' && (tool.name.includes('cdp') || tool.name.includes('pair'))), false); - const summary = await postMcp(endpoint, { id: 3, jsonrpc: '2.0', method: 'tools/call', params: { arguments: {}, name: summaryTool.name } }, initialized.sessionId); - const content = requireRecord(summary.body.result).content; - assert.ok(Array.isArray(content)); - const text = requireRecord(content[0]).text; - assert.equal(typeof text, 'string'); - assert.match(String(text), targetSummaryPattern); } finally { - await viteServer.close(); - bridge.dispose(); + await example.dispose(); } } diff --git a/knip.jsonc b/knip.jsonc index d7238ed..c70d851 100644 --- a/knip.jsonc +++ b/knip.jsonc @@ -8,6 +8,8 @@ "@dvcol/cdb", "@dvcol/cdb-automation-playwright", "@dvcol/cdb-birpc", + "@dvcol/cdb-broker", + "@dvcol/cdb-devframe", "@dvcol/cdb-extension", "@dvcol/cdb-mcp", "@dvcol/cdb-websocket" @@ -23,7 +25,7 @@ "project": ["src/**/*.ts", "test/**/*.ts", "*.config.ts"] }, "examples/*": { - "entry": ["*.ts"], + "entry": ["*.ts", "extension/*.ts"], "project": ["**/*.ts"] }, "tests/fixtures/*": { diff --git a/package.json b/package.json index 6576def..8ae6691 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,8 @@ "@dvcol/cdb": "workspace:*", "@dvcol/cdb-automation-playwright": "workspace:*", "@dvcol/cdb-birpc": "workspace:*", + "@dvcol/cdb-broker": "workspace:*", + "@dvcol/cdb-devframe": "workspace:*", "@dvcol/cdb-extension": "workspace:*", "@dvcol/cdb-mcp": "workspace:*", "@dvcol/cdb-websocket": "workspace:*", @@ -44,6 +46,7 @@ "@vitest/coverage-v8": "catalog:testing", "bumpp": "catalog:release", "changelogithub": "catalog:release", + "devframe": "catalog:devtools", "eslint": "catalog:build", "eslint-plugin-format": "catalog:build", "jiti": "catalog:build", diff --git a/packages/broker/LICENSE b/packages/broker/LICENSE new file mode 100644 index 0000000..c4181d6 --- /dev/null +++ b/packages/broker/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 D.V. Colomban + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/broker/README.md b/packages/broker/README.md new file mode 100644 index 0000000..7ffacb1 --- /dev/null +++ b/packages/broker/README.md @@ -0,0 +1,32 @@ +# @dvcol/cdb-broker + +Browser-control orchestration for an embedding host. Requires Node 24. The host owns its processes, authenticated client identities, tool catalogue and MCP server. + +```ts +import { createBroker, createFileBrokerIdentityStore, defineBroker } from '@dvcol/cdb-broker'; + +const definition = defineBroker({ + identityStore: await createFileBrokerIdentityStore('/host/chosen/identity-directory'), + navigation: { default: 'same-origin', allowed: ['same-origin', 'follow-tab'] }, +}); +const broker = await createBroker(definition); + +// Register these contributions in the host's existing catalogue. +const tools = broker.tools; +const result = await broker.invoke(authenticatedPeer, toolName, arguments_, { signal }); + +await broker.disconnectPeer(authenticatedPeer.id); +await broker.dispose(); +``` + +`defineBroker` validates configuration without opening connections or starting processes. `createBroker` composes CDB authority stores, logical sessions, grant coordination, pairing and per-principal semantic tool sessions. `snapshot()` and `subscribe()` derive management state from those stores. The management interface must be restricted to the host's trusted operators and providers; it is not an agent tool. + +Use `@dvcol/cdb-devframe` to carry providers on an existing Devframe connection. Other transports can use the runtime's registration, authentication, provider connection and claim methods directly. + +Keep an existing identity directory when adopting this package. The file store reads the version-1 `identity.json` format and preserves broker IDs and pairing credentials. The host also preserves each extension's installation ID and pairing-store key. Resume credentials belong to the host, never to model-visible arguments. + +Navigation requests must match a configured preset; disallowed requests fail without downgrading. Each grant binds its selected preset to the requesting principal and exact target. `same-origin` retains the approved origin; `follow-tab` permits subsequent HTTP(S) origins. An optional `navigation.authorize` callback adds host restrictions. Navigation renews target generations and invalidates document-bound element references. + +Native automation is the default. To select the experimental Playwright provider, supply a lazy `automationProvider` factory that imports `@dvcol/cdb-automation-playwright` explicitly. It is not a dependency of this package. + +See the [public Devframe example](../../examples/devframe/README.md) and [architecture](../../ARCHITECTURE.md). diff --git a/packages/broker/package.json b/packages/broker/package.json new file mode 100644 index 0000000..55b5d7d --- /dev/null +++ b/packages/broker/package.json @@ -0,0 +1,62 @@ +{ + "name": "@dvcol/cdb-broker", + "type": "module", + "version": "0.1.0", + "description": "Composed CDB broker, authenticated provider lifecycle, and agent tool sessions.", + "author": "dvcol", + "license": "MIT", + "homepage": "https://github.com/dvcol/chrome-debugger-bridge#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/dvcol/chrome-debugger-bridge.git", + "directory": "packages/broker" + }, + "bugs": { + "url": "https://github.com/dvcol/chrome-debugger-bridge/issues" + }, + "keywords": [ + "chrome", + "chrome-debugger", + "cdp", + "browser-automation", + "mcp", + "agent" + ], + "sideEffects": false, + "exports": { + ".": { + "types": "./dist/index.d.mts", + "import": "./dist/index.mjs" + }, + "./contract": { + "types": "./dist/contract.d.mts", + "import": "./dist/contract.mjs" + } + }, + "files": [ + "LICENSE", + "README.md", + "dist" + ], + "engines": { + "node": ">=24.0.0" + }, + "scripts": { + "build": "tsdown", + "check:package": "publint", + "pack": "pnpm pack --out artifacts/package.tgz", + "test": "vitest run", + "typecheck": "tsc --noEmit --project tsconfig.json" + }, + "dependencies": { + "@dvcol/cdb": "workspace:^", + "@dvcol/cdb-mcp": "workspace:^" + }, + "devDependencies": { + "tsdown": "catalog:build", + "vitest": "catalog:testing" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/broker/src/config.ts b/packages/broker/src/config.ts new file mode 100644 index 0000000..7b10530 --- /dev/null +++ b/packages/broker/src/config.ts @@ -0,0 +1,68 @@ +import type { AuthorityStore, AutomationProvider } from '@dvcol/cdb'; + +import type { BrokerIdentityStore, BrokerTimingPolicy, NavigationPolicy } from './contract.js'; + +import { validateTimeoutMilliseconds } from '@dvcol/cdb'; + +export interface NavigationContext { + readonly approvedOrigin: string; + readonly navigation: NavigationPolicy; + readonly principalId: string; + readonly targetId: string; + readonly url: string | undefined; +} + +export interface BrokerDefinition { + readonly identityStore?: BrokerIdentityStore; + readonly authorityStore?: AuthorityStore; + readonly navigation?: { + readonly default?: NavigationPolicy; + readonly allowed?: readonly NavigationPolicy[]; + /** Adds host restrictions without widening the approved preset. */ + readonly authorize?: (context: NavigationContext) => boolean; + }; + /** Invoked only when a provider authenticates; the native implementation is otherwise used. */ + readonly automationProvider?: () => AutomationProvider | Promise; + readonly timing?: Partial; + readonly maximumProviderMessageBytes?: number; +} + +export const defaultBrokerTimingPolicy: Readonly = Object.freeze({ + accessRequestTimeoutMilliseconds: 60_000, + clientResumeWindowMilliseconds: 15 * 60_000, + pairingLifetimeMilliseconds: 5 * 60_000, + providerRecoveryMilliseconds: 60_000, + requestRateLimitMilliseconds: 2_000, +}); + +/** Validates configuration without creating authority, connections, timers, or processes. */ +export function defineBroker(definition: Definition): Definition { + const allowed = definition.navigation?.allowed ?? ['same-origin', 'follow-tab']; + const defaultPolicy = definition.navigation?.default ?? 'same-origin'; + if (!allowed.includes(defaultPolicy) || allowed.some(policy => policy !== 'same-origin' && policy !== 'follow-tab')) + throw new TypeError('The default navigation policy must be one of the permitted standard policies.'); + for (const [name, value] of Object.entries(definition.timing ?? {})) validateTimeoutMilliseconds(value, name); + if (definition.maximumProviderMessageBytes !== undefined && (!Number.isSafeInteger(definition.maximumProviderMessageBytes) || definition.maximumProviderMessageBytes < 1)) + throw new TypeError('maximumProviderMessageBytes must be a positive safe integer.'); + return definition; +} + +export function browserOrigin(url: string | undefined): string | undefined { + if (url === undefined) return undefined; + try { + const parsed = new URL(url); + return parsed.protocol === 'http:' || parsed.protocol === 'https:' ? parsed.origin : undefined; + } catch { + return undefined; + } +} + +export function allowsNavigation(definition: BrokerDefinition, context: NavigationContext): boolean { + const origin = browserOrigin(context.url); + if (origin === undefined || (context.navigation === 'same-origin' && origin !== context.approvedOrigin)) return false; + try { + return definition.navigation?.authorize?.(context) ?? true; + } catch { + return false; + } +} diff --git a/packages/broker/src/contract.ts b/packages/broker/src/contract.ts new file mode 100644 index 0000000..38a70b8 --- /dev/null +++ b/packages/broker/src/contract.ts @@ -0,0 +1,148 @@ +import type { AuthorityBinding, GrantRequestClaim, JsonValue, Lease, LogicalSessionCredential, TimeoutMilliseconds } from '@dvcol/cdb'; +import type { CdbToolDefinition } from '@dvcol/cdb-mcp'; +import type { AgentAuthenticationTranscript, BrokerAuthenticationClaims } from '@dvcol/cdb/authentication'; + +export type NavigationPolicy = 'same-origin' | 'follow-tab'; +export type AccessLevel = 'observe' | 'inspect' | 'interact' | 'debug' | 'unsafe'; + +export interface BrokerCredential { + readonly agentId: string; + readonly brokerId: string; + readonly credential: Uint8Array; + readonly credentialId: string; + readonly principalId: string; + readonly status: 'active' | 'pending'; +} + +export interface BrokerIdentityStore { + readonly brokerId: string; + activate: (record: BrokerCredential) => Promise; + findByAgentId: (agentId: string) => BrokerCredential | undefined; + load: (credentialId: string) => BrokerCredential | undefined; + remove: (credentialId: string) => Promise; +} + +export interface BrokerTimingPolicy { + readonly accessRequestTimeoutMilliseconds: TimeoutMilliseconds; + readonly clientResumeWindowMilliseconds: TimeoutMilliseconds; + readonly pairingLifetimeMilliseconds: TimeoutMilliseconds; + readonly providerRecoveryMilliseconds: TimeoutMilliseconds; + readonly requestRateLimitMilliseconds: TimeoutMilliseconds; +} + +export interface ProviderRegistration { + readonly id: string; + readonly instanceId: string; + readonly name: string; + readonly version: string; + readonly maximumLevel: AccessLevel; +} + +export interface ProviderState extends ProviderRegistration { + readonly paired: boolean; + readonly state: 'connecting' | 'ready' | 'recovering' | 'offline' | 'disconnected'; + readonly targetCount: number; + readonly recoveryDeadline?: number; +} + +export interface ProviderTarget { + readonly id: string; + readonly generation: number; + readonly scopeId: string; + readonly title?: string | undefined; + readonly url?: string | undefined; + readonly metadata?: JsonValue; +} + +export interface BrokerTarget extends ProviderTarget { + readonly providerId: string; + readonly state: 'available' | 'recovering' | 'offline'; +} + +export interface BrokerPrincipal { + readonly id: string; + readonly label: string; + readonly connectedAt: number; + readonly metadata?: JsonValue; +} + +export interface BrokerRequest { + readonly id: string; + readonly principalId: string; + readonly principalLabel: string; + readonly level: AccessLevel; + readonly navigation: NavigationPolicy; + readonly createdAt: number; + readonly expiresAt: number | null; + readonly state: 'pending' | 'claiming'; + readonly requestedTargetId?: string; + readonly metadata?: JsonValue; +} + +export interface BrokerGrant { + readonly id: string; + readonly requestId: string; + readonly principalId: string; + readonly principalLabel: string; + readonly providerId: string; + readonly targetId: string; + readonly targetGeneration: number; + readonly level: AccessLevel; + readonly navigation: NavigationPolicy; + readonly approvedOrigin: string; + readonly createdAt: number; + readonly state: 'active' | 'out-of-scope' | 'recovering'; + readonly metadata?: JsonValue; +} + +/** Non-secret management state. Agent tools receive only their principal's authorized targets. */ +export interface BrokerState { + readonly revision: number; + readonly providers: readonly ProviderState[]; + readonly principals: readonly BrokerPrincipal[]; + readonly requests: readonly BrokerRequest[]; + readonly targets: readonly BrokerTarget[]; + readonly grants: readonly BrokerGrant[]; + readonly scopes: readonly (BrokerRequest & { readonly providerId: string })[]; + readonly leases: readonly (Lease & { readonly principalId: string })[]; +} + +export interface ProviderPairingOffer { + readonly brokerId: string; + readonly code: string; + readonly expiresAt: string | null; +} + +export interface ProviderAuthenticationInput { + readonly credentialId: string; + readonly clientNonce: string; + readonly pairing?: { readonly code: string; readonly credential: string }; +} + +export interface ProviderAuthenticationResult { + readonly claims: BrokerAuthenticationClaims; + readonly proof: string; +} + +export interface BrowserAccessResult { + readonly grant: Omit & { readonly targetRef: string }; + readonly target: { + readonly availability: 'available'; + readonly capabilities: { readonly level: AccessLevel }; + readonly targetRef: string; + readonly type: 'page'; + readonly title?: string; + readonly url?: string; + }; +} + +export interface BrokerPeer { + /** Set by the authenticated transport adapter, never by tool arguments. */ + readonly id: string; + readonly label?: string; + readonly metadata?: JsonValue; +} + +export type BrokerTool = Omit; + +export type { AgentAuthenticationTranscript, AuthorityBinding, GrantRequestClaim, LogicalSessionCredential }; diff --git a/packages/broker/src/error.ts b/packages/broker/src/error.ts new file mode 100644 index 0000000..9ad35f3 --- /dev/null +++ b/packages/broker/src/error.ts @@ -0,0 +1,12 @@ +export class BrokerError extends Error { + constructor( + readonly code: string, + message: string, + readonly retryable = false, + readonly retryAfterMilliseconds?: number, + readonly details?: Readonly>, + ) { + super(message); + this.name = 'BrokerError'; + } +} diff --git a/packages/broker/src/identity-store.ts b/packages/broker/src/identity-store.ts new file mode 100644 index 0000000..d874424 --- /dev/null +++ b/packages/broker/src/identity-store.ts @@ -0,0 +1,127 @@ +import type { BrokerCredential, BrokerIdentityStore } from './contract.js'; + +import { Buffer } from 'node:buffer'; +import { randomUUID } from 'node:crypto'; +import { chmod, mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +interface StoredIdentity { + readonly version: 1; + readonly brokerId: string; + readonly credentials: readonly { + readonly agentId: string; + readonly principalId: string; + readonly credentialId: string; + readonly credential: string; + }[]; +} + +function record(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +function parseIdentity(source: string): StoredIdentity { + const parsed: unknown = JSON.parse(source); + if (!record(parsed) || parsed.version !== 1 || typeof parsed.brokerId !== 'string' || !parsed.brokerId || !Array.isArray(parsed.credentials)) + throw new Error('The broker identity has an unsupported shape.'); + const credentialIds = new Set(); + const credentials = parsed.credentials.map((candidate: unknown) => { + if (!record(candidate) || typeof candidate.agentId !== 'string' || typeof candidate.principalId !== 'string' + || typeof candidate.credentialId !== 'string' || typeof candidate.credential !== 'string') + throw new Error('The broker identity contains an invalid credential.'); + const { agentId, principalId, credentialId, credential } = candidate; + if (!agentId || !principalId || !credentialId || credentialIds.has(credentialId) + || Buffer.from(credential, 'base64url').byteLength !== 32 || Buffer.from(credential, 'base64url').toString('base64url') !== credential) + throw new Error('The broker identity contains a duplicate or invalid credential.'); + credentialIds.add(credentialId); + return { agentId, principalId, credentialId, credential }; + }); + return { version: 1, brokerId: parsed.brokerId, credentials }; +} + +function createIdentityStore(identity: StoredIdentity, persist: (identity: StoredIdentity) => Promise): BrokerIdentityStore { + let current = identity; + let pending = Promise.resolve(); + const load = (credentialId: string): BrokerCredential | undefined => { + const credential = current.credentials.find(candidate => candidate.credentialId === credentialId); + return credential === undefined + ? undefined + : { + ...credential, + brokerId: current.brokerId, + credential: Uint8Array.from(Buffer.from(credential.credential, 'base64url')), + status: 'active', + }; + }; + const update = async (transform: (identity: StoredIdentity) => StoredIdentity): Promise => { + const operation = pending.then(async () => { + const next = transform(current); + await persist(next); + current = next; + }); + pending = operation.catch(() => {}); + return operation; + }; + return { + brokerId: identity.brokerId, + async activate(credential) { + if (credential.brokerId !== identity.brokerId || credential.credential.byteLength !== 32) + throw new TypeError('The credential does not belong to this broker.'); + await update((previous) => { + if (credential.status === 'pending' && previous.credentials.some(candidate => candidate.agentId === credential.agentId || candidate.credentialId === credential.credentialId)) + throw new Error('The provider was paired by another connection.'); + return { + ...previous, + credentials: [ + ...previous.credentials.filter(candidate => candidate.credentialId !== credential.credentialId), + { agentId: credential.agentId, principalId: credential.principalId, credentialId: credential.credentialId, credential: Buffer.from(credential.credential).toString('base64url') }, + ], + }; + }); + return load(credential.credentialId)!; + }, + findByAgentId(agentId) { + const credential = current.credentials.find(candidate => candidate.agentId === agentId); + return credential === undefined ? undefined : load(credential.credentialId); + }, + load, + remove: async credentialId => update(previous => ({ ...previous, credentials: previous.credentials.filter(candidate => candidate.credentialId !== credentialId) })), + }; +} + +export function createMemoryBrokerIdentityStore(brokerId = randomUUID()): BrokerIdentityStore { + return createIdentityStore({ version: 1, brokerId, credentials: [] }, async () => {}); +} + +/** Keeps the version-one identity format; only identity and pairing records survive a restart. */ +export async function createFileBrokerIdentityStore(directory: string): Promise { + await mkdir(directory, { recursive: true, mode: 0o700 }); + await chmod(directory, 0o700); + const path = join(directory, 'identity.json'); + let identity: StoredIdentity; + try { + identity = parseIdentity(await readFile(path, 'utf8')); + } catch (error) { + if (!record(error) || error.code !== 'ENOENT') + throw new Error(`Cannot read broker identity at ${path}; repair it or explicitly rotate the identity.`, { cause: error }); + identity = { version: 1, brokerId: randomUUID(), credentials: [] }; + try { + await writeFile(path, `${JSON.stringify(identity, null, 2)}\n`, { flag: 'wx', mode: 0o600 }); + } catch (creationError) { + if (!record(creationError) || creationError.code !== 'EEXIST') throw creationError; + identity = parseIdentity(await readFile(path, 'utf8')); + } + } + await chmod(path, 0o600); + return createIdentityStore(identity, async (next) => { + const temporaryPath = `${path}.${process.pid}.${randomUUID()}.tmp`; + try { + await writeFile(temporaryPath, `${JSON.stringify(next, null, 2)}\n`, { flag: 'wx', mode: 0o600 }); + await rename(temporaryPath, path); + } finally { + await unlink(temporaryPath).catch((error: unknown) => { + if (!record(error) || error.code !== 'ENOENT') throw error; + }); + } + }); +} diff --git a/packages/broker/src/index.ts b/packages/broker/src/index.ts new file mode 100644 index 0000000..62fbd00 --- /dev/null +++ b/packages/broker/src/index.ts @@ -0,0 +1,5 @@ +export * from './config.js'; +export * from './contract.js'; +export * from './error.js'; +export * from './identity-store.js'; +export * from './runtime.js'; diff --git a/packages/broker/src/provider-authentication.ts b/packages/broker/src/provider-authentication.ts new file mode 100644 index 0000000..7818a34 --- /dev/null +++ b/packages/broker/src/provider-authentication.ts @@ -0,0 +1,128 @@ +import type { AgentAuthenticationTranscript } from '@dvcol/cdb/authentication'; + +import type { BrokerCredential, BrokerIdentityStore, ProviderAuthenticationInput, ProviderAuthenticationResult, ProviderPairingOffer } from './contract.js'; + +import { + createBrokerAuthenticationProof, + decodeBase64UrlBytes, + generateRandomBase64Url, + importAgentCredential, + verifyAgentAuthenticationProof, +} from '@dvcol/cdb/authentication'; + +interface Challenge { + readonly credential: BrokerCredential; + readonly transcript: AgentAuthenticationTranscript; +} + +export interface ProviderAuthentication { + authenticate: (peerId: string, proof: string) => Promise; + begin: (peerId: string, instanceId: string, input: ProviderAuthenticationInput) => AgentAuthenticationTranscript; + dispose: () => void; + forget: (peerId: string) => void; + invite: (peerId: string, instanceId: string) => ProviderPairingOffer; +} + +/** Proves installation pairing on an existing host-authenticated transport, bound to its actual peer. */ +export function createProviderAuthentication(identityStore: BrokerIdentityStore, pairingLifetimeMilliseconds: number | null): ProviderAuthentication { + const challenges = new Map(); + const invitations = new Map(); + const generations = new Map(); + const attempts = new Map(); + let disposed = false; + + function ensureActive(): void { + if (disposed) throw new Error('Provider authentication has stopped.'); + } + + return { + async authenticate(peerId, proof) { + ensureActive(); + const challenge = challenges.get(peerId); + challenges.delete(peerId); + if (challenge === undefined) throw new Error('No current provider authentication challenge.'); + const { transcript } = challenge; + const current = (): void => { + ensureActive(); + if (attempts.get(peerId) !== transcript.connectionId || Date.parse(transcript.expiresAt) <= Date.now()) + throw new Error('The provider authentication challenge expired or was replaced.'); + }; + current(); + const key = await importAgentCredential(challenge.credential.credential); + if (!await verifyAgentAuthenticationProof(key, transcript, proof)) throw new Error('Provider authentication failed.'); + current(); + if (challenge.credential.status === 'pending' && identityStore.findByAgentId(challenge.credential.agentId) !== undefined) + throw new Error('The provider was paired by another connection.'); + if (challenge.credential.status === 'active' && identityStore.load(challenge.credential.credentialId) === undefined) + throw new Error('The provider pairing was revoked.'); + const credential = challenge.credential.status === 'active' + ? challenge.credential + : await identityStore.activate(challenge.credential); + current(); + const connectionGeneration = (generations.get(credential.principalId) ?? 0) + 1; + generations.set(credential.principalId, connectionGeneration); + const claims = { connectionGeneration, principalId: credential.principalId }; + const brokerProof = await createBrokerAuthenticationProof(key, transcript, claims); + current(); + if (generations.get(credential.principalId) !== connectionGeneration) + throw new Error('A newer provider connection replaced this authentication.'); + return { claims, credential, proof: brokerProof, transcript }; + }, + begin(peerId, instanceId, input) { + ensureActive(); + decodeBase64UrlBytes(input.clientNonce); + let credential = identityStore.load(input.credentialId); + if (input.pairing !== undefined) { + const invitation = invitations.get(instanceId); + if (credential !== undefined || identityStore.findByAgentId(instanceId) !== undefined + || invitation?.peerId !== peerId || invitation.code !== input.pairing.code + || (invitation.expiresAt !== null && invitation.expiresAt <= Date.now())) + throw new Error('The provider pairing invitation is not valid.'); + const bytes = decodeBase64UrlBytes(input.pairing.credential); + invitations.delete(instanceId); + credential = { agentId: instanceId, brokerId: identityStore.brokerId, credential: bytes, credentialId: input.credentialId, principalId: instanceId, status: 'pending' }; + } + if (credential === undefined || credential.agentId !== instanceId || credential.brokerId !== identityStore.brokerId) + throw new Error('The provider pairing is not recognized.'); + const transcript: AgentAuthenticationTranscript = { + agentId: instanceId, + brokerId: identityStore.brokerId, + clientNonce: input.clientNonce, + connectionId: crypto.randomUUID(), + credentialId: credential.credentialId, + endpointPath: 'cdb:broker', + expiresAt: new Date(Date.now() + 5_000).toISOString(), + origin: `peer:${peerId}`, + protocolVersion: 1, + serverNonce: generateRandomBase64Url(32), + transportProtocol: 'chrome-debugger-bridge.rpc.v1', + }; + challenges.set(peerId, { credential, transcript }); + attempts.set(peerId, transcript.connectionId); + return structuredClone(transcript); + }, + dispose() { + disposed = true; + challenges.clear(); + invitations.clear(); + attempts.clear(); + generations.clear(); + }, + forget(peerId) { + challenges.delete(peerId); + attempts.delete(peerId); + for (const [instanceId, invitation] of invitations) if (invitation.peerId === peerId) invitations.delete(instanceId); + }, + invite(peerId, instanceId) { + ensureActive(); + if (identityStore.findByAgentId(instanceId) !== undefined) throw new Error('Forget the existing provider pairing before replacing it.'); + const invitation = { + peerId, + code: generateRandomBase64Url(24), + expiresAt: pairingLifetimeMilliseconds === null ? null : Date.now() + pairingLifetimeMilliseconds, + }; + invitations.set(instanceId, invitation); + return { brokerId: identityStore.brokerId, code: invitation.code, expiresAt: invitation.expiresAt === null ? null : new Date(invitation.expiresAt).toISOString() }; + }, + }; +} diff --git a/packages/broker/src/runtime.ts b/packages/broker/src/runtime.ts new file mode 100644 index 0000000..ae1977f --- /dev/null +++ b/packages/broker/src/runtime.ts @@ -0,0 +1,756 @@ +import type { AgentTargetConnection, AuthorityBinding, GrantedTargetReference, GrantRequest, GrantRequestClaim, GrantRequestProvider, JsonValue, LogicalSessionCredential } from '@dvcol/cdb'; +import type { CdbToolInvocationContext, McpChromeDebuggerBridgeClient } from '@dvcol/cdb-mcp'; +import type { AgentAuthenticationTranscript, BrokerAuthenticationClaims } from '@dvcol/cdb/authentication'; + +import type { BrokerDefinition } from './config.js'; +import type { AccessLevel, BrokerGrant, BrokerPeer, BrokerRequest, BrokerState, BrokerTarget, BrokerTool, BrowserAccessResult, NavigationPolicy, ProviderAuthenticationInput, ProviderRegistration, ProviderState, ProviderTarget } from './contract.js'; +import type { BrokerSession } from './session-client.js'; + +import { connectAgentTargetBroker, createGrantRequestCoordinator, createLogicalSessionManager, createMemoryAuthorityStore, createTargetBroker, scheduleTimeout } from '@dvcol/cdb'; +import { createCdbToolSession } from '@dvcol/cdb-mcp'; + +import packageManifest from '../package.json' with { type: 'json' }; +import { allowsNavigation, browserOrigin, defaultBrokerTimingPolicy, defineBroker } from './config.js'; +import { BrokerError } from './error.js'; +import { createMemoryBrokerIdentityStore } from './identity-store.js'; +import { createProviderAuthentication } from './provider-authentication.js'; +import { createBrokerSession } from './session-client.js'; + +const accessLevels: readonly AccessLevel[] = ['observe', 'inspect', 'interact', 'debug', 'unsafe']; + +interface Provider { + readonly registration: ProviderRegistration; + readonly peer: BrokerPeer; + readonly generation: number; + readonly connection: AgentTargetConnection; + readonly disconnect: () => void; + state: ProviderState['state']; + recoveryDeadline?: number; + recoveryTimer?: ReturnType; +} + +interface RequestMetadata { + readonly navigation: NavigationPolicy; + readonly createdAt: number; +} + +interface BindingMetadata extends RequestMetadata { + readonly requestId: string; + readonly providerId: string; + readonly approvedOrigin: string; +} + +function object(value: unknown): value is Record { + return value !== null && typeof value === 'object' && !Array.isArray(value); +} + +function requestMetadata(request: GrantRequest): RequestMetadata { + const metadata = request.metadata; + if (!object(metadata) || (metadata.navigation !== 'same-origin' && metadata.navigation !== 'follow-tab') || typeof metadata.createdAt !== 'number') + throw new BrokerError('ACCESS_DENIED', 'The grant request has no broker policy.'); + return { navigation: metadata.navigation, createdAt: metadata.createdAt }; +} + +function bindingMetadata(binding: AuthorityBinding): BindingMetadata | undefined { + const metadata = binding.metadata; + if (!object(metadata) || (metadata.navigation !== 'same-origin' && metadata.navigation !== 'follow-tab') + || typeof metadata.requestId !== 'string' || typeof metadata.providerId !== 'string' + || typeof metadata.approvedOrigin !== 'string' || typeof metadata.createdAt !== 'number') return undefined; + return { navigation: metadata.navigation, requestId: metadata.requestId, providerId: metadata.providerId, approvedOrigin: metadata.approvedOrigin, createdAt: metadata.createdAt }; +} + +export interface BrokerRuntime { + readonly brokerId: string; + readonly toolProvider: { readonly name: string; readonly version: string }; + readonly tools: readonly BrokerTool[]; + connectSession: (peer: BrokerPeer, input?: { readonly metadata?: JsonValue; readonly resume?: { readonly credential: string; readonly logicalSessionId: string } }) => Promise; + snapshot: () => BrokerState; + subscribe: (listener: (state: BrokerState) => void) => () => void; + registerProvider: (peer: BrokerPeer, registration: ProviderRegistration) => { readonly brokerId: string }; + createPairingOffer: (peer: BrokerPeer) => { readonly brokerId: string; readonly code: string; readonly expiresAt: string | null }; + beginProviderAuthentication: (peer: BrokerPeer, input: ProviderAuthenticationInput) => AgentAuthenticationTranscript; + authenticateProvider: (peer: BrokerPeer, proof: string, connection: AgentTargetConnection) => Promise<{ readonly proof: string; readonly claims: BrokerAuthenticationClaims; readonly transcript: AgentAuthenticationTranscript }>; + reconcileProvider: (peer: BrokerPeer, targets: readonly ProviderTarget[]) => Promise; + claimRequest: (peer: BrokerPeer, requestId: string) => { readonly claim: GrantRequestClaim; readonly request: BrokerRequest }; + releaseClaim: (peer: BrokerPeer, claim: GrantRequestClaim) => void; + completeClaim: (peer: BrokerPeer, claim: GrantRequestClaim, targets: readonly ProviderTarget[]) => Promise; + reconcileScope: (peer: BrokerPeer, requestId: string, targets: readonly ProviderTarget[]) => Promise; + revokeScope: (requestId: string) => Promise; + revokeGrant: (grantId: string) => Promise; + disconnectProvider: (providerId: string, forgetPairing?: boolean) => Promise; + disconnectPeer: (peerId: string) => Promise; + terminateSession: (peer: BrokerPeer) => Promise; + invoke: (peer: BrokerPeer, name: string, input: unknown, context?: CdbToolInvocationContext) => Promise; + dispose: () => Promise; +} + +/** Composes browser authority and tools without owning any process, server, or transport. */ +export async function createBroker(configuration: BrokerDefinition = {}): Promise { + const definition = defineBroker(configuration); + const timing = { ...defaultBrokerTimingPolicy, ...definition.timing }; + const identityStore = definition.identityStore ?? createMemoryBrokerIdentityStore(); + const authorityStore = definition.authorityStore ?? createMemoryAuthorityStore(); + const targetBroker = createTargetBroker({ + maximumArtifactBytes: 16 * 1_024 * 1_024, + maximumInlineResultBytes: 64 * 1_024, + timing: { reconnectGraceMilliseconds: 0 }, + }); + const logicalSessions = createLogicalSessionManager({ authorityStore, timing: { resumeWindowMilliseconds: timing.clientResumeWindowMilliseconds } }); + const authentication = createProviderAuthentication(identityStore, timing.pairingLifetimeMilliseconds); + const providers = new Map(); + const registrations = new Map(); + const targetMetadata = new Map(); + const sessions = new Map(); + const sessionsByPeer = new Map(); + const connectingSessions = new Map>(); + const fencedPeers = new Set(); + const lastRequests = new Map(); + const pendingRequests = new Map>>(); + const membershipUpdates = new Map>(); + const listeners = new Set<(state: BrokerState) => void>(); + let revision = 0; + let disposed = false; + let publicationQueued = false; + let authorityUpdates = Promise.resolve(); + + const grantCoordinator = createGrantRequestCoordinator({ + authorityStore, + targetDirectory: { + getProviderConnectionGeneration: principalId => [...providers.values()].find(provider => provider.registration.instanceId === principalId && provider.state === 'ready')?.generation, + getTarget(targetId) { + const target = targetBroker.listTargets().find(candidate => candidate.id === targetId); + const providerPrincipalId = targetBroker.getTargetAgentPrincipalId(targetId); + return target === undefined || providerPrincipalId === undefined ? undefined : { target, providerPrincipalId }; + }, + }, + timing: { requestTimeoutMilliseconds: timing.accessRequestTimeoutMilliseconds }, + }); + + function ensureActive(): void { + if (disposed) throw new BrokerError('GRANT_REVOKED', 'The broker has stopped.'); + } + + function currentProvider(peer: BrokerPeer): Provider { + ensureActive(); + const provider = [...providers.values()].find(candidate => candidate.peer.id === peer.id); + if (provider === undefined || provider.state !== 'ready') throw new BrokerError('PROVIDER_RECOVERING', 'The authenticated provider is not ready.', true); + return provider; + } + + function providerAuthority(provider: Provider): GrantRequestProvider { + return { principalId: provider.registration.instanceId, connectionGeneration: provider.generation }; + } + + function assertProviderIdentity(registration: ProviderRegistration): void { + const existing = [...providers.values()].find(provider => provider.registration.id === registration.id || provider.registration.instanceId === registration.instanceId); + if (existing !== undefined && (existing.registration.id !== registration.id || existing.registration.instanceId !== registration.instanceId)) + throw new BrokerError('ACCESS_DENIED', 'A provider cannot replace its stable installation identity.'); + } + + function targets(): BrokerTarget[] { + return targetBroker.listTargets().flatMap((published) => { + const provider = [...providers.values()].find(candidate => candidate.registration.instanceId === targetBroker.getTargetAgentPrincipalId(published.id)); + const metadata = targetMetadata.get(published.id); + if (provider === undefined || metadata === undefined) return []; + return [{ + ...metadata, + generation: published.generation, + ...(published.title === undefined ? {} : { title: published.title }), + ...(published.url === undefined ? {} : { url: published.url }), + providerId: provider.registration.id, + state: provider.state === 'ready' ? 'available' as const : provider.state === 'recovering' || provider.state === 'connecting' ? 'recovering' as const : 'offline' as const, + }]; + }); + } + + function grants(): BrokerGrant[] { + const currentTargets = targets(); + return [...sessions.values()].flatMap(session => session.bindings.flatMap((binding) => { + const metadata = bindingMetadata(binding); + if (metadata === undefined || grantCoordinator.getRequest(metadata.requestId)?.state !== 'granted' || (binding.expiresAt != null && Date.parse(binding.expiresAt) <= Date.now())) return []; + const target = currentTargets.find(candidate => candidate.id === binding.targetId); + const state = target?.state !== 'available' || target.generation !== binding.targetGeneration + ? 'recovering' as const + : allowsNavigation(definition, { ...metadata, principalId: session.principal.id, targetId: binding.targetId, url: target.url }) ? 'active' as const : 'out-of-scope' as const; + return [{ + ...metadata, + id: binding.bindingId, + principalId: session.principal.id, + principalLabel: session.principal.label, + targetId: binding.targetId, + targetGeneration: binding.targetGeneration, + level: binding.capabilities.level ?? 'observe', + state, + ...(target?.metadata === undefined ? {} : { metadata: target.metadata }), + }]; + })); + } + + function projectRequest(request: GrantRequest): BrokerRequest { + const session = sessions.get(request.logicalSessionId); + return { + ...requestMetadata(request), + id: request.id, + principalId: request.principalId, + principalLabel: session?.principal.label ?? request.principalId, + level: request.capabilities.level ?? 'observe', + expiresAt: request.expiresAt == null ? null : Date.parse(request.expiresAt), + state: request.state === 'claimed' ? 'claiming' : 'pending', + ...(request.requestedTargetId === undefined ? {} : { requestedTargetId: request.requestedTargetId }), + ...(session?.principal.metadata === undefined ? {} : { metadata: session.principal.metadata }), + }; + } + + function snapshot(): BrokerState { + const currentTargets = targets(); + const registeredProviders = Array.from(providers.values(), provider => ({ + ...provider.registration, + paired: identityStore.findByAgentId(provider.registration.instanceId) !== undefined, + state: provider.state, + targetCount: currentTargets.filter(target => target.providerId === provider.registration.id).length, + ...(provider.recoveryDeadline === undefined ? {} : { recoveryDeadline: provider.recoveryDeadline }), + })); + for (const registration of registrations.values()) { + if (!registeredProviders.some(provider => provider.id === registration.id)) + registeredProviders.push({ ...registration, paired: identityStore.findByAgentId(registration.instanceId) !== undefined, state: 'connecting', targetCount: 0 }); + } + return structuredClone({ + revision, + providers: registeredProviders, + principals: Array.from(sessions.values(), session => session.principal), + requests: grantCoordinator.inspect().filter(({ request }) => request.state !== 'granted').map(({ request }) => projectRequest(request)), + scopes: grantCoordinator.inspect().flatMap(({ request, provider }) => request.state !== 'granted' || provider === undefined + ? [] + : [{ + ...projectRequest(request), + providerId: [...providers.values()].find(candidate => candidate.registration.instanceId === provider.principalId)?.registration.id ?? provider.principalId, + }]), + targets: currentTargets, + grants: grants(), + leases: targetBroker.listLeases(), + }); + } + + function changed(): void { + if (publicationQueued || disposed) return; + publicationQueued = true; + queueMicrotask(() => { + publicationQueued = false; + if (disposed) return; + revision += 1; + const state = snapshot(); + for (const listener of listeners) { + try { + listener(state); + } catch { /** Presentation observers do not participate in authority changes. */ } + } + }); + } + + async function refreshSessions(): Promise { + await Promise.all(Array.from(sessions.values(), async session => session.refresh())); + } + + const unsubscribeAuthority = authorityStore.subscribe(({ logicalSessionId }) => { + authorityUpdates = authorityUpdates.then(async () => { + const session = sessions.get(logicalSessionId); + if (session === undefined) return; + await session.refresh(); + let record; + try { + record = await authorityStore.get(logicalSessionId); + } catch { + changed(); + return; + } + if (record === undefined) { + session.dispose(); + sessions.delete(logicalSessionId); + for (const [peerId, registeredSession] of sessionsByPeer) { + if (registeredSession === session) { + sessionsByPeer.delete(peerId); + fencedPeers.add(peerId); + } + } + await Promise.all(grantCoordinator.inspect().filter(({ request }) => request.logicalSessionId === logicalSessionId).map(async ({ request }) => grantCoordinator.cancel(request.id))); + } + changed(); + }).catch(() => { + changed(); + }); + }); + + const unsubscribeRequests = grantCoordinator.subscribe(({ requestId, request, error }) => { + for (const session of sessions.values()) session.refreshTargets(); + const pending = pendingRequests.get(requestId); + if (pending !== undefined && (request === undefined || error !== undefined)) { + pendingRequests.delete(requestId); + pending.reject(new BrokerError(error?.code ?? 'ACCESS_REQUEST_TIMEOUT', error?.message ?? 'The browser access request ended.', error?.retryable ?? false)); + } + changed(); + }); + + async function establishSession(peer: BrokerPeer, input: { readonly metadata?: JsonValue; readonly resume?: { readonly credential: string; readonly logicalSessionId: string } }): Promise { + ensureActive(); + if (sessionsByPeer.has(peer.id)) throw new BrokerError('ACCESS_DENIED', 'This peer already has a browser session.'); + const credential = input.resume === undefined + ? await logicalSessions.create({ connectionId: peer.id, principalId: crypto.randomUUID(), ...(input.metadata === undefined ? {} : { metadata: input.metadata }) }) + : await logicalSessions.resume({ connectionId: peer.id, logicalSessionId: input.resume.logicalSessionId, resumeCredential: input.resume.credential }); + ensureActive(); + const record = await authorityStore.get(credential.logicalSessionId); + if (record === undefined || record.activeConnectionId !== peer.id) throw new BrokerError('GRANT_REVOKED', 'The browser session was replaced.'); + let session = sessions.get(record.logicalSessionId); + if (session === undefined) { + session = createBrokerSession({ definition, authorityStore, targetBroker, changed, isProviderReady: providerId => providers.get(providerId)?.state === 'ready', isRequestGranted: requestId => grantCoordinator.getRequest(requestId)?.state === 'granted' }, { + id: record.principalId, + label: peer.label ?? `Browser client ${record.principalId.slice(-4)}`, + connectedAt: Date.now(), + ...(record.metadata === undefined ? {} : { metadata: record.metadata }), + }, record); + sessions.set(record.logicalSessionId, session); + } + for (const [previousPeerId, previousSession] of sessionsByPeer) { + if (previousSession === session) { + sessionsByPeer.delete(previousPeerId); + fencedPeers.add(previousPeerId); + } + } + session.bind(peer); + sessionsByPeer.set(peer.id, session); + await session.refresh(); + changed(); + return credential; + } + + async function connectSession(peer: BrokerPeer, input: Parameters[1] = {}): Promise { + ensureActive(); + if (fencedPeers.has(peer.id) || connectingSessions.has(peer.id)) throw new BrokerError('ACCESS_DENIED', 'This browser connection was replaced or is already connecting.'); + const operation = establishSession(peer, input).finally(() => connectingSessions.delete(peer.id)); + connectingSessions.set(peer.id, operation); + return operation; + } + + async function sessionFor(peer: BrokerPeer): Promise { + ensureActive(); + let session = sessionsByPeer.get(peer.id); + if (session === undefined) { + await (connectingSessions.get(peer.id) ?? connectSession(peer, { ...(peer.metadata === undefined ? {} : { metadata: peer.metadata }) })); + session = sessionsByPeer.get(peer.id); + } + if (session === undefined) throw new BrokerError('GRANT_REVOKED', 'The browser session is unavailable.'); + await session.refresh(); + return session; + } + + async function cancelProviderRequests(provider: Provider): Promise { + for (const state of grantCoordinator.inspect()) { + if (state.provider?.principalId === provider.registration.instanceId) await grantCoordinator.cancel(state.request.id); + } + await refreshSessions(); + } + + function recover(provider: Provider): void { + if (disposed || providers.get(provider.registration.id) !== provider || provider.state === 'disconnected') return; + provider.state = 'recovering'; + if (provider.recoveryTimer !== undefined) clearTimeout(provider.recoveryTimer); + if (timing.providerRecoveryMilliseconds === null) delete provider.recoveryDeadline; + else provider.recoveryDeadline = Date.now() + timing.providerRecoveryMilliseconds; + for (const session of sessions.values()) session.refreshTargets(); + const timer = scheduleTimeout(() => { + if (providers.get(provider.registration.id) !== provider || provider.state !== 'recovering') return; + provider.state = 'offline'; + targetBroker.revokeAgentTargets(providerAuthority(provider), 'detached'); + void cancelProviderRequests(provider).catch(() => {}).finally(changed); + }, timing.providerRecoveryMilliseconds); + if (timer !== undefined) { + provider.recoveryTimer = timer; + timer.unref(); + } + changed(); + } + + function checkedTargets(provider: Provider, values: readonly ProviderTarget[]): BrokerTarget[] { + const uniqueIds = new Set(); + return values.map((value) => { + const published = targetBroker.listTargets().find(target => target.id === value.id && target.generation === value.generation); + if (uniqueIds.has(value.id) || published === undefined || targetBroker.getTargetAgentPrincipalId(value.id) !== provider.registration.instanceId) + throw new BrokerError('TARGET_GONE', 'The provider has not published this exact target generation.', true); + uniqueIds.add(value.id); + const target = { ...value, ...(published.url === undefined ? {} : { url: published.url }), providerId: provider.registration.id, state: 'available' as const }; + if (browserOrigin(target.url) === undefined) throw new BrokerError('ACCESS_DENIED', 'Only HTTP(S) targets can be approved.'); + return target; + }); + } + + function bindingTargets(request: GrantRequest, provider: Provider, values: readonly ProviderTarget[]): GrantedTargetReference[] { + const previous = grantCoordinator.inspect().find(state => state.request.id === request.id)?.bindings ?? []; + return checkedTargets(provider, values).map((target) => { + const prior = previous.find(binding => binding.targetId === target.id); + const metadata: BindingMetadata = { + ...requestMetadata(request), + requestId: request.id, + providerId: provider.registration.id, + approvedOrigin: (prior === undefined ? undefined : bindingMetadata(prior)?.approvedOrigin) ?? browserOrigin(target.url)!, + }; + targetMetadata.set(target.id, structuredClone(target)); + return { targetId: target.id, targetGeneration: target.generation, metadata: { ...metadata } }; + }); + } + + async function reconcileScope(peer: BrokerPeer, requestId: string, values: readonly ProviderTarget[]): Promise { + const targets = structuredClone(values); + const update = (membershipUpdates.get(requestId) ?? Promise.resolve()).catch(() => {}).then(async () => { + const provider = currentProvider(peer); + const request = grantCoordinator.getRequest(requestId); + if (request === undefined) throw new BrokerError('GRANT_REVOKED', 'The approved scope no longer exists.'); + await grantCoordinator.reconcile(requestId, providerAuthority(provider), bindingTargets(request, provider, targets)); + await refreshSessions(); + changed(); + }); + membershipUpdates.set(requestId, update); + try { + await update; + } finally { + if (membershipUpdates.get(requestId) === update) membershipUpdates.delete(requestId); + } + } + + function accessResult(grant: BrokerGrant, session: BrokerSession): BrowserAccessResult { + const published = targetBroker.listTargets(session.authority).find(target => target.id === grant.targetId); + const targetRef = published === undefined ? undefined : session.tools.projectTarget(published)?.targetRef; + if (targetRef === undefined) throw new BrokerError('TARGET_GONE', 'The approved target is not available yet.', true); + const target = targets().find(candidate => candidate.id === grant.targetId); + const { targetId: _targetId, targetGeneration: _targetGeneration, ...publicGrant } = grant; + return { + grant: { ...publicGrant, targetRef }, + target: { availability: 'available', capabilities: { level: grant.level }, targetRef, type: 'page', ...(target?.title === undefined ? {} : { title: target.title }), ...(target?.url === undefined ? {} : { url: target.url }) }, + }; + } + + const requestTool: BrokerTool = { + name: 'browser.request_access', + description: 'Request user-approved browser control. Reuse a current approved target or request another tab. No browser authority exists until approval completes.', + inputSchema: { + type: 'object', + properties: { + level: { type: 'string', enum: [...accessLevels], description: 'Exact cumulative access level requested.' }, + navigation: { type: 'string', enum: [...(definition.navigation?.allowed ?? ['same-origin', 'follow-tab'])], description: 'Approved navigation scope; defaults to same-origin.' }, + targetRef: { type: 'string', description: 'Existing stable target reference to request.' }, + newTarget: { type: 'boolean', description: 'Request another approved target instead of reusing an existing grant.' }, + }, + required: ['level'], + additionalProperties: false, + }, + }; + + const descriptorClient = new Proxy({} as McpChromeDebuggerBridgeClient, { get() { + return () => { + throw new Error('Tool descriptors have no client authority.'); + }; + } }); + const descriptors = createCdbToolSession({ client: descriptorClient, enableRawCdp: true, ...(definition.automationProvider === undefined ? {} : { automationProvider: 'registered' }) }); + + async function requestAccess(peer: BrokerPeer, input: unknown, context?: CdbToolInvocationContext): Promise { + context?.signal.throwIfAborted(); + if (!object(input) || !accessLevels.includes(input.level as AccessLevel)) throw new BrokerError('ACCESS_DENIED', 'A valid browser access level is required.'); + const level = input.level as AccessLevel; + const navigation = input.navigation ?? definition.navigation?.default ?? 'same-origin'; + if ((navigation !== 'same-origin' && navigation !== 'follow-tab') || !(definition.navigation?.allowed ?? ['same-origin', 'follow-tab']).includes(navigation)) + throw new BrokerError('ACCESS_DENIED', 'The requested navigation policy is not permitted.'); + if (input.targetRef !== undefined && (typeof input.targetRef !== 'string' || input.newTarget === true)) throw new BrokerError('ACCESS_DENIED', 'targetRef and newTarget are mutually exclusive.'); + const session = await sessionFor(peer); + const requestedTargetId = typeof input.targetRef === 'string' ? session.tools.targetIdForReference(input.targetRef) : undefined; + if (input.targetRef !== undefined && requestedTargetId === undefined) throw new BrokerError('MCP_TARGET_REF_STALE', 'The target reference is no longer valid.'); + const candidates = grants().filter(grant => grant.principalId === session.principal.id && grant.state === 'active' + && (requestedTargetId === undefined || grant.targetId === requestedTargetId) + && accessLevels.indexOf(grant.level) >= accessLevels.indexOf(level) && (grant.navigation === 'follow-tab' || grant.navigation === navigation)); + const targetIds = new Set(candidates.map(grant => grant.targetId)); + if (input.newTarget !== true && targetIds.size === 1 && candidates[0] !== undefined) { + return accessResult(candidates[0], session); + } + if (input.newTarget !== true && targetIds.size > 1) throw new BrokerError('TARGET_AMBIGUOUS', 'Choose a targetRef or request a new target.', true); + if (grantCoordinator.inspect().some(state => state.request.logicalSessionId === session.logicalSessionId && state.request.state !== 'granted')) + throw new BrokerError('REQUEST_RATE_LIMITED', 'This browser session already has a pending request.', true); + const previous = lastRequests.get(session.principal.id); + if (previous !== undefined && timing.requestRateLimitMilliseconds !== null && Date.now() - previous < timing.requestRateLimitMilliseconds) + throw new BrokerError('REQUEST_RATE_LIMITED', 'Wait before requesting browser control again.', true, timing.requestRateLimitMilliseconds - (Date.now() - previous)); + lastRequests.set(session.principal.id, Date.now()); + const id = crypto.randomUUID(); + const pending = Promise.withResolvers(); + pendingRequests.set(id, pending); + const cancel = (): void => { + pending.reject(new BrokerError('MCP_ACTION_CANCELLED', 'The browser access request was cancelled.')); + void grantCoordinator.cancel(id).catch(() => {}); + }; + context?.signal.addEventListener('abort', cancel, { once: true }); + const outcome = pending.promise.finally(() => context?.signal.removeEventListener('abort', cancel)); + void outcome.catch(() => {}); + try { + await grantCoordinator.request({ + id, + principalId: session.principal.id, + logicalSessionId: session.logicalSessionId, + capabilities: { level }, + metadata: { navigation, createdAt: Date.now() }, + ...(requestedTargetId === undefined ? {} : { requestedTargetId }), + }); + } catch (error) { + pendingRequests.delete(id); + context?.signal.removeEventListener('abort', cancel); + throw error; + } + if (context?.signal.aborted) { + await grantCoordinator.cancel(id); + cancel(); + } + changed(); + return outcome; + } + + return { + brokerId: identityStore.brokerId, + toolProvider: { name: '@dvcol/cdb', version: packageManifest.version }, + connectSession, + snapshot, + tools: [requestTool, ...descriptors.definitions.map(({ name, description, inputSchema }) => ({ name, description, inputSchema }))], + subscribe(listener: (state: BrokerState) => void) { + ensureActive(); + listeners.add(listener); + return () => listeners.delete(listener); + }, + registerProvider(peer: BrokerPeer, registration: ProviderRegistration) { + ensureActive(); + if (!registration.id || !registration.instanceId || !accessLevels.includes(registration.maximumLevel)) throw new BrokerError('ACCESS_DENIED', 'Invalid provider registration.'); + assertProviderIdentity(registration); + registrations.set(peer.id, structuredClone(registration)); + changed(); + return { brokerId: identityStore.brokerId }; + }, + createPairingOffer(peer: BrokerPeer) { + ensureActive(); + const registration = registrations.get(peer.id); + if (registration === undefined) throw new BrokerError('ACCESS_DENIED', 'Register the provider before pairing.'); + return authentication.invite(peer.id, registration.instanceId); + }, + beginProviderAuthentication(peer: BrokerPeer, input: ProviderAuthenticationInput) { + const registration = registrations.get(peer.id); + if (registration === undefined) throw new BrokerError('ACCESS_DENIED', 'Register the provider before authenticating.'); + return authentication.begin(peer.id, registration.instanceId, input); + }, + async authenticateProvider(peer: BrokerPeer, proof: string, connection: AgentTargetConnection) { + const registration = registrations.get(peer.id); + if (registration === undefined) throw new BrokerError('ACCESS_DENIED', 'The provider registration expired.'); + const authenticated = await authentication.authenticate(peer.id, proof); + ensureActive(); + if (registrations.get(peer.id) !== registration) throw new BrokerError('ACCESS_DENIED', 'The provider registration was replaced.'); + assertProviderIdentity(registration); + const previous = providers.get(registration.id); + if (previous?.recoveryTimer !== undefined) clearTimeout(previous.recoveryTimer); + previous?.disconnect(); + previous?.connection.close?.(4000, 'Provider connection replaced'); + const authority = { principalId: registration.instanceId, connectionGeneration: authenticated.claims.connectionGeneration }; + const provider: Provider = { + registration, + peer, + generation: authority.connectionGeneration, + connection, + state: 'connecting', + disconnect: connectAgentTargetBroker(connection, targetBroker, { + authority, + connectionGeneration: authority.connectionGeneration, + revokeTargetsOnDisconnect: false, + connectionLimits: { maximumArtifactBytes: 16 * 1_024 * 1_024, maximumInlineResultBytes: 64 * 1_024, maximumMessageBytes: definition.maximumProviderMessageBytes ?? 64 * 1_024 * 1_024 }, + }), + }; + providers.set(registration.id, provider); + for (const session of sessions.values()) session.refreshTargets(); + void connection.closed?.then(() => { + provider.disconnect(); + recover(provider); + }); + if (definition.automationProvider !== undefined) { + try { + const automation = await definition.automationProvider(); + ensureActive(); + if (providers.get(registration.id) !== provider || provider.state !== 'connecting') + throw new BrokerError('PROVIDER_RECOVERING', 'The provider connection changed during automation setup.', true); + targetBroker.registerAutomationProvider(automation, authority); + } catch (error) { + provider.disconnect(); + connection.close?.(3001, 'Automation setup failed'); + recover(provider); + throw error; + } + } + changed(); + return { proof: authenticated.proof, claims: authenticated.claims, transcript: authenticated.transcript }; + }, + async reconcileProvider(peer: BrokerPeer, values: readonly ProviderTarget[]) { + const provider = [...providers.values()].find(candidate => candidate.peer.id === peer.id); + if (provider === undefined || (provider.state !== 'connecting' && provider.state !== 'ready')) throw new BrokerError('PROVIDER_RECOVERING', 'The provider connection is unavailable.', true); + const checked = checkedTargets(provider, values); + for (const target of checked) targetMetadata.set(target.id, target); + provider.state = 'ready'; + for (const session of sessions.values()) session.refreshTargets(); + for (const state of grantCoordinator.inspect()) { + if (state.request.state !== 'granted' || state.provider?.principalId !== provider.registration.instanceId) continue; + const selected = checked.filter(target => state.bindings.some(binding => binding.targetId === target.id)); + await reconcileScope(peer, state.request.id, selected); + } + changed(); + return snapshot(); + }, + claimRequest(peer: BrokerPeer, requestId: string) { + const provider = currentProvider(peer); + const request = grantCoordinator.getRequest(requestId); + if (request === undefined || accessLevels.indexOf(provider.registration.maximumLevel) < accessLevels.indexOf(request.capabilities.level ?? 'observe')) + throw new BrokerError('ACCESS_DENIED', 'This provider cannot satisfy the requested access level.'); + const claim = grantCoordinator.claim(requestId, providerAuthority(provider)); + return { claim, request: projectRequest(request) }; + }, + releaseClaim(peer: BrokerPeer, claim: GrantRequestClaim) { + const provider = currentProvider(peer); + if (claim.provider.principalId !== provider.registration.instanceId) throw new BrokerError('ACCESS_DENIED', 'The claim belongs to another provider.'); + grantCoordinator.release(claim); + }, + async completeClaim(peer: BrokerPeer, claim: GrantRequestClaim, values: readonly ProviderTarget[]) { + const provider = currentProvider(peer); + if (claim.provider.principalId !== provider.registration.instanceId) throw new BrokerError('ACCESS_DENIED', 'The claim belongs to another provider.'); + const request = grantCoordinator.getRequest(claim.requestId); + if (request === undefined) throw new BrokerError('GRANT_REVOKED', 'The grant request ended.'); + await grantCoordinator.complete(claim, bindingTargets(request, provider, values)); + await refreshSessions(); + const session = sessions.get(request.logicalSessionId); + const grant = grants().find(candidate => candidate.requestId === request.id); + if (session === undefined || grant === undefined) throw new BrokerError('GRANT_REVOKED', 'The requesting session ended before approval completed.'); + const pending = pendingRequests.get(request.id); + pendingRequests.delete(request.id); + try { + pending?.resolve(accessResult(grant, session)); + } catch (error) { + pending?.reject(error); + throw error; + } + changed(); + return snapshot(); + }, + reconcileScope, + async revokeScope(requestId: string) { + await grantCoordinator.cancel(requestId); + await refreshSessions(); + changed(); + }, + async revokeGrant(grantId: string) { + const grant = grants().find(candidate => candidate.id === grantId); + if (grant === undefined) return false; + await grantCoordinator.revokeBindings(grant.requestId, [grantId]); + await refreshSessions(); + changed(); + return true; + }, + async disconnectProvider(providerId: string, forgetPairing = false) { + const provider = providers.get(providerId); + if (provider === undefined) return false; + provider.state = 'disconnected'; + if (provider.recoveryTimer !== undefined) clearTimeout(provider.recoveryTimer); + provider.disconnect(); + provider.connection.close?.(4001, 'Provider disconnected'); + targetBroker.revokeAgentTargets(providerAuthority(provider), 'explicit'); + await cancelProviderRequests(provider); + if (forgetPairing) { + let credential = identityStore.findByAgentId(provider.registration.instanceId); + while (credential !== undefined) { + await identityStore.remove(credential.credentialId); + credential = identityStore.findByAgentId(provider.registration.instanceId); + } + } + changed(); + return true; + }, + async disconnectPeer(peerId: string) { + authentication.forget(peerId); + registrations.delete(peerId); + const provider = [...providers.values()].find(candidate => candidate.peer.id === peerId); + if (provider !== undefined) { + provider.disconnect(); + recover(provider); + } + const session = sessionsByPeer.get(peerId); + if (session !== undefined) { + sessionsByPeer.delete(peerId); + session.disconnect(); + for (const state of grantCoordinator.inspect()) { + if (state.request.logicalSessionId === session.logicalSessionId && state.request.state !== 'granted') await grantCoordinator.cancel(state.request.id); + } + await logicalSessions.disconnect(session.logicalSessionId, peerId); + } + changed(); + }, + async terminateSession(peer: BrokerPeer) { + const session = sessionsByPeer.get(peer.id); + if (session === undefined) return false; + await logicalSessions.terminate(session.logicalSessionId); + await authorityUpdates; + return true; + }, + async invoke(peer: BrokerPeer, name: string, input: unknown, context?: CdbToolInvocationContext): Promise { + if (name === 'browser.request_access') return requestAccess(peer, input, context); + const session = await sessionFor(peer); + if (object(input) && typeof input.targetRef === 'string') { + const targetId = session.tools.targetIdForReference(input.targetRef); + const grant = grants().find(candidate => candidate.principalId === session.principal.id && candidate.targetId === targetId && candidate.state === 'active') + ?? grants().find(candidate => candidate.principalId === session.principal.id && candidate.targetId === targetId); + if (grant?.state === 'out-of-scope') throw new BrokerError('TARGET_OUT_OF_SCOPE', 'The tab is outside this grant’s approved origin.', true); + if (grant?.state === 'recovering') throw new BrokerError('PROVIDER_RECOVERING', 'The browser provider is recovering.', true); + } + const tool = session.tools.definitions.find(candidate => candidate.name === name); + if (tool === undefined) throw new BrokerError('ACCESS_DENIED', 'Unknown browser tool.'); + const result = await tool.invoke(input, context); + const content = result.content.find(candidate => candidate.type === 'text'); + let value: unknown = content?.text; + if (typeof value === 'string') { + try { + value = JSON.parse(value); + } catch { /** Plain tool text remains text. */ } + } + if (result.isError === true) { + throw new BrokerError( + object(value) && typeof value.code === 'string' ? value.code : 'MCP_TOOL_FAILED', + object(value) && typeof value.message === 'string' ? value.message : String(value), + object(value) && value.retryable === true, + object(value) && typeof value.retryAfterMs === 'number' ? value.retryAfterMs : undefined, + object(value) && object(value.details) ? value.details : undefined, + ); + } + return value; + }, + async dispose() { + if (disposed) return; + disposed = true; + authentication.dispose(); + unsubscribeAuthority(); + unsubscribeRequests(); + for (const pending of pendingRequests.values()) pending.reject(new BrokerError('GRANT_REVOKED', 'The broker stopped.')); + pendingRequests.clear(); + for (const provider of providers.values()) { + if (provider.recoveryTimer !== undefined) clearTimeout(provider.recoveryTimer); + provider.disconnect(); + provider.connection.close?.(1001, 'Broker stopped'); + } + for (const session of sessions.values()) session.dispose(); + await grantCoordinator.dispose(); + await Promise.allSettled(membershipUpdates.values()); + await Promise.all(Array.from(sessions.keys(), async id => logicalSessions.terminate(id))); + await authorityUpdates; + logicalSessions.dispose(); + descriptors.dispose(); + targetBroker.dispose(); + sessions.clear(); + sessionsByPeer.clear(); + listeners.clear(); + providers.clear(); + registrations.clear(); + targetMetadata.clear(); + }, + }; +} diff --git a/packages/broker/src/session-client.ts b/packages/broker/src/session-client.ts new file mode 100644 index 0000000..cf43fee --- /dev/null +++ b/packages/broker/src/session-client.ts @@ -0,0 +1,158 @@ +import type { AuthorityRecord, AuthorityStore, TargetBroker } from '@dvcol/cdb'; +import type { CdbToolSession, McpChromeDebuggerBridgeClient } from '@dvcol/cdb-mcp'; +import type { ClientAuthority } from '@dvcol/cdb/broker'; + +import type { BrokerDefinition } from './config.js'; +import type { BrokerPeer, BrokerPrincipal } from './contract.js'; + +import { createCdbToolSession } from '@dvcol/cdb-mcp'; + +import { allowsNavigation } from './config.js'; +import { BrokerError } from './error.js'; + +export interface SessionContext { + readonly definition: BrokerDefinition; + readonly authorityStore: AuthorityStore; + readonly targetBroker: TargetBroker; + changed: () => void; + isProviderReady: (providerId: string) => boolean; + isRequestGranted: (requestId: string) => boolean; +} + +export interface BrokerSession { + readonly authority: ClientAuthority; + readonly client: McpChromeDebuggerBridgeClient; + readonly logicalSessionId: string; + readonly principal: BrokerPrincipal; + readonly tools: CdbToolSession; + readonly bindings: AuthorityRecord['bindings']; + readonly peer: BrokerPeer | undefined; + bind: (peer: BrokerPeer) => void; + disconnect: () => void; + dispose: () => void; + refresh: () => Promise; + refreshTargets: () => void; +} + +/** Connection and client resources; live grant authority remains in the supplied store. */ +export function createBrokerSession(context: SessionContext, principal: BrokerPrincipal, initialRecord: AuthorityRecord): BrokerSession { + let record: AuthorityRecord | undefined = initialRecord; + let peer: BrokerPeer | undefined; + let disposed = false; + let refreshed = Promise.resolve(); + const logicalSessionId = initialRecord.logicalSessionId; + + function bindingAllowed(binding: AuthorityRecord['bindings'][number]): boolean { + const metadata = binding.metadata; + if (metadata == null || typeof metadata !== 'object' || Array.isArray(metadata)) return false; + if (typeof metadata.approvedOrigin !== 'string' || typeof metadata.providerId !== 'string' || typeof metadata.requestId !== 'string' || !context.isRequestGranted(metadata.requestId) + || (metadata.navigation !== 'same-origin' && metadata.navigation !== 'follow-tab')) return false; + const target = context.targetBroker.listTargets().find(candidate => candidate.id === binding.targetId); + return context.isProviderReady(metadata.providerId) && target?.generation === binding.targetGeneration + && (binding.expiresAt == null || Date.parse(binding.expiresAt) > Date.now()) + && allowsNavigation(context.definition, { + approvedOrigin: metadata.approvedOrigin, + navigation: metadata.navigation, + principalId: principal.id, + targetId: binding.targetId, + url: target.url, + }); + } + + const authority: ClientAuthority = { + get authorityAvailable() { + return !disposed && record !== undefined && peer !== undefined && record.activeConnectionId === peer.id; + }, + get connectionId() { + return peer?.id ?? `disconnected:${logicalSessionId}`; + }, + displayName: principal.label, + logicalSessionId, + principalId: principal.id, + get targetGrants() { + return record?.bindings.filter(bindingAllowed) ?? []; + }, + }; + + async function refresh(): Promise { + refreshed = refreshed.then(async () => { + if (disposed) return; + try { + record = await context.authorityStore.get(logicalSessionId); + } catch { + record = undefined; + } + context.targetBroker.refreshClientAuthority(authority); + }); + return refreshed; + } + + async function authorized(operation: (authority: ClientAuthority) => Result | Promise): Promise { + await refresh(); + if (!authority.authorityAvailable) throw new BrokerError('GRANT_REVOKED', 'The browser-control session is disconnected or fenced.'); + return operation(authority); + } + + const client: McpChromeDebuggerBridgeClient = { + acquireLease: async request => authorized((authority) => { + const lease = context.targetBroker.acquireLease(request, authority); + context.changed(); + return lease; + }), + cancelAutomation: async request => authorized(authority => context.targetBroker.cancelAutomation(request.operationId, authority)), + cancelCommand: async request => authorized(authority => context.targetBroker.cancelCommand(request.operationId, authority)), + executeAutomation: async request => authorized(async authority => context.targetBroker.executeAutomation(request, authority)), + executeCommand: async request => authorized(async authority => context.targetBroker.executeCommand(request, authority)), + listTargets: async () => authorized(authority => context.targetBroker.listTargets(authority)), + readArtifact: async request => authorized(authority => context.targetBroker.readArtifact(request, authority)), + releaseArtifact: async request => authorized(authority => context.targetBroker.releaseArtifact(request, authority)), + releaseLease: async request => authorized((authority) => { + context.targetBroker.releaseLease(request, authority); + context.changed(); + }), + renewLease: async request => authorized(authority => context.targetBroker.renewLease(request, authority)), + subscribe: async request => authorized(async authority => context.targetBroker.subscribe(request, authority)), + watchTargets: () => context.targetBroker.watchTargets(authority), + }; + const tools = createCdbToolSession({ + client, + enableRawCdp: true, + ...(context.definition.automationProvider === undefined ? {} : { automationProvider: 'registered' }), + }); + + return { + authority, + client, + logicalSessionId, + principal, + tools, + get bindings() { + return record?.bindings ?? []; + }, + get peer() { + return peer; + }, + bind(nextPeer: BrokerPeer) { + if (peer !== undefined) context.targetBroker.disconnectClient(authority); + peer = nextPeer; + tools.rebindClient(client); + context.targetBroker.connectClient(authority); + }, + disconnect() { + context.targetBroker.disconnectClient(authority); + peer = undefined; + tools.rebindClient(client); + }, + dispose() { + if (disposed) return; + context.targetBroker.disconnectClient(authority); + disposed = true; + peer = undefined; + tools.dispose(); + }, + refresh, + refreshTargets() { + context.targetBroker.refreshClientAuthority(authority); + }, + }; +} diff --git a/packages/broker/test/identity-store.test.ts b/packages/broker/test/identity-store.test.ts new file mode 100644 index 0000000..599168b --- /dev/null +++ b/packages/broker/test/identity-store.test.ts @@ -0,0 +1,101 @@ +import type { BrokerCredential } from '../src/contract.js'; + +import { Buffer } from 'node:buffer'; +import { mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises'; +import * as filesystem from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +import { afterEach, expect, it, vi } from 'vitest'; + +import { defineBroker } from '../src/config.js'; +import { createFileBrokerIdentityStore, createMemoryBrokerIdentityStore } from '../src/identity-store.js'; + +vi.mock('node:fs/promises', async (importOriginal) => { + const original = await importOriginal(); + return { ...original, rename: vi.fn(original.rename) }; +}); + +const directories: string[] = []; +afterEach(async () => { + vi.restoreAllMocks(); + await Promise.all(directories.splice(0).map(async directory => rm(directory, { recursive: true, force: true }))); +}); + +function credential(brokerId: string): BrokerCredential { + return { brokerId, agentId: crypto.randomUUID(), principalId: crypto.randomUUID(), credentialId: crypto.randomUUID(), credential: crypto.getRandomValues(new Uint8Array(32)), status: 'pending' }; +} + +it('preserves a version-one identity and pairing across adoption and restart', async () => { + expect.assertions(5); + const directory = await mkdtemp(join(tmpdir(), 'cdb-identity-')); + directories.push(directory); + const stored = credential(crypto.randomUUID()); + const { brokerId, status: _status, credential: secret, ...identity } = stored; + await writeFile(join(directory, 'identity.json'), JSON.stringify({ version: 1, brokerId, credentials: [{ ...identity, credential: Buffer.from(secret).toString('base64url') }] })); + const store = await createFileBrokerIdentityStore(directory); + expect(store.brokerId).toBe(brokerId); + expect(store.load(stored.credentialId)).toEqual({ ...stored, status: 'active' }); + expect((await stat(join(directory, 'identity.json'))).mode & 0o777).toBe(0o600); + await store.remove(stored.credentialId); + expect((await createFileBrokerIdentityStore(directory)).findByAgentId(stored.agentId)).toBeUndefined(); + expect(JSON.parse(await readFile(join(directory, 'identity.json'), 'utf8'))).toMatchObject({ brokerId, credentials: [] }); +}); + +it('preserves multiple existing credentials for one provider when activating and restarting', async () => { + expect.assertions(4); + const directory = await mkdtemp(join(tmpdir(), 'cdb-identity-')); + directories.push(directory); + const first = credential(crypto.randomUUID()); + const second = { ...credential(first.brokerId), agentId: first.agentId, principalId: first.principalId }; + await writeFile(join(directory, 'identity.json'), JSON.stringify({ + version: 1, + brokerId: first.brokerId, + credentials: [first, second].map(({ brokerId: _brokerId, status: _status, credential: secret, ...identity }) => ({ + ...identity, + credential: Buffer.from(secret).toString('base64url'), + })), + })); + const store = await createFileBrokerIdentityStore(directory); + expect(store.load(first.credentialId)).toEqual({ ...first, status: 'active' }); + expect(store.load(second.credentialId)).toEqual({ ...second, status: 'active' }); + await store.activate({ ...second, status: 'active' }); + const restarted = await createFileBrokerIdentityStore(directory); + expect(restarted.load(first.credentialId)).toEqual({ ...first, status: 'active' }); + expect(restarted.load(second.credentialId)).toEqual({ ...second, status: 'active' }); +}); + +it('keeps credentials uncommitted after a persistence failure and recovers for a later write', async () => { + expect.assertions(4); + const directory = await mkdtemp(join(tmpdir(), 'cdb-identity-')); + directories.push(directory); + const store = await createFileBrokerIdentityStore(directory); + const record = credential(store.brokerId); + vi.spyOn(filesystem, 'rename').mockRejectedValueOnce(new Error('Storage unavailable')); + await expect(store.activate(record)).rejects.toThrow('Storage unavailable'); + expect(store.load(record.credentialId)).toBeUndefined(); + expect((await createFileBrokerIdentityStore(directory)).load(record.credentialId)).toBeUndefined(); + await store.activate(record); + expect((await createFileBrokerIdentityStore(directory)).load(record.credentialId)).toEqual({ ...record, status: 'active' }); +}); + +it('serializes concurrent pairing without replacing the first committed credential', async () => { + expect.assertions(3); + const store = createMemoryBrokerIdentityStore(); + const first = credential(store.brokerId); + const second = { ...credential(store.brokerId), agentId: first.agentId }; + const results = await Promise.allSettled([store.activate(first), store.activate(second)]); + expect(results.map(result => result.status)).toEqual(['fulfilled', 'rejected']); + expect(store.findByAgentId(first.agentId)?.credentialId).toBe(first.credentialId); + expect(store.load(second.credentialId)).toBeUndefined(); +}); + +it('defines navigation restrictions without constructing a runtime', () => { + expect.assertions(4); + const createAutomationProvider = vi.fn(); + const configuration = defineBroker({ navigation: { default: 'same-origin', allowed: ['same-origin'] }, automationProvider: createAutomationProvider }); + expect(configuration.navigation.default).toBe('same-origin'); + expect(createAutomationProvider).not.toHaveBeenCalled(); + expect(() => defineBroker({ navigation: { default: 'follow-tab', allowed: ['same-origin'] } })).toThrow(TypeError); + expect(() => defineBroker({ timing: { providerRecoveryMilliseconds: -1 } })).toThrow(TypeError); +}); diff --git a/packages/broker/test/provider-authentication.test.ts b/packages/broker/test/provider-authentication.test.ts new file mode 100644 index 0000000..aa74a57 --- /dev/null +++ b/packages/broker/test/provider-authentication.test.ts @@ -0,0 +1,64 @@ +import { createAgentAuthenticationProof, decodeBase64UrlBytes, generateRandomBase64Url, importAgentCredential } from '@dvcol/cdb/authentication'; +import { expect, it } from 'vitest'; + +import { createMemoryBrokerIdentityStore } from '../src/identity-store.js'; +import { createProviderAuthentication } from '../src/provider-authentication.js'; + +it('binds pairing and proofs to their peer and retains the installation identity on reconnect', async () => { + expect.assertions(6); + const store = createMemoryBrokerIdentityStore(); + const authentication = createProviderAuthentication(store, 60_000); + const offer = authentication.invite('first-peer', 'installation'); + const credential = generateRandomBase64Url(32); + const input = { credentialId: 'credential', clientNonce: generateRandomBase64Url(32), pairing: { code: offer.code, credential } }; + expect(() => authentication.begin('another-peer', 'installation', input)).toThrow('invitation'); + const transcript = authentication.begin('first-peer', 'installation', input); + const key = await importAgentCredential(decodeBase64UrlBytes(credential)); + const proof = await createAgentAuthenticationProof(key, transcript); + expect(store.findByAgentId('installation')).toBeUndefined(); + const authenticated = await authentication.authenticate('first-peer', proof); + expect(authenticated.claims).toEqual({ principalId: 'installation', connectionGeneration: 1 }); + await expect(authentication.authenticate('first-peer', proof)).rejects.toThrow('No current'); + const next = authentication.begin('second-peer', 'installation', { credentialId: input.credentialId, clientNonce: generateRandomBase64Url(32) }); + const reconnected = await authentication.authenticate('second-peer', await createAgentAuthenticationProof(key, next)); + expect(reconnected.claims).toEqual({ principalId: 'installation', connectionGeneration: 2 }); + expect(store.findByAgentId('installation')?.credentialId).toBe(input.credentialId); + authentication.dispose(); +}); + +it('authenticates either retained pairing for one installation and revokes only the selected credential', async () => { + expect.assertions(4); + const store = createMemoryBrokerIdentityStore(); + const authentication = createProviderAuthentication(store, 60_000); + const credentials = ['first', 'second'].map(credentialId => ({ credentialId, credential: crypto.getRandomValues(new Uint8Array(32)) })); + for (const credential of credentials) { + await store.activate({ ...credential, agentId: 'installation', brokerId: store.brokerId, principalId: 'installation', status: 'active' }); + const transcript = authentication.begin(credential.credentialId, 'installation', { credentialId: credential.credentialId, clientNonce: generateRandomBase64Url(32) }); + const proof = await createAgentAuthenticationProof(await importAgentCredential(credential.credential), transcript); + expect((await authentication.authenticate(credential.credentialId, proof)).claims.principalId).toBe('installation'); + } + await store.remove('first'); + expect(() => authentication.begin('first', 'installation', { credentialId: 'first', clientNonce: generateRandomBase64Url(32) })).toThrow('not recognized'); + const remaining = credentials[1]!; + const transcript = authentication.begin('second', 'installation', { credentialId: 'second', clientNonce: generateRandomBase64Url(32) }); + const proof = await createAgentAuthenticationProof(await importAgentCredential(remaining.credential), transcript); + expect((await authentication.authenticate('second', proof)).claims.connectionGeneration).toBe(3); + authentication.dispose(); +}); + +it('rejects a stale challenge and a proof after pairing revocation', async () => { + expect.assertions(2); + const store = createMemoryBrokerIdentityStore(); + const credential = generateRandomBase64Url(32); + await store.activate({ agentId: 'installation', brokerId: store.brokerId, credential: decodeBase64UrlBytes(credential), credentialId: 'credential', principalId: 'installation', status: 'pending' }); + const authentication = createProviderAuthentication(store, 60_000); + const key = await importAgentCredential(decodeBase64UrlBytes(credential)); + const input = { credentialId: 'credential', clientNonce: generateRandomBase64Url(32) }; + const stale = authentication.begin('peer', 'installation', input); + authentication.begin('peer', 'installation', { ...input, clientNonce: generateRandomBase64Url(32) }); + await expect(authentication.authenticate('peer', await createAgentAuthenticationProof(key, stale))).rejects.toThrow('authentication failed'); + const current = authentication.begin('peer', 'installation', input); + await store.remove('credential'); + await expect(authentication.authenticate('peer', await createAgentAuthenticationProof(key, current))).rejects.toThrow('revoked'); + authentication.dispose(); +}); diff --git a/packages/broker/test/runtime.test.ts b/packages/broker/test/runtime.test.ts new file mode 100644 index 0000000..bdff42f --- /dev/null +++ b/packages/broker/test/runtime.test.ts @@ -0,0 +1,146 @@ +import type { AgentToBrokerMessage, BrokerToAgentMessage, PublishedTarget } from '@dvcol/cdb'; + +import type { BrokerRuntime } from '../src/runtime.js'; + +import { createAgentAuthenticationProof, decodeBase64UrlBytes, generateRandomBase64Url, importAgentCredential } from '@dvcol/cdb/authentication'; +import { afterEach, describe, expect, it } from 'vitest'; + +import { createMemoryBrokerIdentityStore } from '../src/identity-store.js'; +import { createBroker } from '../src/runtime.js'; + +const brokers: BrokerRuntime[] = []; +afterEach(async () => { + await Promise.all(brokers.splice(0).map(async broker => broker.dispose())); +}); + +async function fixture() { + const identityStore = createMemoryBrokerIdentityStore(); + const broker = await createBroker({ identityStore, timing: { requestRateLimitMilliseconds: 0 } }); + brokers.push(broker); + const peer = { id: 'provider-peer' }; + const registration = { id: 'test-provider', instanceId: crypto.randomUUID(), name: 'Public test provider', version: '1.0.0', maximumLevel: 'debug' as const }; + broker.registerProvider(peer, registration); + const invitation = broker.createPairingOffer(peer); + const credential = generateRandomBase64Url(32); + const transcript = broker.beginProviderAuthentication(peer, { credentialId: 'credential', clientNonce: generateRandomBase64Url(32), pairing: { code: invitation.code, credential } }); + const key = await importAgentCredential(decodeBase64UrlBytes(credential)); + const proof = await createAgentAuthenticationProof(key, transcript); + const listeners = new Set<(message: AgentToBrokerMessage) => void>(); + const received: BrokerToAgentMessage[] = []; + const closed = Promise.withResolvers(); + const authenticated = await broker.authenticateProvider(peer, proof, { + closed: closed.promise, + close: () => closed.resolve(), + onMessage(listener) { + listeners.add(listener); + return () => listeners.delete(listener); + }, + async send(message) { + received.push(message); + }, + }); + const send = (message: AgentToBrokerMessage): void => { + for (const listener of listeners) listener(message); + }; + send({ kind: 'request', method: 'agent.hello', protocolVersion: 1, requestId: crypto.randomUUID(), parameters: { + connectionGeneration: authenticated.claims.connectionGeneration, + implementation: { instanceId: registration.instanceId, name: registration.name, role: 'agent', version: registration.version }, + protocolVersions: { minimum: 1, maximum: 1 }, + features: [], + heartbeat: { intervalMilliseconds: 15_000, timeoutMilliseconds: 45_000 }, + limits: { maximumArtifactBytes: 16_777_216, maximumInlineResultBytes: 65_536, maximumMessageBytes: 67_108_864 }, + } }); + const target: PublishedTarget = { id: crypto.randomUUID(), generation: 1, scopeId: crypto.randomUUID(), availability: 'available', capabilities: { level: 'debug' }, type: 'page', url: 'https://example.test/start', title: 'Public fixture' }; + send({ kind: 'notification', method: 'targets.publish', protocolVersion: 1, parameters: { target } }); + await broker.reconcileProvider(peer, [target]); + return { broker, identityStore, peer, received, registration, send, target }; +} + +async function approve(setup: Awaited>, principalId: string, navigation: 'same-origin' | 'follow-tab' = 'same-origin') { + const waiting = setup.broker.invoke({ id: principalId }, 'browser.request_access', { level: 'interact', navigation }); + const outcome = waiting.then(value => ({ value }), (error: unknown) => ({ error })); + await expect.poll(() => setup.broker.snapshot().requests.length).toBe(1); + const request = setup.broker.snapshot().requests[0]!; + const { claim } = setup.broker.claimRequest(setup.peer, request.id); + await setup.broker.completeClaim(setup.peer, claim, [setup.target]); + return { claim, outcome: await outcome, request }; +} + +describe('composed browser broker', () => { + it('forgets every retained credential for a provider installation', async () => { + expect.assertions(4); + const setup = await fixture(); + const credential = setup.identityStore.load('credential')!; + await setup.identityStore.activate({ ...credential, credentialId: 'another-existing-credential' }); + expect(await setup.broker.disconnectProvider(setup.registration.id, true)).toBe(true); + expect(setup.identityStore.load('credential')).toBeUndefined(); + expect(setup.identityStore.load('another-existing-credential')).toBeUndefined(); + expect(setup.broker.createPairingOffer(setup.peer).brokerId).toBe(setup.broker.brokerId); + }); + + it('serializes concurrent membership publications for one approved scope', async () => { + expect.assertions(4); + const setup = await fixture(); + const approved = await approve(setup, 'member-agent'); + await expect(Promise.all([ + setup.broker.reconcileScope(setup.peer, approved.request.id, [setup.target]), + setup.broker.reconcileScope(setup.peer, approved.request.id, [setup.target]), + ])).resolves.toEqual([undefined, undefined]); + expect(setup.broker.snapshot().grants).toHaveLength(1); + expect(await setup.broker.invoke({ id: 'member-agent' }, 'browser.list_targets', {})).toMatchObject([{ targetRef: 't1' }]); + }); + + it('isolates principals and rejects replayed approval claims', async () => { + expect.assertions(6); + const setup = await fixture(); + const approved = await approve(setup, 'first-agent'); + expect(approved.outcome).toMatchObject({ value: { target: { targetRef: 't1' } } }); + expect(await setup.broker.invoke({ id: 'first-agent' }, 'browser.list_targets', {})).toMatchObject([{ targetRef: 't1' }]); + expect(await setup.broker.invoke({ id: 'second-agent' }, 'browser.list_targets', {})).toEqual([]); + await expect(setup.broker.completeClaim(setup.peer, approved.claim, [setup.target])).rejects.toMatchObject({ code: 'GRANT_CLAIM_INVALID' }); + expect(setup.broker.snapshot().grants).toHaveLength(1); + }); + + it('applies navigation policy per principal across generation replacement', async () => { + expect.assertions(9); + const setup = await fixture(); + await approve(setup, 'same-origin-agent'); + await approve(setup, 'follow-tab-agent', 'follow-tab'); + const navigated = { ...setup.target, generation: 2, url: 'https://other.test/next' }; + setup.send({ kind: 'notification', method: 'targets.publish', protocolVersion: 1, parameters: { target: navigated } }); + await setup.broker.reconcileProvider(setup.peer, [navigated]); + expect(setup.broker.snapshot().grants.map(grant => grant.state).sort()).toEqual(['active', 'out-of-scope']); + await expect(setup.broker.invoke({ id: 'same-origin-agent' }, 'browser.snapshot', { targetRef: 't1' })).rejects.toMatchObject({ code: 'TARGET_OUT_OF_SCOPE' }); + expect(await setup.broker.invoke({ id: 'follow-tab-agent' }, 'browser.list_targets', {})).toMatchObject([{ targetRef: 't1', url: navigated.url }]); + const firstGrant = setup.broker.snapshot().grants[0]!; + expect(await setup.broker.revokeGrant(firstGrant.id)).toBe(true); + expect(setup.broker.snapshot().grants).toHaveLength(1); + expect(setup.broker.snapshot().scopes).toHaveLength(2); + await setup.broker.reconcileScope(setup.peer, firstGrant.requestId, [navigated]); + expect(setup.broker.snapshot().grants).toHaveLength(1); + }); + + it('fences the previous peer on resume and preserves stable target references', async () => { + expect.assertions(5); + const setup = await fixture(); + const credential = await setup.broker.connectSession({ id: 'original-peer' }); + await approve(setup, 'original-peer'); + await setup.broker.connectSession({ id: 'resumed-peer' }, { resume: { logicalSessionId: credential.logicalSessionId, credential: credential.resumeCredential } }); + await expect(setup.broker.invoke({ id: 'original-peer' }, 'browser.list_targets', {})).rejects.toMatchObject({ code: 'ACCESS_DENIED' }); + expect(await setup.broker.invoke({ id: 'resumed-peer' }, 'browser.list_targets', {})).toMatchObject([{ targetRef: 't1' }]); + await expect(setup.broker.connectSession({ id: 'replayed-peer' }, { resume: { logicalSessionId: credential.logicalSessionId, credential: credential.resumeCredential } })).rejects.toBeDefined(); + expect(setup.broker.snapshot().principals).toHaveLength(1); + }); + + it('removes only the revoked grant during provider recovery', async () => { + expect.assertions(6); + const setup = await fixture(); + await approve(setup, 'first-agent'); + await approve(setup, 'second-agent'); + await setup.broker.disconnectPeer(setup.peer.id); + expect(setup.broker.snapshot().grants.map(grant => grant.state)).toEqual(['recovering', 'recovering']); + expect(await setup.broker.revokeGrant(setup.broker.snapshot().grants[0]!.id)).toBe(true); + expect(setup.broker.snapshot().grants).toHaveLength(1); + await expect(setup.broker.invoke({ id: 'second-agent' }, 'browser.snapshot', { targetRef: 't1' })).rejects.toMatchObject({ code: 'PROVIDER_RECOVERING' }); + }); +}); diff --git a/packages/broker/tsconfig.json b/packages/broker/tsconfig.json new file mode 100644 index 0000000..1e5a4d0 --- /dev/null +++ b/packages/broker/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "lib": [ + "ES2024", + "DOM" + ], + "types": [ + "node" + ] + }, + "include": [ + "src/**/*.ts" + ] +} diff --git a/packages/broker/tsdown.config.ts b/packages/broker/tsdown.config.ts new file mode 100644 index 0000000..7bc5ea3 --- /dev/null +++ b/packages/broker/tsdown.config.ts @@ -0,0 +1,3 @@ +import { defineConfig } from 'tsdown'; + +export default defineConfig({ deps: { neverBundle: true }, dts: { sourcemap: true }, entry: ['src/index.ts', 'src/contract.ts'], format: 'esm', platform: 'node', sourcemap: true, target: 'node24' }); diff --git a/packages/broker/vitest.config.ts b/packages/broker/vitest.config.ts new file mode 100644 index 0000000..ecd277c --- /dev/null +++ b/packages/broker/vitest.config.ts @@ -0,0 +1,3 @@ +import { defineProject } from 'vitest/config'; + +export default defineProject({ test: { environment: 'node', include: ['test/**/*.test.ts'], name: 'unit-node:broker' } }); diff --git a/packages/core/package.json b/packages/core/package.json index 54d9873..233dd8d 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -76,6 +76,10 @@ "./timing": { "types": "./dist/timing.d.ts", "import": "./dist/timing.js" + }, + "./authentication": { + "types": "./dist/authentication.d.ts", + "import": "./dist/authentication.js" } }, "files": [ diff --git a/packages/core/src/authentication.ts b/packages/core/src/authentication.ts new file mode 100644 index 0000000..d05bd95 --- /dev/null +++ b/packages/core/src/authentication.ts @@ -0,0 +1,277 @@ +export const agentAuthenticationProtocol = 'chrome-debugger-bridge.agent.v1'; + +const authenticationDomain = 'chrome-debugger-bridge/auth/v1'; +const authenticatedFrameDomain = 'chrome-debugger-bridge/frame/v1'; +const base64PaddingPattern = /=+$/u; +const textEncoder = new TextEncoder(); + +export type AuthenticatedFrameDirection = 'agent-to-broker' | 'broker-to-agent'; + +export interface AgentAuthenticationTranscript { + readonly agentId: string; + readonly brokerId: string; + readonly clientNonce: string; + readonly connectionId: string; + readonly credentialId: string; + readonly endpointPath: string; + readonly expiresAt: string; + readonly origin: string; + readonly protocolVersion: number; + /** Separate authentication transcripts for transports sharing a pairing credential. */ + readonly transportProtocol?: string; + readonly serverNonce: string; +} + +export interface AuthenticatedFrame { + readonly authenticationCode: string; + readonly connectionId: string; + readonly direction: AuthenticatedFrameDirection; + readonly kind: 'authenticated'; + readonly payload: string; + readonly sequence: number; +} + +export interface BrokerAuthenticationClaims { + readonly connectionGeneration: number; + readonly principalId: string; +} + +function encodeBase64UrlBytes(bytes: Uint8Array): string { + let binary = ''; + for (const byte of bytes) { + binary += String.fromCharCode(byte); + } + return globalThis.btoa(binary).replaceAll('+', '-').replaceAll('/', '_').replace(base64PaddingPattern, ''); +} + +export function decodeBase64UrlBytes(value: string): Uint8Array { + const paddingLength = (4 - (value.length % 4)) % 4; + const base64 = `${value.replaceAll('-', '+').replaceAll('_', '/')}${'='.repeat(paddingLength)}`; + const binary = globalThis.atob(base64); + const bytes = Uint8Array.from(binary, character => character.charCodeAt(0)); + if (bytes.byteLength !== 32 || encodeBase64UrlBytes(bytes) !== value) { + throw new Error('Expected a canonical 32-byte base64url value'); + } + return bytes; +} + +export function generateRandomBase64Url(byteLength: number): string { + const bytes = new Uint8Array(byteLength); + globalThis.crypto.getRandomValues(bytes); + return encodeBase64UrlBytes(bytes); +} + +export function createRandomIdentifier(): string { + return globalThis.crypto.randomUUID(); +} + +export async function importAgentCredential(credential: Uint8Array): Promise { + if (credential.byteLength !== 32) { + throw new Error('Agent credentials must contain exactly 32 bytes'); + } + return globalThis.crypto.subtle.importKey('raw', Uint8Array.from(credential), 'HKDF', false, ['deriveKey']); +} + +function createBaseAuthenticationTranscript(transcript: AgentAuthenticationTranscript): readonly unknown[] { + return [ + authenticationDomain, + transcript.transportProtocol ?? agentAuthenticationProtocol, + 'agent', + transcript.endpointPath, + transcript.origin, + transcript.brokerId, + transcript.agentId, + transcript.credentialId, + transcript.connectionId, + transcript.protocolVersion, + transcript.clientNonce, + transcript.serverNonce, + transcript.expiresAt, + ]; +} + +function createAuthenticationTranscriptBytes( + transcript: AgentAuthenticationTranscript, + proofDirection: 'agent' | 'broker', + brokerClaims?: BrokerAuthenticationClaims, +): Uint8Array { + /** A fixed primitive tuple is already in RFC 8785 canonical order and avoids object-key ambiguity. */ + return textEncoder.encode(JSON.stringify([ + ...createBaseAuthenticationTranscript(transcript), + proofDirection, + ...(brokerClaims === undefined ? [] : [brokerClaims.connectionGeneration, brokerClaims.principalId]), + ])); +} + +async function createKeyDerivationSalt(transcript: AgentAuthenticationTranscript): Promise { + return globalThis.crypto.subtle.digest( + 'SHA-256', + textEncoder.encode(JSON.stringify(createBaseAuthenticationTranscript(transcript))), + ); +} + +async function deriveAuthenticationKey( + credential: CryptoKey, + transcript: AgentAuthenticationTranscript, + purpose: 'agent' | 'broker' | AuthenticatedFrameDirection, +): Promise { + return globalThis.crypto.subtle.deriveKey( + { + hash: 'SHA-256', + info: textEncoder.encode(`${authenticationDomain}/${purpose}`), + name: 'HKDF', + salt: await createKeyDerivationSalt(transcript), + }, + credential, + { hash: 'SHA-256', length: 256, name: 'HMAC' }, + false, + ['sign', 'verify'], + ); +} + +export async function createAgentAuthenticationProof( + credential: CryptoKey, + transcript: AgentAuthenticationTranscript, +): Promise { + const proofKey = await deriveAuthenticationKey(credential, transcript, 'agent'); + const proof = await globalThis.crypto.subtle.sign( + 'HMAC', + proofKey, + createAuthenticationTranscriptBytes(transcript, 'agent'), + ); + return encodeBase64UrlBytes(new Uint8Array(proof)); +} + +export async function verifyAgentAuthenticationProof( + credential: CryptoKey, + transcript: AgentAuthenticationTranscript, + proof: string, +): Promise { + const proofKey = await deriveAuthenticationKey(credential, transcript, 'agent'); + return globalThis.crypto.subtle.verify( + 'HMAC', + proofKey, + decodeBase64UrlBytes(proof), + createAuthenticationTranscriptBytes(transcript, 'agent'), + ); +} + +export async function createBrokerAuthenticationProof( + credential: CryptoKey, + transcript: AgentAuthenticationTranscript, + claims: BrokerAuthenticationClaims, +): Promise { + const proofKey = await deriveAuthenticationKey(credential, transcript, 'broker'); + const proof = await globalThis.crypto.subtle.sign( + 'HMAC', + proofKey, + createAuthenticationTranscriptBytes(transcript, 'broker', claims), + ); + return encodeBase64UrlBytes(new Uint8Array(proof)); +} + +export async function verifyBrokerAuthenticationProof( + credential: CryptoKey, + transcript: AgentAuthenticationTranscript, + claims: BrokerAuthenticationClaims, + proof: string, +): Promise { + const proofKey = await deriveAuthenticationKey(credential, transcript, 'broker'); + return globalThis.crypto.subtle.verify( + 'HMAC', + proofKey, + decodeBase64UrlBytes(proof), + createAuthenticationTranscriptBytes(transcript, 'broker', claims), + ); +} + +function createAuthenticatedFrameBytes( + frame: Omit, +): Uint8Array { + return textEncoder.encode(JSON.stringify([ + authenticatedFrameDomain, + frame.connectionId, + frame.direction, + frame.sequence, + frame.payload, + ])); +} + +export async function createAuthenticatedFrame( + credential: CryptoKey, + transcript: AgentAuthenticationTranscript, + direction: AuthenticatedFrameDirection, + sequence: number, + payload: unknown, +): Promise { + const frameWithoutAuthenticationCode = { + connectionId: transcript.connectionId, + direction, + payload: JSON.stringify(payload), + sequence, + }; + const sessionKey = await deriveAuthenticationKey(credential, transcript, direction); + const authenticationCode = await globalThis.crypto.subtle.sign( + 'HMAC', + sessionKey, + createAuthenticatedFrameBytes(frameWithoutAuthenticationCode), + ); + return { + authenticationCode: encodeBase64UrlBytes(new Uint8Array(authenticationCode)), + ...frameWithoutAuthenticationCode, + kind: 'authenticated', + }; +} + +export async function openAuthenticatedFrame( + credential: CryptoKey, + transcript: AgentAuthenticationTranscript, + expectedDirection: AuthenticatedFrameDirection, + expectedSequence: number, + frameValue: unknown, +): Promise { + const frame = parseAuthenticatedFrame(frameValue); + if ( + frame.connectionId !== transcript.connectionId + || frame.direction !== expectedDirection + || frame.sequence !== expectedSequence + ) { + throw new Error('Authenticated frame context is invalid'); + } + + const sessionKey = await deriveAuthenticationKey(credential, transcript, expectedDirection); + const valid = await globalThis.crypto.subtle.verify( + 'HMAC', + sessionKey, + decodeBase64UrlBytes(frame.authenticationCode), + createAuthenticatedFrameBytes(frame), + ); + if (!valid) { + throw new Error('Authenticated frame proof is invalid'); + } + return JSON.parse(frame.payload) as unknown; +} + +function parseAuthenticatedFrame(value: unknown): AuthenticatedFrame { + if (value === null || typeof value !== 'object' || Array.isArray(value)) { + throw new Error('Authenticated frame envelope is invalid'); + } + const frame = value as Partial; + const keys = Object.keys(value).sort(); + const expectedKeys = ['authenticationCode', 'connectionId', 'direction', 'kind', 'payload', 'sequence']; + if ( + keys.length !== expectedKeys.length + || keys.some((key, index) => key !== expectedKeys[index]) + || frame.kind !== 'authenticated' + || typeof frame.authenticationCode !== 'string' + || typeof frame.connectionId !== 'string' + || (frame.direction !== 'agent-to-broker' && frame.direction !== 'broker-to-agent') + || typeof frame.payload !== 'string' + || !Number.isSafeInteger(frame.sequence) + || (frame.sequence ?? 0) < 1 + ) { + throw new Error('Authenticated frame envelope is invalid'); + } + decodeBase64UrlBytes(frame.authenticationCode); + return frame as AuthenticatedFrame; +} diff --git a/packages/core/src/authority.ts b/packages/core/src/authority.ts index edfc903..4ef9e0a 100644 --- a/packages/core/src/authority.ts +++ b/packages/core/src/authority.ts @@ -3,6 +3,8 @@ import type { CapabilityGrant, JsonValue } from './protocol.js'; export interface AuthorityBinding { readonly bindingId: string; readonly capabilities: CapabilityGrant; + /** Opaque host annotations; never interpreted as authority. */ + readonly metadata?: JsonValue; readonly expiresAt?: string | null; readonly targetGeneration: number; readonly targetId: string; diff --git a/packages/core/src/broker.ts b/packages/core/src/broker.ts index e648b45..a2bda93 100644 --- a/packages/core/src/broker.ts +++ b/packages/core/src/broker.ts @@ -266,6 +266,8 @@ export interface CreateTargetBrokerOptions { } export interface TargetBroker { + /** Current broker-owned leases, scoped when an authenticated client is provided. */ + listLeases: (authority?: ClientAuthority) => readonly (Lease & { readonly principalId: string })[]; acquireLease: ( request: AcquireLeaseRequest, authority?: ClientAuthority, @@ -1549,6 +1551,16 @@ export function createTargetBroker( ensureActive(); return targetAgentPrincipalIdsById.get(targetId); }, + listLeases(authority = localClientAuthority) { + ensureActive(); + if (authority.authorityAvailable === false) return []; + return [...leasesById.values()].flatMap((lease) => { + const principalId = leasePrincipalIdsById.get(lease.id); + return principalId === undefined || (authority !== localClientAuthority && principalId !== authority.principalId) + ? [] + : [structuredClone({ ...lease, principalId })]; + }); + }, listTargets(authority = localClientAuthority) { ensureActive(); if (authority.targetGrants === undefined) diff --git a/packages/core/src/client-target-connection.ts b/packages/core/src/client-target-connection.ts index 7d08adc..64d4991 100644 --- a/packages/core/src/client-target-connection.ts +++ b/packages/core/src/client-target-connection.ts @@ -134,6 +134,7 @@ export function connectClientTargetBroker(connection: ClientTargetConnection, br const subscriptions = new Map(); let stopped = false; async function sendError(message: Extract, error: unknown): Promise { + if (stopped) return; const targetBrokerError = error instanceof Error && 'code' in error ? error as TargetBrokerError : undefined; const code = targetBrokerError?.code ?? 'FEATURE_UNSUPPORTED'; await connection.send({ @@ -176,7 +177,7 @@ export function connectClientTargetBroker(connection: ClientTargetConnection, br if (message.method === 'cdp.subscribe') { const subscription = await broker.subscribe(message.parameters, authority); subscriptions.set(subscription.id, subscription); - void streamSubscription(subscription).finally(() => subscriptions.delete(subscription.id)); + void streamSubscription(subscription).finally(() => subscriptions.delete(subscription.id)).catch(disconnect); await connection.send({ kind: 'response', method: 'cdp.subscribe', protocolVersion: 1, requestId: message.requestId, result: { subscriptionId: subscription.id } }); } else if (message.method === 'cdp.unsubscribe') { subscriptions.get(message.parameters.subscriptionId)?.close(); @@ -205,7 +206,7 @@ export function connectClientTargetBroker(connection: ClientTargetConnection, br } catch (error) { await sendError(message, error); } - })(); + })().catch(disconnect); }); void (async () => { while (true) { @@ -222,13 +223,16 @@ export function connectClientTargetBroker(connection: ClientTargetConnection, br await connection.send({ kind: 'notification', method: 'targets.revoked', parameters: { reason: change.reason, targetGeneration: change.targetGeneration, targetId: change.targetId }, protocolVersion: 1 }); } } - })().catch(() => {}); - return () => { + })().catch(disconnect); + return disconnect; + + function disconnect(): void { + if (stopped) return; stopped = true; disconnectMessages?.(); for (const subscription of subscriptions.values()) subscription.close(); subscriptions.clear(); broker.disconnectClient(authority); void iterator.return?.(); - }; + } } diff --git a/packages/core/src/grant-request.ts b/packages/core/src/grant-request.ts index 284f3e5..d57ece1 100644 --- a/packages/core/src/grant-request.ts +++ b/packages/core/src/grant-request.ts @@ -1,6 +1,6 @@ import type { AuthorityBinding, AuthorityRecord, AuthorityStore } from './authority.js'; import type { AgentAuthority } from './broker.js'; -import type { CapabilityGrant, PublishedTarget } from './protocol.js'; +import type { CapabilityGrant, JsonValue, PublishedTarget } from './protocol.js'; import type { TimeoutMilliseconds } from './timing.js'; import { cdpCapabilityLevels, isCdpNameAllowed } from './cdp-catalogue.js'; @@ -8,6 +8,7 @@ import { capabilityGrantSchema } from './protocol.js'; import { validateTimeoutMilliseconds } from './timing.js'; export interface GrantedTargetReference { + readonly metadata?: JsonValue; readonly targetGeneration: number; readonly targetId: string; } @@ -24,6 +25,7 @@ export interface GrantRequestTargetDirectory { } export interface GrantRequestInput { + readonly metadata?: JsonValue; readonly bindingExpiresAt?: string | null; readonly capabilities: CapabilityGrant; readonly expiresAt?: string | null; @@ -59,6 +61,8 @@ export const defaultGrantRequestTimingPolicy: Readonly }); export interface GrantRequestCoordinator { + /** Copies of coordinator-owned requests and their last committed bindings, without claim secrets. */ + inspect: () => readonly { readonly request: GrantRequest; readonly bindings: readonly AuthorityBinding[]; readonly provider?: GrantRequestProvider }[]; cancel: (requestId: string) => Promise; claim: (requestId: string, provider: GrantRequestProvider) => GrantRequestClaim; complete: (claim: GrantRequestClaim, targets: readonly GrantedTargetReference[]) => Promise; @@ -66,6 +70,8 @@ export interface GrantRequestCoordinator { getRequest: (requestId: string) => GrantRequest | undefined; reconcile: (requestId: string, provider: GrantRequestProvider, targets: readonly GrantedTargetReference[]) => Promise; release: (claim: GrantRequestClaim) => void; + /** Revokes these targets for this request, including later membership reconciliation. Other requests retain their bindings. */ + revokeBindings: (requestId: string, bindingIds: readonly string[]) => Promise; request: (input: GrantRequestInput) => Promise; subscribe: (listener: (change: GrantRequestChange) => void) => () => void; } @@ -88,6 +94,7 @@ export class GrantRequestError extends Error { interface StoredGrantRequest { readonly bindingIds: Set; + readonly revokedTargetIds: Set; bindings: readonly AuthorityBinding[]; cancelled: boolean; claim?: GrantRequestClaim; @@ -285,6 +292,13 @@ export function createGrantRequestCoordinator( } return { + inspect() { + return [...requests.values()].filter(stored => !stored.cancelled).map(stored => structuredClone({ + request: stored.request, + bindings: stored.bindings, + ...(stored.claim === undefined ? {} : { provider: stored.claim.provider }), + })); + }, cancel, claim(requestId, provider) { ensureActive(); @@ -325,7 +339,7 @@ export function createGrantRequestCoordinator( if (stored?.request.state !== 'granted' || stored.cancelled || stored.completing || stored.claim?.provider.principalId !== provider.principalId) throw new GrantRequestError('GRANT_CLAIM_INVALID', 'This provider does not own an idle approved grant request.'); assertUnexpired(stored); - const operation = commitTargets(stored, structuredClone(provider), structuredClone(targets)); + const operation = commitTargets(stored, structuredClone(provider), structuredClone(targets.filter(target => !stored.revokedTargetIds.has(target.targetId)))); stored.operation = operation; return operation; }, @@ -335,6 +349,33 @@ export function createGrantRequestCoordinator( stored.request = { ...stored.request, state: 'pending' }; notify(stored.request.id); }, + async revokeBindings(requestId, bindingIds) { + ensureActive(); + const stored = requests.get(requestId); + if (stored === undefined || stored.cancelled) return; + const revokedTargetIds = new Set(stored.bindings.filter(binding => bindingIds.includes(binding.bindingId)).map(binding => binding.targetId)); + while (stored.operation !== undefined) await stored.operation.catch(() => undefined); + if (stored.cancelled) return; + stored.completing = true; + const operation = (async (): Promise => { + try { + const removedIds = new Set(stored.bindings.filter(binding => revokedTargetIds.has(binding.targetId)).map(binding => binding.bindingId)); + await options.authorityStore.update(stored.request.logicalSessionId, record => record === undefined + ? undefined + : { ...record, bindings: record.bindings.filter(binding => !removedIds.has(binding.bindingId)) }).catch(authorityFailure); + for (const targetId of revokedTargetIds) stored.revokedTargetIds.add(targetId); + stored.bindings = stored.bindings.filter(binding => !removedIds.has(binding.bindingId)); + for (const bindingId of removedIds) stored.bindingIds.delete(bindingId); + notify(requestId); + return stored.bindings; + } finally { + stored.completing = false; + delete stored.operation; + } + })(); + stored.operation = operation; + await operation; + }, async request(input) { ensureActive(); const requested = structuredClone(input); @@ -360,7 +401,7 @@ export function createGrantRequestCoordinator( id, state: 'pending', }; - const stored: StoredGrantRequest = { bindingIds: new Set(), bindings: [], cancelled: false, completing: false, request }; + const stored: StoredGrantRequest = { bindingIds: new Set(), revokedTargetIds: new Set(), bindings: [], cancelled: false, completing: false, request }; requests.set(id, stored); scheduleExpiry(stored); notify(id); diff --git a/packages/core/test/grant-request.test.ts b/packages/core/test/grant-request.test.ts index 2ce9876..604918d 100644 --- a/packages/core/test/grant-request.test.ts +++ b/packages/core/test/grant-request.test.ts @@ -134,6 +134,30 @@ it('reconciles an approved live scope while preserving other grants and unchange await coordinator.dispose(); }); +it('revokes the replacement generation when renewal is already committing', async () => { + expect.assertions(4); + const { authorityStore, coordinator, targets } = setup(); + const request = await coordinator.request({ capabilities: { level: 'interact' }, logicalSessionId: 'session', principalId: 'client' }); + const initial = await coordinator.complete(coordinator.claim(request.id, provider), [{ targetGeneration: 1, targetId: target.id }]); + targets.set(target.id, { providerPrincipalId: provider.principalId, target: { ...target, generation: 2 } }); + const acknowledgement = Promise.withResolvers(); + const update = authorityStore.update.bind(authorityStore); + vi.spyOn(authorityStore, 'update').mockImplementationOnce(async (...parameters) => { + const committed = await update(...parameters); + await acknowledgement.promise; + return committed; + }); + const renewal = coordinator.reconcile(request.id, provider, [{ targetGeneration: 2, targetId: target.id }]); + const revocation = coordinator.revokeBindings(request.id, [initial[0]!.bindingId]); + acknowledgement.resolve(); + const [renewed] = await Promise.all([renewal, revocation]); + expect(renewed[0]?.bindingId).not.toBe(initial[0]?.bindingId); + expect((await authorityStore.get('session'))?.bindings).toEqual([unrelatedBinding]); + expect(await coordinator.reconcile(request.id, provider, [{ targetGeneration: 2, targetId: target.id }])).toEqual([]); + expect(coordinator.inspect()[0]?.bindings).toEqual([]); + await coordinator.dispose(); +}); + it('revokes an authority commit cancelled while its store acknowledgement is pending', async () => { expect.assertions(4); const { authorityStore, coordinator } = setup(); @@ -381,3 +405,20 @@ it('expires a pending request when its binding deadline arrives before its appro expect(coordinator.getRequest(request.id)).toBeUndefined(); await coordinator.dispose(); }); + +it('projects committed request metadata without claim secrets and revokes bindings while the provider is offline', async () => { + expect.assertions(5); + const { authorityStore, coordinator, providerGenerations } = setup(); + const request = await coordinator.request({ capabilities: { level: 'interact' }, logicalSessionId: 'session', principalId: 'client', metadata: { selection: 'live-scope' } }); + const claim = coordinator.claim(request.id, provider); + const bindings = await coordinator.complete(claim, [{ targetId: target.id, targetGeneration: 1, metadata: { approvedOrigin: 'https://example.test' } }]); + const view = coordinator.inspect(); + expect(view[0]?.request.metadata).toEqual({ selection: 'live-scope' }); + expect(view[0]?.bindings[0]?.metadata).toEqual({ approvedOrigin: 'https://example.test' }); + expect(JSON.stringify(view)).not.toContain(claim.id); + providerGenerations.delete(provider.principalId); + await coordinator.revokeBindings(request.id, [bindings[0]!.bindingId, unrelatedBinding.bindingId]); + expect((await authorityStore.get('session'))?.bindings).toEqual([unrelatedBinding]); + expect(coordinator.inspect()[0]?.bindings).toEqual([]); + await coordinator.dispose(); +}); diff --git a/packages/core/test/target-directory.test.ts b/packages/core/test/target-directory.test.ts index 3e0c9f4..f4a6530 100644 --- a/packages/core/test/target-directory.test.ts +++ b/packages/core/test/target-directory.test.ts @@ -2443,3 +2443,30 @@ it('fences automation cancellation, forbidden provider commands, replacement, an expect(broker.listTargets()).toEqual([{ ...target, generation: 2 }]); expect(() => broker.unregisterAutomationProvider(agentAuthority)).not.toThrow(); }); + +it('disposes the client binding when a late reply cannot be delivered', async () => { + expect.assertions(2); + const broker = createTargetBroker(); + const disconnected = vi.spyOn(broker, 'disconnectClient'); + let listener: ((message: ClientToBrokerMessage) => void) | undefined; + const disconnect = connectClientTargetBroker({ + onMessage(received) { + listener = received; + return () => { + listener = undefined; + }; + }, + async send(message) { + if (message.kind !== 'notification') throw new Error('The client transport closed before its reply.'); + }, + }, broker); + try { + listener!({ kind: 'request', method: 'targets.list', parameters: {}, protocolVersion: 1, requestId: '70000000-0000-4000-8000-000000000099' }); + await vi.waitUntil(() => disconnected.mock.calls.length > 0); + expect(disconnected).toHaveBeenCalledOnce(); + expect(listener).toBeUndefined(); + } finally { + disconnect(); + broker.dispose(); + } +}); diff --git a/packages/core/tsdown.config.ts b/packages/core/tsdown.config.ts index fe628cf..8d7dc0c 100644 --- a/packages/core/tsdown.config.ts +++ b/packages/core/tsdown.config.ts @@ -11,6 +11,7 @@ export default defineConfig({ 'src/index.ts', 'src/agent.ts', 'src/authority.ts', + 'src/authentication.ts', 'src/automation.ts', 'src/broker.ts', 'src/cdp-catalogue.ts', diff --git a/packages/devframe/LICENSE b/packages/devframe/LICENSE new file mode 100644 index 0000000..c4181d6 --- /dev/null +++ b/packages/devframe/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 D.V. Colomban + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/devframe/README.md b/packages/devframe/README.md new file mode 100644 index 0000000..786b3f8 --- /dev/null +++ b/packages/devframe/README.md @@ -0,0 +1,52 @@ +# @dvcol/cdb-devframe + +CDB service, provider transport and browser-control panel for Devframe 0.9.10 and Node 24. The DevTools example retains DevTools Kit 0.6.1 and Vite 8.1.5. + +Install `createCdbService({ broker: defineBroker(...) })` in a definition's `services` array. During `setup`, obtain its handle through `getCdbService(context)`. Forward the host's `onPeerConnect(connection, session)` and `onPeerDisconnect(connection)` callbacks to that handle. Call `dispose()` on startup failure and shutdown. Installation lasts for the Devframe context; no live service removal is required. + +```ts +import { createCdbClient } from '@dvcol/cdb-devframe/client'; + +const browser = createCdbClient(existingAuthenticatedDevframeClient); +const provider = await browser.connectProvider({ + registration, + pairingStore, + pairingKey: existingPairingKey, + confirmPairing, +}); +``` + +This reuses the supplied peer. CDB pairing still verifies the provider's persistent implementation identity with a fresh, peer-bound proof. Browser operations send explicit cancellation messages. Closing a provider channel or disposing CDB does not close the host's connection. Forward transport loss to `browser.disconnected()`; reuse the handle for that context, or create a new one when the host replaces its Devframe client. + +Use `createCdbClientSession({ credentialKey, credentialStore, metadata })` to retain an agent principal across replacement peers. Call `session.connect(browser)` after each connection and `session.terminate(browser)` on shutdown. CDB rotates the stored credential on resume and starts a new session when the previous one has ended. Connection and storage failures remain errors; they do not silently replace the principal. + +`authorizePeer(session, operation)` adds application policy after Devframe authenticates the actual calling peer. Restrict management operations to the host's intended UI/provider roles. Agent tool contributions and invocation are available on the service's `broker`; this package does not install an MCP server or publish tools to WebMCP. Registry aggregation, owner selection and routing remain in the host. + +For an existing tool catalogue, wrap the client call in `browser.withCancellation(signal, operationId => callRegisteredTool({ name, arguments, operationId }))`. After the host selects the registered browser contribution, invoke `service.invoke(actualPeerSession, { name, arguments, operationId })`. This shares CDB's operation tracking and cancellation while preserving the host's routing. Return structured error codes and details through the host's result contract; RPC exception serializers may preserve only an error's message. + +The IndexedDB pairing store can locate an existing credential by broker and installation identity when its transport endpoint changes. It retains the original stored key and verifies the broker's proof before establishing the replacement channel. + +## Panel + +`createCdbPanel({ client: () => existingBrokerClient })` returns a Devframe `definition` and an explicit `dispose()` hook. Standalone mode serves Devframe's first-party `@devframes/json-render-ui` SPA. The shared JSON view contains summary cards, Requests/Access/Providers/Activity tabs, and expandable connection details. Mount the definition with `createEmbedded` or `createPluginFromDevframe`, or run it with `createDevServer`. Its client supplies only management state and actions; mounting it never creates a broker. + +Mounted hubs can select an already registered JSON renderer and override component types or properties. This keeps the view and broker actions in CDB while the host supplies its design system: + +```ts +const panel = createCdbPanel({ + client: () => existingBrokerClient, + renderer: { + type: 'host-json-render', + components: { Card: { props: { interactive: true } } }, + }, + dock: { category: 'settings', defaultOrder: 1_000 }, +}); +``` + +The renderer must support Devframe's JSON view and action contracts. Component overrides apply only to that mounted renderer; the standalone SPA uses the reference catalogue. Dock ordering follows the host's categories and saved preferences. The page script remains available through `dock.clientScript` for host notifications and review intents. + +For an existing application container, import `mountBrowserControlPanel` from `@dvcol/cdb-devframe/panel` and pass a `client`, `container`, optional branding/CSS and an `onReview` callback. Unmounting releases the panel subscription without disposing that client. + +The page script emits `cdb:review-request` with `{ requestId }` to request the embedding application's final approval UI. This is an untrusted presentation intent. It never grants authority or proves a human decision. The extension must authenticate final approval from its trusted popup or other host-approved channel. + +See the [runnable example](../../examples/devframe/README.md). diff --git a/packages/devframe/package.json b/packages/devframe/package.json new file mode 100644 index 0000000..c6103de --- /dev/null +++ b/packages/devframe/package.json @@ -0,0 +1,76 @@ +{ + "name": "@dvcol/cdb-devframe", + "type": "module", + "version": "0.1.0", + "description": "CDB services, provider transport, and browser-control panels for Devframe hosts.", + "author": "dvcol", + "license": "MIT", + "homepage": "https://github.com/dvcol/chrome-debugger-bridge#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/dvcol/chrome-debugger-bridge.git", + "directory": "packages/devframe" + }, + "bugs": { + "url": "https://github.com/dvcol/chrome-debugger-bridge/issues" + }, + "keywords": [ + "chrome", + "chrome-debugger", + "cdp", + "browser-automation", + "mcp", + "agent" + ], + "sideEffects": false, + "exports": { + ".": { + "types": "./dist/index.d.mts", + "import": "./dist/index.mjs" + }, + "./client": { + "types": "./dist/client.d.mts", + "import": "./dist/client.mjs" + }, + "./panel": { + "types": "./dist/panel.d.mts", + "import": "./dist/panel.mjs" + }, + "./page-script": { + "types": "./dist/page-script.d.mts", + "import": "./dist/page-script.mjs" + } + }, + "files": [ + "LICENSE", + "README.md", + "dist" + ], + "engines": { + "node": ">=24.0.0" + }, + "scripts": { + "build": "tsdown && vite build", + "check:package": "publint", + "pack": "pnpm pack --out artifacts/package.tgz", + "test": "vitest run", + "typecheck": "tsc --noEmit --project tsconfig.json" + }, + "dependencies": { + "@devframes/hub": "catalog:devtools", + "@devframes/json-render": "catalog:devtools", + "@devframes/json-render-ui": "catalog:devtools", + "@dvcol/cdb": "workspace:^", + "@dvcol/cdb-broker": "workspace:^", + "@dvcol/cdb-extension": "workspace:^", + "devframe": "catalog:devtools" + }, + "devDependencies": { + "tsdown": "catalog:build", + "vite": "catalog:devtools", + "vitest": "catalog:testing" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/devframe/src/client.ts b/packages/devframe/src/client.ts new file mode 100644 index 0000000..f83468b --- /dev/null +++ b/packages/devframe/src/client.ts @@ -0,0 +1,251 @@ +import type { BrokerToAgentMessage, CredentialStore, JsonValue, LogicalSessionCredential } from '@dvcol/cdb'; +import type { BrokerState, ProviderAuthenticationResult, ProviderRegistration } from '@dvcol/cdb-broker/contract'; +import type { IndexedDbPairingStore, ProviderConnection, StoredBrokerPairing } from '@dvcol/cdb-extension'; + +import type { CdbDevframeClient, CdbProviderClosure, CdbProviderFrame, CdbReply } from './wire.js'; + +import { createMemoryCredentialStore } from '@dvcol/cdb'; +import { + createAgentAuthenticationProof, + decodeBase64UrlBytes, + generateRandomBase64Url, + importAgentCredential, + verifyBrokerAuthenticationProof, +} from '@dvcol/cdb/authentication'; + +import { cdbServiceScope } from './wire.js'; + +export interface ConnectCdbProviderOptions { + readonly registration: ProviderRegistration; + /** Preserve the existing key when moving an installation to another transport. */ + readonly pairingStore: IndexedDbPairingStore; + readonly pairingKey: string; + readonly confirmPairing: (brokerId: string) => boolean | Promise; +} + +export interface CdbClient { + watch: (listener: (state: BrokerState) => void) => Promise<() => void>; + snapshot: () => Promise; + revokeScope: (requestId: string) => Promise; + revokeGrant: (grantId: string) => Promise; + disconnectProvider: (providerId: string, forgetPairing?: boolean) => Promise; + connectSession: (input?: { readonly metadata?: JsonValue; readonly resume?: { readonly credential: string; readonly logicalSessionId: string } }) => Promise; + terminateSession: () => Promise; + invoke: (name: string, input: unknown, signal?: AbortSignal) => Promise; + /** Routes cancellation for a browser call registered in the embedding host's catalogue. */ + withCancellation: (signal: AbortSignal | undefined, invoke: (operationId: string) => Promise) => Promise; + connectProvider: (options: ConnectCdbProviderOptions) => Promise; + disconnected: () => void; + dispose: () => Promise; +} + +const clients = new WeakMap(); + +export interface CdbClientSession { + connect: (client: CdbClient) => Promise; + terminate: (client: CdbClient) => Promise; +} + +/** Retains and rotates one principal's resume credential; hosts forward connection lifecycle events. */ +export function createCdbClientSession(options: { + readonly credentialKey: string; + readonly credentialStore?: CredentialStore; + readonly metadata?: JsonValue; +}): CdbClientSession { + const store = options.credentialStore ?? createMemoryCredentialStore(); + const input = options.metadata === undefined ? {} : { metadata: options.metadata }; + let pending = Promise.resolve(); + return { + async connect(client) { + const operation = pending.catch(() => {}).then(async () => { + const resume = await store.get(options.credentialKey); + let connected: LogicalSessionCredential; + try { + connected = await client.connectSession({ ...input, ...(resume === undefined ? {} : { resume }) }); + } catch (error) { + if (resume === undefined || !(error instanceof Error) || !('code' in error) + || (error.code !== 'SESSION_NOT_FOUND' && error.code !== 'SESSION_EXPIRED')) throw error; + await store.delete(options.credentialKey); + connected = await client.connectSession(input); + } + await store.set(options.credentialKey, { credential: connected.resumeCredential, logicalSessionId: connected.logicalSessionId }); + }); + pending = operation; + return operation; + }, + async terminate(client) { + await pending; + await client.terminateSession(); + await store.delete(options.credentialKey); + }, + }; +} + +/** Uses the supplied Devframe peer. It neither creates nor closes a socket or another RPC client. */ +export function createCdbClient(client: CdbDevframeClient): CdbClient { + const existing = clients.get(client); + if (existing !== undefined) return existing; + const rpc = client.scope(cdbServiceScope).rpc; + const stateListeners = new Set<(state: BrokerState) => void>(); + let provider: { readonly channelId: string; close: (code?: number, reason?: string) => void; receive: (message: BrokerToAgentMessage) => void } | undefined; + let disposed = false; + let watching: Promise | undefined; + let state: BrokerState | undefined; + + async function call(name: string, ...arguments_: unknown[]): Promise { + if (disposed) throw new Error('The CDB client has been disposed.'); + const result = await rpc.call(name, ...arguments_) as CdbReply; + if (!result.ok) throw Object.assign(new Error(result.error.message), result.error); + return result.value; + } + + function publish(value: BrokerState): void { + if (disposed || (state !== undefined && value.revision < state.revision)) return; + state = value; + for (const listener of stateListeners) listener(value); + } + + rpc.register({ name: 'state-changed', type: 'event', handler: publish }); + rpc.register({ name: 'provider-frame', type: 'event', handler(envelope: CdbProviderFrame) { + if (!disposed && provider?.channelId === envelope.channelId) provider.receive(envelope.message); + } }); + rpc.register({ name: 'provider-closed', type: 'event', handler(closure: CdbProviderClosure) { + if (!disposed && provider?.channelId === closure.channelId) provider.close(closure.code, closure.reason); + } }); + + async function watch(listener: (state: BrokerState) => void): Promise<() => void> { + watching ??= call('watch').then((value) => { + publish(value); + return value; + }).catch((error) => { + watching = undefined; + throw error; + }); + const initial = await watching; + if (disposed) throw new Error('The CDB client has been disposed.'); + stateListeners.add(listener); + listener(state ?? initial); + return () => { + stateListeners.delete(listener); + }; + } + + const handle: CdbClient = { + watch, + snapshot: async () => call('state'), + revokeScope: async (requestId: string) => call('revoke-scope', requestId), + revokeGrant: async (grantId: string) => call('revoke-grant', grantId), + disconnectProvider: async (providerId: string, forgetPairing = false) => call('disconnect-provider', providerId, forgetPairing), + connectSession: async (input = {}) => call('session-connect', input), + terminateSession: async () => call('session-terminate'), + async invoke(name: string, input: unknown, signal?: AbortSignal): Promise { + return handle.withCancellation(signal, async operationId => call('invoke', { operationId, name, arguments: input })); + }, + async withCancellation(signal, invoke) { + signal?.throwIfAborted(); + const operationId = crypto.randomUUID(); + const cancel = (): void => { + rpc.callEvent('cancel', operationId); + }; + signal?.addEventListener('abort', cancel, { once: true }); + try { + return await invoke(operationId); + } finally { + signal?.removeEventListener('abort', cancel); + } + }, + async connectProvider(options: ConnectCdbProviderOptions): Promise { + const { brokerId } = await call<{ readonly brokerId: string }>('provider-register', options.registration); + let pairing = await options.pairingStore.load(options.pairingKey); + pairing ??= await options.pairingStore.findByIdentity?.(brokerId, options.registration.instanceId); + if (pairing !== undefined && (pairing.brokerId !== brokerId || pairing.agentId !== options.registration.instanceId)) + throw new Error('The broker or provider identity changed. Explicitly forget the old pairing before pairing again.'); + let invitation: { readonly code: string; readonly credential: string } | undefined; + if (pairing === undefined) { + if (!await options.confirmPairing(brokerId)) throw new Error('Broker pairing was declined.'); + const offer = await call<{ readonly code: string }>('provider-pair'); + const credential = generateRandomBase64Url(32); + invitation = { code: offer.code, credential }; + pairing = { brokerId, agentId: options.registration.instanceId, credentialId: crypto.randomUUID(), endpoint: options.pairingKey, key: await importAgentCredential(decodeBase64UrlBytes(credential)) } satisfies StoredBrokerPairing; + } + const clientNonce = generateRandomBase64Url(32); + const transcript = await call('provider-challenge', { credentialId: pairing.credentialId, clientNonce, ...(invitation === undefined ? {} : { pairing: invitation }) }); + if (transcript.brokerId !== brokerId || transcript.agentId !== pairing.agentId || transcript.credentialId !== pairing.credentialId || transcript.clientNonce !== clientNonce || transcript.transportProtocol !== 'chrome-debugger-bridge.rpc.v1') + throw new Error('The broker authentication challenge is not bound to this provider.'); + const result = await call('provider-authenticate', await createAgentAuthenticationProof(pairing.key, transcript)); + try { + if (disposed || result.claims.principalId !== pairing.agentId || !await verifyBrokerAuthenticationProof(pairing.key, transcript, result.claims, result.proof)) + throw new Error('The broker authentication proof is invalid or the client was disposed.'); + if (invitation !== undefined) await options.pairingStore.save(pairing); + } catch (error) { + rpc.callEvent('provider-close', result.channelId); + throw error; + } + provider?.close(4000, 'Provider connection replaced'); + const closed = Promise.withResolvers<{ readonly code: number; readonly reason: string }>(); + const listeners = new Set<(message: BrokerToAgentMessage) => void>(); + let active = true; + const channel = { + channelId: result.channelId, + close(this: void, code = 1000, reason = 'Provider channel closed') { + if (!active) return; + active = false; + listeners.clear(); + closed.resolve({ code, reason }); + if (!disposed) rpc.callEvent('provider-close', result.channelId); + }, + receive(message: BrokerToAgentMessage) { + if (active) for (const listener of listeners) listener(message); + }, + }; + provider = channel; + async function providerCall(name: string, ...arguments_: unknown[]): Promise { + if (!active) throw new Error('The provider channel is closed.'); + return call(name, ...arguments_); + } + return { + brokerId, + registration: options.registration, + generation: result.claims.connectionGeneration, + closed: closed.promise, + close: channel.close, + onMessage(listener) { + listeners.add(listener); + return () => listeners.delete(listener); + }, + async send(message) { + if (!active) throw new Error('The provider channel is closed.'); + await call('provider-frame', { channelId: channel.channelId, message }); + }, + snapshot: async () => providerCall('state'), + claim: async requestId => providerCall('provider-claim', requestId), + release: async claim => providerCall('provider-release', claim), + approve: async (claim, targets) => providerCall('provider-approve', claim, targets), + reconcile: async targets => providerCall('provider-reconcile', targets), + reconcileScope: async (requestId, targets) => providerCall('provider-scope', requestId, targets), + revokeScope: async requestId => providerCall('revoke-scope', requestId), + watch, + }; + }, + /** The embedding host calls this for transport loss. A later reconnect reuses this client context. */ + disconnected() { + provider?.close(1006, 'Devframe connection lost'); + watching = undefined; + state = undefined; + }, + async dispose() { + if (disposed) return; + provider?.close(1000, 'CDB client disposed'); + try { + if (watching !== undefined) await call('unwatch'); + } finally { + disposed = true; + stateListeners.clear(); + watching = undefined; + state = undefined; + } + }, + }; + clients.set(client, handle); + return handle; +} diff --git a/packages/devframe/src/index.ts b/packages/devframe/src/index.ts new file mode 100644 index 0000000..5ef0f1d --- /dev/null +++ b/packages/devframe/src/index.ts @@ -0,0 +1,4 @@ +export { type CdbPanel, type CdbPanelOptions, createCdbPanel } from './panel-definition.js'; +export { type BrowserControlPanelComponents, buildBrowserControlPanelView } from './panel-view.js'; +export { type CdbDevframePeerSession, type CdbDevframeService, type CdbInvocation, type CdbServiceOptions, createCdbService, createCdbService as default, getCdbService } from './service.js'; +export { cdbServiceScope } from './wire.js'; diff --git a/packages/devframe/src/page-script.ts b/packages/devframe/src/page-script.ts new file mode 100644 index 0000000..176890e --- /dev/null +++ b/packages/devframe/src/page-script.ts @@ -0,0 +1,79 @@ +import type { DevframeRpcClient } from 'devframe/client'; + +import { createBrowserControlNotificationController } from '@dvcol/cdb-extension/notifications'; + +import { createBrowserControlPanelClient } from './panel.js'; + +/** Page events request the host's final approval UI. They never approve browser access. */ +export const browserControlReviewEvent = 'cdb:review-request'; + +export interface BrowserControlPageContext { + readonly rpc: DevframeRpcClient; + readonly current: { readonly domElements: { readonly iframe?: HTMLIFrameElement | null } }; + readonly commands: { + register: (command: { id: string; title: string; source: 'client'; action: () => Promise; showInPalette: boolean }) => () => void; + }; + readonly messages: { + info: (message: string, options: { id: string; description: string; notify: boolean; autoDismiss: false; actions: { id: string; label: string; kind: 'command'; command: { id: string } }[] }) => Promise<{ dismiss: () => Promise }>; + }; +} + +/** Uses the hub's existing page connection; the embedding extension handles the review intent. */ +export default async function setupBrowserControlPage(context: BrowserControlPageContext): Promise<() => void> { + const client = createBrowserControlPanelClient(context.rpc); + const review = (requestId: string): void => { + window.dispatchEvent(new CustomEvent(browserControlReviewEvent, { detail: { requestId } })); + }; + const controller = createBrowserControlNotificationController({ onReview: request => review(request.id), onRevoke: async requestId => client.revokeScope(requestId) }); + const notifications = new Map void>(); + const prefix = `cdb:browser-control:${crypto.randomUUID()}`; + const stopNotifications = controller.subscribe((state) => { + const items = [ + ...state.requests.map(request => ({ id: `request:${request.id}`, title: 'Browser control requested', description: `${request.principalLabel} requests ${request.level} access with ${request.navigation} navigation.`, label: 'Review request', action: async () => controller.review(request.id) })), + ...Array.from(Map.groupBy(state.grants, grant => grant.requestId), ([requestId, grants]) => ({ id: `grant:${requestId}`, title: 'Browser control active', description: `${grants[0]!.principalLabel}: ${grants[0]!.level} access to ${grants.length} approved ${grants.length === 1 ? 'tab' : 'tabs'}.`, label: 'Stop control', action: async () => controller.revoke(requestId) })), + ]; + for (const [id, remove] of notifications) { + if (items.some(item => item.id === id)) continue; + remove(); + notifications.delete(id); + } + for (const item of items) { + if (notifications.has(item.id)) continue; + const id = `${prefix}:${item.id}`; + const unregister = context.commands.register({ id, title: item.label, source: 'client', action: item.action, showInPalette: false }); + const message = context.messages.info(item.title, { id, description: item.description, notify: true, autoDismiss: false, actions: [{ id: 'control', label: item.label, kind: 'command', command: { id } }] }); + notifications.set(item.id, () => { + unregister(); + void message.then(async handle => handle.dismiss()).catch(error => console.error('Unable to dismiss browser-control notification.', error)); + }); + void message.catch(error => console.error('Unable to show browser-control notification.', error)); + } + }); + const receive = (event: MessageEvent): void => { + if (event.source !== context.current.domElements.iframe?.contentWindow || event.source === null) return; + const data = event.data; + if (data !== null && typeof data === 'object' && 'type' in data && data.type === browserControlReviewEvent && 'requestId' in data && typeof data.requestId === 'string') review(data.requestId); + }; + let stopWatching: (() => void) | undefined; + const dispose = (): void => { + stopWatching?.(); + document.documentElement.removeAttribute('data-cdb-notifications-ready'); + stopNotifications(); + for (const remove of notifications.values()) remove(); + notifications.clear(); + controller.dispose(); + window.removeEventListener('message', receive); + window.removeEventListener('pagehide', dispose); + }; + try { + stopWatching = await client.watch(state => controller.update(state)); + /** Embedding extensions defer their fallback card only once this notifier is listening. */ + document.documentElement.setAttribute('data-cdb-notifications-ready', ''); + window.addEventListener('message', receive); + window.addEventListener('pagehide', dispose, { once: true }); + return dispose; + } catch (error) { + dispose(); + throw error; + } +} diff --git a/packages/devframe/src/panel-definition.ts b/packages/devframe/src/panel-definition.ts new file mode 100644 index 0000000..8ad3488 --- /dev/null +++ b/packages/devframe/src/panel-definition.ts @@ -0,0 +1,112 @@ +import type { DevframeHubContext } from '@devframes/hub'; +import type { JsonRenderView } from '@devframes/json-render'; +import type { DevframeDefinition, DevframeNodeContext, DevframeScopedNodeRpc } from 'devframe'; + +import type { BrowserControlPanelComponents } from './panel-view.js'; +import type { BrowserControlPanelClient } from './panel.js'; + +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { jsonRenderSpaDir } from '@devframes/json-render-ui/spa'; +import { toJsonRenderDockEntry } from '@devframes/json-render/hub'; +import { createJsonRenderView } from '@devframes/json-render/node'; + +import packageManifest from '../package.json' with { type: 'json' }; +import { buildBrowserControlPanelView } from './panel-view.js'; + +/** Hosts may augment their RPC catalogue without exporting those declarations to CDB. */ +interface PanelContext { + rpc: { sharedState: Pick }; + scope: (namespace: string) => { readonly rpc: Pick & { + register: (definition: Pick[0], 'name' | 'type' | 'handler'>) => unknown; + }; }; +} + +export type CdbPanelDefinition = Pick & { + setup: (context: PanelContext) => Promise; +}; + +export interface CdbPanelOptions { + readonly client: () => BrowserControlPanelClient; + readonly name?: string; + /** A mounted host may use its own JSON renderer and component properties. */ + readonly renderer?: { readonly type: string; readonly components?: BrowserControlPanelComponents }; + readonly dock?: { readonly category?: string; readonly defaultOrder?: number }; +} + +export interface CdbPanel { + readonly definition: CdbPanelDefinition; + dispose: () => void; +} + +/** Serves Devframe's reference SPA; mounted hosts can select their own renderer. */ +export function createCdbPanel(options: CdbPanelOptions): CdbPanel { + const directory = dirname(fileURLToPath(import.meta.url)); + let disposed = false; + let unsubscribe: (() => void) | undefined; + let view: JsonRenderView | undefined; + return { + definition: { + id: 'cdb-browser-control', + name: options.name ?? 'Browser control', + version: packageManifest.version, + packageName: packageManifest.name, + importMetaUrl: import.meta.url, + homepage: packageManifest.homepage, + description: 'Manage browser providers and approved access.', + icon: 'ph:browser-duotone', + capabilities: { build: false }, + clientAssets: jsonRenderSpaDir, + dock: { defaultOrder: 1_000, ...options.dock, clientScript: { importFrom: join(directory, 'view/page-script.js') } }, + async setup(context) { + if (disposed) throw new Error('The CDB panel was disposed.'); + const client = options.client(); + const renderer = 'docks' in context ? options.renderer : undefined; + const rpc = context.scope('cdb:panel').rpc; + const initial = await client.snapshot(); + const state = await rpc.sharedState('state', { initialValue: { broker: initial } }); + view = createJsonRenderView(context as DevframeNodeContext, { + id: 'browser-control', + title: options.name ?? 'Browser control', + spec: buildBrowserControlPanelView(initial, renderer?.components), + ...(renderer === undefined ? {} : { schema: false as const }), + }); + const stop = await client.watch((broker) => { + state.mutate((value) => { + value.broker = broker; + }); + view?.update(buildBrowserControlPanelView(broker, renderer?.components)); + }); + if (disposed) { + stop(); + view?.dispose(); + return; + } + unsubscribe = stop; + rpc.register({ name: 'state', type: 'query', handler: async () => client.snapshot() }); + rpc.register({ name: 'revoke-scope', type: 'action', handler: async (requestId: string) => client.revokeScope(requestId) }); + rpc.register({ name: 'revoke-grant', type: 'action', handler: async (grantId: string) => client.revokeGrant(grantId) }); + rpc.register({ name: 'disconnect-provider', type: 'action', handler: async (providerId: string, forgetPairing: boolean) => client.disconnectProvider(providerId, forgetPairing) }); + rpc.register({ name: 'revoke-target', type: 'action', handler: async ({ grantId }: { grantId: string }) => client.revokeGrant(grantId) }); + rpc.register({ name: 'disconnect', type: 'action', handler: async ({ providerId }: { providerId: string }) => client.disconnectProvider(providerId) }); + rpc.register({ name: 'forget', type: 'action', handler: async ({ providerId }: { providerId: string }) => client.disconnectProvider(providerId, true) }); + if ('docks' in context) { + const hub = context as unknown as DevframeHubContext; + const entry = hub.docks.views.get('cdb-browser-control'); + if (entry === undefined) throw new Error('Install the CDB panel before selecting its renderer.'); + const dock = toJsonRenderDockEntry(view, entry); + /** Host renderer names use the same JSON view contract as the reference renderer. */ + hub.docks.update({ ...dock, type: (options.renderer?.type ?? 'json-render') as typeof dock.type }); + } + }, + }, + dispose() { + disposed = true; + unsubscribe?.(); + unsubscribe = undefined; + view?.dispose(); + view = undefined; + }, + }; +} diff --git a/packages/devframe/src/panel-view.ts b/packages/devframe/src/panel-view.ts new file mode 100644 index 0000000..82ffe21 --- /dev/null +++ b/packages/devframe/src/panel-view.ts @@ -0,0 +1,89 @@ +import type { Spec, UIElement } from '@devframes/json-render'; +import type { BrokerState } from '@dvcol/cdb-broker/contract'; + +export interface BrowserControlPanelComponents { + readonly [component: string]: { readonly type?: string; readonly props?: Record }; +} + +/** One view for the reference renderer and host-supplied component registries. */ +export function buildBrowserControlPanelView(state: BrokerState, components: BrowserControlPanelComponents = {}): Spec { + const elements: Record = {}; + function element(id: string, type: string, props: Record, children?: string[]): string { + const override = components[type]; + elements[id] = { type: override?.type ?? type, props: { ...props, ...override?.props }, ...(children === undefined ? {} : { children }) }; + return id; + } + function stack(id: string, children: string[], row = false): string { + return element(id, 'Stack', { direction: row ? 'row' : 'column', gap: 12, ...(row ? { wrap: true, align: 'stretch' } : {}) }, children); + } + function text(id: string, value: string, variant = 'body'): string { + return element(id, 'Text', { text: value, variant }); + } + function button(id: string, label: string, action: string, parameters: Record, danger = false): string { + element(id, 'Button', { label, variant: danger ? 'danger' : 'secondary' }); + elements[id]!.on = { press: { action: `cdb:panel:${action}`, params: parameters } }; + return stack(`${id}-actions`, [id], true); + } + function facts(id: string, data: Record): string { + return element(id, 'KeyValueTable', { data }); + } + function details(id: string, data: Record, actions: string[] = []): string { + return element(id, 'Card', { title: 'Connection details', collapsible: true, defaultCollapsed: true }, [stack(`${id}-body`, [facts(`${id}-facts`, data), ...actions])]); + } + function card(id: string, title: string, children: string[]): string { + return element(id, 'Card', { title }, [stack(`${id}-body`, children)]); + } + function section(id: string, children: string[], empty: string): string { + return stack(id, children.length === 0 ? [text(`${id}-empty`, empty)] : children); + } + const summaries = [ + ['providers', 'Connected browsers', state.providers.filter(provider => provider.state === 'ready').length], + ['requests', 'Pending requests', state.requests.length], + ['access', 'Active grants', state.grants.filter(grant => grant.state === 'active').length], + ['operations', 'Active operations', state.leases.length], + ] as const; + const summary = stack('summary', summaries.map(([id, title, count]) => element(`summary-${id}-cell`, 'Stack', { flex: '1 1 140px' }, [card(`summary-${id}`, title, [text(`summary-${id}-value`, String(count), 'heading')])])), true); + const requests = state.requests.map(request => card(`request-${request.id}`, request.principalLabel, [ + facts(`request-${request.id}-facts`, { Grant: request.level, Navigation: request.navigation, Status: request.state }), + text(`request-${request.id}-help`, 'Review this request in the browser tab you want to share.', 'caption'), + ])); + const access = state.grants.map((grant) => { + const id = `grant-${grant.id}`; + const target = state.targets.find(candidate => candidate.id === grant.targetId); + const badge = element(`${id}-status`, 'Badge', { text: grant.state, variant: grant.state === 'active' ? 'success' : 'warning' }); + return card(id, target?.title ?? 'Approved tab', [ + stack(`${id}-header`, [text(`${id}-client`, grant.principalLabel), badge], true), + facts(`${id}-access`, { Grant: grant.level, Navigation: grant.navigation, Origin: grant.approvedOrigin }), + ...(target?.url === undefined ? [] : [text(`${id}-url`, target.url, 'caption')]), + button(`${id}-revoke`, 'Revoke target', 'revoke-target', { grantId: grant.id }, true), + details(`${id}-details`, { 'Grant ID': grant.id, 'Target ID': grant.targetId, 'Provider': grant.providerId }), + ]); + }); + const providers = state.providers.map((provider) => { + const id = `provider-${provider.id}`; + return card(id, provider.name, [ + element(`${id}-status`, 'Badge', { text: provider.state, variant: provider.state === 'ready' ? 'success' : 'warning' }), + facts(`${id}-facts`, { Tabs: provider.targetCount, Version: provider.version, Pairing: provider.paired ? 'Paired' : 'Not paired' }), + button(`${id}-disconnect`, 'Disconnect', 'disconnect', { providerId: provider.id }), + details(`${id}-details`, { 'Provider ID': provider.id, 'Installation ID': provider.instanceId, 'Maximum access': provider.maximumLevel }, [button(`${id}-forget`, 'Forget pairing', 'forget', { providerId: provider.id }, true)]), + ]); + }); + const operations = state.leases.map((lease, index) => card(`operation-${index}`, state.principals.find(principal => principal.id === lease.principalId)?.label ?? 'Browser client', [facts(`operation-${index}-facts`, { Mode: lease.mode, Methods: lease.methods.join(', ') })])); + const panels = [ + section('requests', requests, 'No pending requests. An agent can request access when it needs a browser tab.'), + section('access', access, 'No shared tabs. Approve an agent request in the browser to grant access.'), + section('providers', providers, 'Connect a browser extension to begin.'), + section('operations', operations, 'No actions are running.'), + ]; + const tabs = element('tabs', 'Tabs', { + defaultValue: state.requests.length > 0 ? 'requests' : 'access', + tabs: [ + { value: 'requests', label: 'Requests' }, + { value: 'access', label: 'Access' }, + { value: 'providers', label: 'Providers' }, + { value: 'operations', label: 'Activity' }, + ], + }, panels); + stack('root', [summary, tabs]); + return { root: 'root', elements }; +} diff --git a/packages/devframe/src/panel.ts b/packages/devframe/src/panel.ts new file mode 100644 index 0000000..fc811e2 --- /dev/null +++ b/packages/devframe/src/panel.ts @@ -0,0 +1,139 @@ +import type { BrokerRequest, BrokerState } from '@dvcol/cdb-broker/contract'; +import type { BrowserControlNotificationRendererOptions } from '@dvcol/cdb-extension/notifications'; + +import type { CdbDevframeClient } from './wire.js'; + +import { createBrowserControlNotificationController, renderBrowserControlNotifications } from '@dvcol/cdb-extension/notifications'; + +/** A panel consumes a broker connection; it does not own its runtime or transport. */ +export interface BrowserControlPanelClient { + snapshot: () => BrokerState | Promise; + watch: (listener: (state: BrokerState) => void) => (() => void) | Promise<() => void>; + revokeScope: (requestId: string) => Promise; + revokeGrant: (grantId: string) => Promise; + disconnectProvider: (providerId: string, forgetPairing?: boolean) => Promise; +} + +export interface BrowserControlPanelOptions { + readonly client: BrowserControlPanelClient; + readonly container: HTMLElement; + readonly onReview: (request: BrokerRequest) => void | Promise; + readonly branding?: BrowserControlNotificationRendererOptions['branding']; + readonly css?: string; +} + +/** Mounts framework-neutral management UI into an existing application. */ +export async function mountBrowserControlPanel(options: BrowserControlPanelOptions): Promise<{ dispose: () => void }> { + const document = options.container.ownerDocument; + const host = document.createElement('section'); + const root = host.attachShadow({ mode: 'open' }); + const style = document.createElement('style'); + style.textContent = ` + :host { display: block; color-scheme: light dark; color: CanvasText; background: Canvas; font: 14px/1.5 system-ui, sans-serif; padding: 16px; } + h1 { font-size: 22px; margin: 0 0 16px; } h2 { font-size: 16px; margin: 18px 0 8px; } + article { padding: 12px; margin: 8px 0; border: 1px solid color-mix(in srgb, CanvasText 20%, transparent); border-radius: 8px; } + p { margin: 4px 0; overflow-wrap: anywhere; } small { opacity: .75; } + button { color: inherit; background: transparent; border: 1px solid currentColor; border-radius: 4px; font: inherit; padding: 6px 10px; margin: 6px 8px 0 0; cursor: pointer; } + [role=alert] { color: #b42318; } + ${options.css ?? ''} + `; + const heading = document.createElement('h1'); + heading.textContent = options.branding?.title ?? 'Browser control'; + const notifications = document.createElement('div'); + const content = document.createElement('div'); + root.append(style, heading, notifications, content); + options.container.append(host); + let disposed = false; + const controller = createBrowserControlNotificationController({ onReview: options.onReview, onRevoke: async requestId => options.client.revokeScope(requestId) }); + const renderer = renderBrowserControlNotifications({ controller, container: notifications, ...(options.branding === undefined ? {} : { branding: options.branding }) }); + + function text(parent: HTMLElement, tag: string, value: string): void { + const element = document.createElement(tag); + element.textContent = value; + parent.append(element); + } + function action(parent: HTMLElement, label: string, invoke: () => Promise): void { + const button = document.createElement('button'); + button.type = 'button'; + button.textContent = label; + button.onclick = async () => { + button.disabled = true; + try { + await invoke(); + } catch (error) { + const alert = document.createElement('p'); + alert.role = 'alert'; + alert.textContent = error instanceof Error ? error.message : String(error); + if (!disposed) parent.append(alert); + } finally { + button.disabled = false; + } + }; + parent.append(button); + } + function update(state: BrokerState): void { + if (disposed) return; + controller.update(state); + content.replaceChildren(); + text(content, 'h2', `Providers (${state.providers.length})`); + if (state.providers.length === 0) text(content, 'p', 'Connect a browser extension to begin.'); + for (const provider of state.providers) { + const row = document.createElement('article'); + text(row, 'strong', `${provider.name} · ${provider.state}`); + text(row, 'p', `${provider.targetCount} targets · version ${provider.version}`); + text(row, 'small', `Provider ${provider.id} · installation ${provider.instanceId}`); + action(row, 'Disconnect', async () => options.client.disconnectProvider(provider.id)); + action(row, 'Forget pairing', async () => options.client.disconnectProvider(provider.id, true)); + content.append(row); + } + text(content, 'h2', `Approved targets (${state.grants.length})`); + for (const grant of state.grants) { + const row = document.createElement('article'); + const target = state.targets.find(candidate => candidate.id === grant.targetId); + text(row, 'strong', `${grant.principalLabel} · ${grant.level} · ${grant.state}`); + text(row, 'p', target?.title ?? grant.targetId); + if (target?.url !== undefined) text(row, 'p', target.url); + text(row, 'small', `Navigation: ${grant.navigation} · approved origin: ${grant.approvedOrigin}`); + action(row, 'Revoke target', async () => options.client.revokeGrant(grant.id)); + content.append(row); + } + text(content, 'h2', `Active leases (${state.leases.length})`); + for (const lease of state.leases) text(content, 'p', `${lease.principalId} · ${lease.mode} · ${lease.methods.join(', ')}`); + } + try { + const stopWatching = await options.client.watch(update); + return { dispose() { + disposed = true; + stopWatching(); + renderer.dispose(); + controller.dispose(); + host.remove(); + } }; + } catch (error) { + renderer.dispose(); + controller.dispose(); + host.remove(); + throw error; + } +} + +/** Reads the panel projection hosted by createCdbPanel, over the supplied Devframe connection. */ +export function createBrowserControlPanelClient(client: CdbDevframeClient): BrowserControlPanelClient { + const rpc = client.scope('cdb:panel').rpc; + return { + snapshot: async () => rpc.call('state') as Promise, + async watch(listener) { + const state = await rpc.sharedState<{ broker: BrokerState }>('state'); + const update = (value: ReturnType | undefined): void => { + /** Devframe initializes shared state after the connection becomes trusted. */ + if (value !== undefined) listener(structuredClone(value.broker) as BrokerState); + }; + const unsubscribe = state.on('updated', update); + update(state.value()); + return unsubscribe; + }, + revokeScope: async requestId => rpc.call('revoke-scope', requestId) as Promise, + revokeGrant: async grantId => rpc.call('revoke-grant', grantId) as Promise, + disconnectProvider: async (providerId, forgetPairing = false) => rpc.call('disconnect-provider', providerId, forgetPairing) as Promise, + }; +} diff --git a/packages/devframe/src/service.ts b/packages/devframe/src/service.ts new file mode 100644 index 0000000..d972c03 --- /dev/null +++ b/packages/devframe/src/service.ts @@ -0,0 +1,249 @@ +import type { AgentToBrokerMessage, JsonValue } from '@dvcol/cdb'; +import type { BrokerDefinition, BrokerPeer, BrokerRuntime, ProviderAuthenticationInput, ProviderRegistration, ProviderTarget } from '@dvcol/cdb-broker'; +import type { GrantRequestClaim } from '@dvcol/cdb-broker/contract'; +import type { DevframeNodeRpcSession, DevframeRpcConnection, DevframeScopedNodeContext, DevframeServiceDefinition } from 'devframe'; + +import { Buffer } from 'node:buffer'; + +import { agentToBrokerMessageSchema } from '@dvcol/cdb'; +import { BrokerError, createBroker } from '@dvcol/cdb-broker'; + +import packageManifest from '../package.json' with { type: 'json' }; +import { cdbServiceScope } from './wire.js'; + +export interface CdbDevframePeerSession { + readonly meta: Omit; + readonly rpc: Pick; +} + +/** Gets the installed service without exposing the host context's transport types. */ +export function getCdbService(context: { readonly services: { get: (id: string) => unknown } }): CdbDevframeService { + const service = context.services.get('@dvcol/cdb-devframe'); + if (service === undefined) throw new Error('Install createCdbService in this Devframe context first.'); + return service as CdbDevframeService; +} + +export interface CdbServiceOptions { + readonly broker?: BrokerDefinition; + /** Adds host policy after Devframe authenticates the peer. */ + readonly authorizePeer?: (session: CdbDevframePeerSession, operation: string) => boolean; + readonly peerMetadata?: (session: CdbDevframePeerSession) => { readonly label?: string; readonly metadata?: JsonValue }; +} + +export interface CdbDevframeService { + readonly broker: BrokerRuntime; + /** Invokes a browser contribution selected by the embedding host's existing catalogue. */ + invoke: (session: CdbDevframePeerSession, input: CdbInvocation) => Promise; + dispose: () => Promise; + onPeerConnect: (connection: Pick, session: CdbDevframePeerSession) => void; + onPeerDisconnect: (connection: Pick) => Promise; +} + +export interface CdbInvocation { + readonly operationId: string; + readonly name: string; + readonly arguments: unknown; +} + +declare module 'devframe' { + interface DevframeServicesRegistry { + '@dvcol/cdb-devframe': CdbDevframeService; + } +} + +declare module 'devframe/types' { + interface DevframeServicesRegistry { + '@dvcol/cdb-devframe': CdbDevframeService; + } +} + +/** Install once for the Devframe context. The host forwards peer lifecycle and explicitly disposes it. */ +export function createCdbService(options: CdbServiceOptions = {}): DevframeServiceDefinition { + return { + package: '@dvcol/cdb-devframe', + version: packageManifest.version, + scope: cdbServiceScope, + async setup(context) { + const broker = await createBroker(options.broker); + try { + return installBroker(context, broker, options); + } catch (error) { + await broker.dispose(); + throw error; + } + }, + }; +} + +function installBroker(context: DevframeScopedNodeContext, broker: BrokerRuntime, options: CdbServiceOptions): CdbDevframeService { + const peers = new Map(); + const channels = new Map void; + receive: (message: unknown) => Promise; + }>(); + const subscriptions = new Map void>(); + const operations = new Map>(); + let disposed = false; + + function authorized(session: CdbDevframePeerSession, operation: string): BrokerPeer { + if (disposed || session.meta.isTrusted !== true || peers.get(session.meta.id)?.rpc !== session.rpc || options.authorizePeer?.(session, operation) === false) + throw new BrokerError('ACCESS_DENIED', 'An active authenticated Devframe peer is required for this browser operation.'); + return { ...options.peerMetadata?.(session), id: String(session.meta.id) }; + } + + function register(name: string, handler: (peer: BrokerPeer, session: CdbDevframePeerSession, ...arguments_: Arguments) => Result | Promise): void { + context.rpc.register({ + name, + type: 'action', + jsonSerializable: true, + handler: async (...arguments_: Arguments) => { + const session = context.rpc.getCurrentRpcSession(); + if (session === undefined) throw new BrokerError('ACCESS_DENIED', 'Browser RPC requires its actual peer session.'); + try { + return { ok: true, value: await handler(authorized(session, name), session, ...arguments_) }; + } catch (error) { + const failure = error instanceof Error ? error as Partial : {}; + return { ok: false, error: { code: failure.code ?? 'CDB_OPERATION_FAILED', message: error instanceof Error ? error.message : String(error), retryable: failure.retryable ?? false, ...(failure.details === undefined ? {} : { details: failure.details }), ...(failure.retryAfterMilliseconds === undefined ? {} : { retryAfterMilliseconds: failure.retryAfterMilliseconds }) } }; + } + }, + }); + } + + register('provider-register', (peer, _session, input: ProviderRegistration) => broker.registerProvider(peer, input)); + register('provider-pair', peer => broker.createPairingOffer(peer)); + register('provider-challenge', (peer, _session, input: ProviderAuthenticationInput) => broker.beginProviderAuthentication(peer, input)); + register('provider-authenticate', async (peer, session, proof: string) => { + const listeners = new Set<(message: AgentToBrokerMessage) => void>(); + const closed = Promise.withResolvers(); + let active = true; + const id = crypto.randomUUID(); + const channel = { + id, + close(this: void, code = 1000, reason = 'CDB channel closed') { + if (!active) return; + active = false; + listeners.clear(); + closed.resolve(); + void session.rpc.$callEvent(`${cdbServiceScope}:provider-closed`, { channelId: id, code, reason }).catch(() => {}); + }, + async receive(value: unknown) { + if (!active) throw new BrokerError('ACCESS_DENIED', 'The provider channel was closed.'); + if (Buffer.byteLength(JSON.stringify(value) ?? '') > (options.broker?.maximumProviderMessageBytes ?? 64 * 1_024 * 1_024)) + throw new BrokerError('MESSAGE_TOO_LARGE', 'The provider message exceeds the configured CDB limit.'); + const result = await agentToBrokerMessageSchema['~standard'].validate(value); + if (result.issues !== undefined) throw new BrokerError('ACCESS_DENIED', 'Invalid provider protocol message.'); + if (!active) throw new BrokerError('ACCESS_DENIED', 'The provider channel was replaced.'); + for (const listener of listeners) listener(result.value); + }, + }; + try { + const result = await broker.authenticateProvider(peer, proof, { + closed: closed.promise, + close: channel.close, + onMessage(listener) { + listeners.add(listener); + return () => listeners.delete(listener); + }, + async send(message) { + if (!active) throw new BrokerError('PROVIDER_RECOVERING', 'The provider channel was closed.', true); + await session.rpc.$callEvent(`${cdbServiceScope}:provider-frame`, { channelId: id, message }); + }, + }); + authorized(session, 'provider-authenticate'); + channels.get(session.meta.id)?.close(4000, 'CDB channel replaced'); + channels.set(session.meta.id, channel); + return { ...result, channelId: id }; + } catch (error) { + channel.close(); + throw error; + } + }); + register('provider-frame', async (_peer, session, envelope: { readonly channelId: string; readonly message: unknown }) => { + const channel = channels.get(session.meta.id); + if (channel === undefined || channel.id !== envelope.channelId) throw new BrokerError('ACCESS_DENIED', 'The provider channel belongs to another connection.'); + await channel.receive(envelope.message); + }); + register('provider-close', (_peer, session, channelId: string) => { + const channel = channels.get(session.meta.id); + if (channel?.id === channelId) { + channel.close(); + channels.delete(session.meta.id); + } + }); + register('provider-reconcile', async (peer, _session, targets: readonly ProviderTarget[]) => broker.reconcileProvider(peer, targets)); + register('provider-claim', (peer, _session, requestId: string) => broker.claimRequest(peer, requestId)); + register('provider-release', (peer, _session, claim: GrantRequestClaim) => broker.releaseClaim(peer, claim)); + register('provider-approve', async (peer, _session, claim: GrantRequestClaim, targets: readonly ProviderTarget[]) => broker.completeClaim(peer, claim, targets)); + register('provider-scope', async (peer, _session, requestId: string, targets: readonly ProviderTarget[]) => broker.reconcileScope(peer, requestId, targets)); + register('session-connect', async (peer, _session, input: Parameters[1]) => broker.connectSession(peer, input)); + register('session-terminate', async peer => broker.terminateSession(peer)); + register('state', () => broker.snapshot()); + register('watch', (_peer, session) => { + subscriptions.get(session.meta.id)?.(); + subscriptions.set(session.meta.id, broker.subscribe((state) => { + authorized(session, 'watch'); + void session.rpc.$callEvent(`${cdbServiceScope}:state-changed`, state).catch(() => {}); + })); + return broker.snapshot(); + }); + register('unwatch', (_peer, session) => { + subscriptions.get(session.meta.id)?.(); + subscriptions.delete(session.meta.id); + }); + register('revoke-scope', async (_peer, _session, requestId: string) => broker.revokeScope(requestId)); + register('revoke-grant', async (_peer, _session, grantId: string) => broker.revokeGrant(grantId)); + register('disconnect-provider', async (_peer, _session, providerId: string, forgetPairing: boolean = false) => broker.disconnectProvider(providerId, forgetPairing)); + async function invoke(session: CdbDevframePeerSession, input: CdbInvocation): Promise { + const peer = authorized(session, 'invoke'); + let pending = operations.get(session.meta.id); + if (pending === undefined) { + pending = new Map(); + operations.set(session.meta.id, pending); + } + if (!input.operationId || pending.has(input.operationId)) throw new BrokerError('ACCESS_DENIED', 'A unique operation identifier is required.'); + const controller = new AbortController(); + pending.set(input.operationId, controller); + try { + return await broker.invoke(peer, input.name, input.arguments, { signal: controller.signal }); + } finally { + pending.delete(input.operationId); + if (pending.size === 0) operations.delete(session.meta.id); + } + } + register('invoke', async (_peer, session, input: CdbInvocation) => invoke(session, input)); + register('cancel', (_peer, session, operationId: string) => { + operations.get(session.meta.id)?.get(operationId)?.abort(new BrokerError('MCP_ACTION_CANCELLED', 'The browser operation was cancelled.')); + }); + + return { + broker, + invoke, + onPeerConnect(connection, session) { + if (disposed) throw new BrokerError('GRANT_REVOKED', 'The CDB service was disposed.'); + peers.set(connection.id, session); + }, + async onPeerDisconnect(connection) { + peers.delete(connection.id); + subscriptions.get(connection.id)?.(); + subscriptions.delete(connection.id); + for (const operation of operations.get(connection.id)?.values() ?? []) operation.abort(); + operations.delete(connection.id); + channels.get(connection.id)?.close(1006, 'Devframe peer disconnected'); + channels.delete(connection.id); + await broker.disconnectPeer(String(connection.id)); + }, + async dispose() { + if (disposed) return; + disposed = true; + for (const unsubscribe of subscriptions.values()) unsubscribe(); + for (const pending of operations.values()) for (const operation of pending.values()) operation.abort(); + for (const channel of channels.values()) channel.close(1001, 'CDB service stopped'); + subscriptions.clear(); + operations.clear(); + channels.clear(); + peers.clear(); + await broker.dispose(); + }, + }; +} diff --git a/packages/devframe/src/wire.ts b/packages/devframe/src/wire.ts new file mode 100644 index 0000000..f96bf98 --- /dev/null +++ b/packages/devframe/src/wire.ts @@ -0,0 +1,47 @@ +import type { BrokerToAgentMessage } from '@dvcol/cdb'; +import type { BrokerState } from '@dvcol/cdb-broker/contract'; +import type { DevframeScopedClientRpc } from 'devframe/client'; +import type {} from 'devframe/types'; + +/** Only the existing scoped RPC operations are required, not Devframe's transport or context internals. */ +export interface CdbDevframeClient { + scope: (namespace: string) => { readonly rpc: Pick & { + register: (definition: Pick[0], 'name' | 'type' | 'handler'>) => void; + }; }; +} + +export const cdbServiceScope = 'cdb:broker'; + +export type CdbReply = { readonly ok: true; readonly value: Value } | { + readonly ok: false; + readonly error: { readonly code: string; readonly message: string; readonly retryable: boolean; readonly retryAfterMilliseconds?: number; readonly details?: Readonly> }; +}; + +export interface CdbProviderFrame { + readonly channelId: string; + readonly message: BrokerToAgentMessage; +} + +export interface CdbProviderClosure { + readonly channelId: string; + readonly code: number; + readonly reason: string; +} + +interface CdbRpcClientFunctions { + 'cdb:broker:provider-frame': (frame: CdbProviderFrame) => void; + 'cdb:broker:provider-closed': (closure: CdbProviderClosure) => void; + 'cdb:broker:state-changed': (state: BrokerState) => void; +} + +declare module 'devframe' { + interface DevframeRpcClientFunctions extends CdbRpcClientFunctions {} + interface DevframeServicesScopeRegistry { + '@dvcol/cdb-devframe': typeof cdbServiceScope; + } +} + +/** DevTools Kit 0.6.1 consumes this legacy type entry. */ +declare module 'devframe/types' { + interface DevframeRpcClientFunctions extends CdbRpcClientFunctions {} +} diff --git a/packages/devframe/test/page-script.test.ts b/packages/devframe/test/page-script.test.ts new file mode 100644 index 0000000..043ecf8 --- /dev/null +++ b/packages/devframe/test/page-script.test.ts @@ -0,0 +1,51 @@ +// @vitest-environment jsdom +import type { BrokerState } from '@dvcol/cdb-broker/contract'; + +import type { BrowserControlPageContext } from '../src/page-script.js'; + +import { expect, it, vi } from 'vitest'; + +import setupBrowserControlPage from '../src/page-script.js'; +import * as panel from '../src/panel.js'; + +it('uses native hub notifications and removes actions when a request ends', async () => { + expect.assertions(8); + const state: BrokerState = { revision: 1, providers: [], principals: [], targets: [], grants: [], scopes: [], leases: [], requests: [{ id: 'request', principalId: 'principal', principalLabel: 'Agent', level: 'interact', navigation: 'same-origin', state: 'pending', createdAt: 0, expiresAt: null }] }; + let update: (state: BrokerState) => void = () => {}; + const stopWatching = vi.fn(); + const dismiss = vi.fn(async () => {}); + const unregister = vi.fn(); + const register = vi.fn(() => unregister); + const info = vi.fn(async () => ({ dismiss })); + const dispatch = vi.spyOn(window, 'dispatchEvent'); + const client = vi.spyOn(panel, 'createBrowserControlPanelClient').mockReturnValue({ + snapshot: () => state, + watch: (listener) => { + update = listener; + listener(state); + return stopWatching; + }, + revokeScope: async () => {}, + revokeGrant: async () => true, + disconnectProvider: async () => true, + }); + const dispose = await setupBrowserControlPage({ rpc: {} as BrowserControlPageContext['rpc'], current: { domElements: {} }, commands: { register }, messages: { info } }); + try { + expect(info).toHaveBeenCalledWith('Browser control requested', expect.objectContaining({ notify: true, autoDismiss: false, description: 'Agent requests interact access with same-origin navigation.' })); + expect(document.querySelector('aside')).toBeNull(); + expect(document.documentElement.hasAttribute('data-cdb-notifications-ready')).toBe(true); + const command = register.mock.calls[0]![0]; + await command.action(); + expect(dispatch).toHaveBeenCalledWith(expect.objectContaining({ type: 'cdb:review-request', detail: { requestId: 'request' } })); + update({ ...state, requests: [] }); + await vi.waitUntil(() => dismiss.mock.calls.length === 1); + expect(unregister).toHaveBeenCalledOnce(); + expect(dismiss).toHaveBeenCalledOnce(); + } finally { + dispose(); + client.mockRestore(); + dispatch.mockRestore(); + } + expect(stopWatching).toHaveBeenCalledOnce(); + expect(document.documentElement.hasAttribute('data-cdb-notifications-ready')).toBe(false); +}); diff --git a/packages/devframe/test/panel-view.test.ts b/packages/devframe/test/panel-view.test.ts new file mode 100644 index 0000000..1884661 --- /dev/null +++ b/packages/devframe/test/panel-view.test.ts @@ -0,0 +1,38 @@ +import type { BrokerState } from '@dvcol/cdb-broker/contract'; + +import { expect, it } from 'vitest'; + +import { buildBrowserControlPanelView } from '../src/panel-view.js'; + +const state: BrokerState = { + revision: 1, + providers: [{ id: 'provider', instanceId: 'installation', name: 'Fixture browser', version: '1.0.0', maximumLevel: 'debug', paired: true, state: 'ready', targetCount: 1 }], + principals: [{ id: 'client', label: 'Fixture client', connectedAt: 1, metadata: { privateDetail: 'not-for-display' } }], + requests: [], + grants: [{ id: 'grant', requestId: 'request', principalId: 'client', principalLabel: 'Fixture client', providerId: 'provider', targetId: 'target', targetGeneration: 1, level: 'interact', navigation: 'same-origin', approvedOrigin: 'https://fixture.test', createdAt: 1, state: 'active' }], + targets: [{ id: 'target', generation: 1, scopeId: 'scope', title: 'Fixture page', url: 'https://fixture.test', providerId: 'provider', state: 'available' }], + scopes: [], + leases: [], +}; + +it('projects summary counts, tabbed access and exact management actions without private metadata', () => { + expect.assertions(8); + const view = buildBrowserControlPanelView(state); + expect(view.elements['summary-providers-value']?.props).toMatchObject({ text: '1' }); + expect(view.elements['summary-access-value']?.props).toMatchObject({ text: '1' }); + expect(view.elements.tabs?.children).toEqual(['requests', 'access', 'providers', 'operations']); + expect(view.elements['grant-grant-revoke']?.on).toEqual({ press: { action: 'cdb:panel:revoke-target', params: { grantId: 'grant' } } }); + expect(view.elements['provider-provider-forget']?.on).toEqual({ press: { action: 'cdb:panel:forget', params: { providerId: 'provider' } } }); + expect(view.elements['provider-provider-details']?.props).toMatchObject({ collapsible: true, defaultCollapsed: true }); + expect(JSON.stringify(view)).not.toContain('not-for-display'); + expect(view.elements['grant-grant']?.props).toEqual({ title: 'Fixture page' }); +}); + +it('lets a host replace component types and styling while retaining content and action bindings', () => { + expect.assertions(4); + const view = buildBrowserControlPanelView(state, { Card: { type: 'HostCard', props: { interactive: true, variant: 'primary' } } }); + expect(view.elements['grant-grant']?.type).toBe('HostCard'); + expect(view.elements['grant-grant']?.props).toEqual({ title: 'Fixture page', interactive: true, variant: 'primary' }); + expect(view.elements['grant-grant']?.children).toEqual(['grant-grant-body']); + expect(view.elements['grant-grant-revoke']?.on).toEqual({ press: { action: 'cdb:panel:revoke-target', params: { grantId: 'grant' } } }); +}); diff --git a/packages/devframe/test/panel.test.ts b/packages/devframe/test/panel.test.ts new file mode 100644 index 0000000..3f8aea9 --- /dev/null +++ b/packages/devframe/test/panel.test.ts @@ -0,0 +1,31 @@ +// @vitest-environment jsdom +import type { BrokerState } from '@dvcol/cdb-broker/contract'; + +import type { BrowserControlPanelClient } from '../src/panel.js'; + +import { expect, it, vi } from 'vitest'; + +import { mountBrowserControlPanel } from '../src/panel.js'; + +it('uses supplied management actions and releases its subscription without disposing the broker connection', async () => { + expect.assertions(6); + const state: BrokerState = { revision: 1, providers: [{ id: 'provider', instanceId: 'installation', name: 'Fixture provider', version: '1.0.0', maximumLevel: 'debug', paired: true, state: 'ready', targetCount: 0 }], principals: [], requests: [], grants: [], scopes: [], targets: [], leases: [] }; + const unsubscribe = vi.fn(); + const disconnect = vi.fn(async () => true); + const client: BrowserControlPanelClient = { snapshot: () => state, watch(listener) { + listener(state); + return unsubscribe; + }, revokeScope: async () => {}, revokeGrant: async () => true, disconnectProvider: disconnect }; + const container = document.createElement('div'); + const panel = await mountBrowserControlPanel({ client, container, onReview: async () => {} }); + const root = container.querySelector('section')!.shadowRoot!; + expect(root.textContent).toContain('Fixture provider'); + expect(root.textContent).toContain('installation'); + [...root.querySelectorAll('button')].find(button => button.textContent === 'Disconnect')!.click(); + await vi.waitUntil(() => disconnect.mock.calls.length === 1); + expect(disconnect).toHaveBeenCalledWith('provider'); + panel.dispose(); + expect(unsubscribe).toHaveBeenCalledOnce(); + expect(container.children).toHaveLength(0); + expect(client.snapshot()).toEqual(state); +}); diff --git a/packages/devframe/test/service.test.ts b/packages/devframe/test/service.test.ts new file mode 100644 index 0000000..7e1bb52 --- /dev/null +++ b/packages/devframe/test/service.test.ts @@ -0,0 +1,173 @@ +import type { PublishedTarget } from '@dvcol/cdb'; +import type { ProviderConnection, StoredBrokerPairing } from '@dvcol/cdb-extension'; +import type { DevframeRpcClient } from 'devframe/client'; + +import type { CdbDevframeService } from '../src/service.js'; + +import { mkdtemp, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +import { createMemoryCredentialStore } from '@dvcol/cdb'; +import { defineDevframe } from 'devframe'; +import { createDevServer } from 'devframe/adapters/dev'; +import { connectDevframe } from 'devframe/client'; +import { DEVFRAME_WS_ROUTE } from 'devframe/constants'; +import { afterEach, expect, it, vi } from 'vitest'; + +import { createCdbClient, createCdbClientSession } from '../src/client.js'; +import { createCdbService } from '../src/service.js'; + +const cleanups: (() => Promise | void)[] = []; +afterEach(async () => { + for (const cleanup of cleanups.splice(0).reverse()) await cleanup(); + vi.unstubAllEnvs(); + vi.unstubAllGlobals(); +}); + +async function fixture() { + const directory = await mkdtemp(join(tmpdir(), 'cdb-devframe-test-')); + cleanups.push(async () => rm(directory, { recursive: true, force: true })); + vi.stubEnv('DEVFRAME_INSTANCES_DIR', directory); + let service: CdbDevframeService | undefined; + const server = await createDevServer(defineDevframe({ + id: 'cdb-public-test', + name: 'CDB public transport test', + version: '1.0.0', + packageName: 'cdb-test', + description: 'Public CDB shared-RPC regression.', + homepage: 'https://example.test', + services: [createCdbService({ broker: { timing: { requestRateLimitMilliseconds: 0 } } })], + async setup(context) { + service = context.services.get('@dvcol/cdb-devframe'); + context.rpc.register({ name: 'fixture:echo', type: 'query', handler: (value: string) => value }); + context.rpc.register({ name: 'fixture:browser', type: 'action', handler: async (input: Parameters[1]) => { + try { + return { value: await service!.invoke(context.rpc.getCurrentRpcSession()!, input) }; + } catch (error) { + return { error: { code: error instanceof Error && 'code' in error ? error.code : undefined } }; + } + } }); + }, + }), { + host: '127.0.0.1', + port: 0, + auth: false, + mcp: false, + openBrowser: false, + onPeerConnect: (connection, session) => service!.onPeerConnect(connection, session), + onPeerDisconnect: (connection) => { + void service!.onPeerDisconnect(connection); + }, + }); + cleanups.push(async () => { + await server.close(); + await service?.dispose(); + }); + async function connect(): Promise { + vi.stubGlobal('location', new URL(`http://127.0.0.1:${server.port}/`)); + const client = await connectDevframe({ baseURL: `http://127.0.0.1:${server.port}/`, simpleAuth: false, connectionMeta: { backend: 'websocket', websocket: { path: DEVFRAME_WS_ROUTE } } }); + cleanups.push(() => client.close?.()); + await client.ensureTrusted(); + return client; + } + return { connect, service: service! }; +} + +async function publish(connection: ProviderConnection): Promise { + const target: PublishedTarget = { id: crypto.randomUUID(), generation: 1, type: 'page', scopeId: crypto.randomUUID(), capabilities: { level: 'debug' }, availability: 'available', url: 'https://example.test' }; + await connection.send({ kind: 'request', method: 'agent.hello', requestId: crypto.randomUUID(), protocolVersion: 1, parameters: { + connectionGeneration: connection.generation, + protocolVersions: { minimum: 1, maximum: 1 }, + features: [], + implementation: { instanceId: connection.registration.instanceId, name: connection.registration.name, version: connection.registration.version, role: 'agent' }, + heartbeat: { intervalMilliseconds: 15_000, timeoutMilliseconds: 45_000 }, + limits: { maximumArtifactBytes: 16_777_216, maximumInlineResultBytes: 65_536, maximumMessageBytes: 67_108_864 }, + } }); + await connection.send({ kind: 'notification', method: 'targets.publish', protocolVersion: 1, parameters: { target } }); + await connection.reconcile([target]); + return target; +} + +it('rotates a principal resume credential across peers and starts fresh after its session ends', async () => { + expect.assertions(9); + const setup = await fixture(); + const store = createMemoryCredentialStore(); + const session = createCdbClientSession({ credentialKey: 'agent', credentialStore: store, metadata: { workspace: 'fixture' } }); + const first = createCdbClient(await setup.connect()); + await session.connect(first); + const initial = await store.get('agent'); + const principal = setup.service.broker.snapshot().principals[0]!; + expect(principal.metadata).toEqual({ workspace: 'fixture' }); + const next = createCdbClient(await setup.connect()); + await store.set('agent', { ...initial!, credential: 'invalid-resume-credential' }); + await expect(session.connect(next)).rejects.toMatchObject({ code: 'SESSION_CREDENTIAL_INVALID' }); + expect((await store.get('agent'))?.credential).toBe('invalid-resume-credential'); + await store.set('agent', initial!); + await session.connect(next); + const resumed = await store.get('agent'); + expect(resumed?.logicalSessionId).toBe(initial?.logicalSessionId); + expect(resumed?.credential).not.toBe(initial?.credential); + expect(setup.service.broker.snapshot().principals.map(value => value.id)).toEqual([principal.id]); + await next.terminateSession(); + const replacement = createCdbClient(await setup.connect()); + await session.connect(replacement); + expect((await store.get('agent'))?.logicalSessionId).not.toBe(initial?.logicalSessionId); + await session.terminate(replacement); + expect(await store.get('agent')).toBeUndefined(); + expect(setup.service.broker.snapshot().principals).toEqual([]); +}, 20_000); + +it.each(['direct', 'host-catalogue'] as const)('cancels %s browser work over an existing RPC peer while ordinary traffic continues', async (route) => { + expect.assertions(8); + const setup = await fixture(); + const providerRpc = await setup.connect(); + const providerClient = createCdbClient(providerRpc); + const pairings = new Map(); + const provider = await providerClient.connectProvider({ + registration: { id: 'fixture-provider', instanceId: crypto.randomUUID(), maximumLevel: 'debug', name: 'Public provider', version: '1.0.0' }, + pairingKey: 'fixture-broker', + confirmPairing: () => true, + pairingStore: { load: async key => pairings.get(key), save: async (pairing) => { + pairings.set(pairing.endpoint, pairing); + }, remove: async (credentialId) => { + for (const [key, pairing] of pairings) if (pairing.credentialId === credentialId) pairings.delete(key); + } }, + }); + const target = await publish(provider); + const agentRpc = await setup.connect(); + const agent = createCdbClient(agentRpc); + const access = agent.invoke('browser.request_access', { level: 'interact' }); + await expect.poll(() => setup.service.broker.snapshot().requests.length).toBe(1); + const { claim } = await provider.claim(setup.service.broker.snapshot().requests[0]!.id); + await provider.approve(claim, [target]); + expect(await access).toMatchObject({ target: { targetRef: 't1' } }); + const controller = new AbortController(); + const commandSeen = Promise.withResolvers(); + const cancellationSeen = Promise.withResolvers(); + provider.onMessage((message) => { + if (message.kind === 'request' && message.method === 'cdp.execute') commandSeen.resolve(); + if (message.kind === 'notification' && message.method === 'cdp.cancel') cancellationSeen.resolve(); + }); + const action = (route === 'direct' + ? agent.invoke('browser.snapshot', { targetRef: 't1' }, controller.signal) + : agent.withCancellation(controller.signal, async (operationId) => { + const result = await agentRpc.scope('fixture').rpc.call('browser', { operationId, name: 'browser.snapshot', arguments: { targetRef: 't1' } }) as { error?: { code: string }; value?: unknown }; + if (result.error !== undefined) throw Object.assign(new Error('Host catalogue reported a browser failure.'), result.error); + return result.value; + })) + .catch(error => error as Error); + await Promise.race([commandSeen.promise, action.then((result) => { + throw result; + })]); + controller.abort(); + await cancellationSeen.promise; + expect(await action).toMatchObject({ code: 'MCP_ACTION_CANCELLED' }); + expect(await agentRpc.scope('fixture').rpc.call('echo', 'ordinary traffic')).toBe('ordinary traffic'); + expect(await providerRpc.scope('fixture').rpc.call('echo', 'provider traffic')).toBe('provider traffic'); + expect(setup.service.broker.snapshot().leases).toEqual([]); + await agent.dispose(); + expect(await agentRpc.scope('fixture').rpc.call('echo', 'after disposal')).toBe('after disposal'); + provider.close(); + expect(await providerRpc.scope('fixture').rpc.call('echo', 'after channel closure')).toBe('after channel closure'); +}, 20_000); diff --git a/packages/devframe/tsconfig.json b/packages/devframe/tsconfig.json new file mode 100644 index 0000000..1e5a4d0 --- /dev/null +++ b/packages/devframe/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "lib": [ + "ES2024", + "DOM" + ], + "types": [ + "node" + ] + }, + "include": [ + "src/**/*.ts" + ] +} diff --git a/packages/devframe/tsdown.config.ts b/packages/devframe/tsdown.config.ts new file mode 100644 index 0000000..29af20e --- /dev/null +++ b/packages/devframe/tsdown.config.ts @@ -0,0 +1,3 @@ +import { defineConfig } from 'tsdown'; + +export default defineConfig({ deps: { neverBundle: true }, dts: { sourcemap: true }, entry: ['src/index.ts', 'src/client.ts', 'src/panel.ts', 'src/page-script.ts'], format: 'esm', platform: 'node', sourcemap: true, target: 'es2024' }); diff --git a/packages/devframe/vite.config.ts b/packages/devframe/vite.config.ts new file mode 100644 index 0000000..81ded33 --- /dev/null +++ b/packages/devframe/vite.config.ts @@ -0,0 +1,18 @@ +import { fileURLToPath } from 'node:url'; + +import { defineConfig } from 'vite'; + +export default defineConfig({ + base: './', + root: fileURLToPath(new URL('./src/', import.meta.url)), + build: { + outDir: fileURLToPath(new URL('./dist/view/', import.meta.url)), + emptyOutDir: true, + target: 'chrome125', + rollupOptions: { + preserveEntrySignatures: 'strict', + input: { 'page-script': fileURLToPath(new URL('./src/page-script.ts', import.meta.url)) }, + output: { entryFileNames: '[name].js' }, + }, + }, +}); diff --git a/packages/devframe/vitest.config.ts b/packages/devframe/vitest.config.ts new file mode 100644 index 0000000..e9153ff --- /dev/null +++ b/packages/devframe/vitest.config.ts @@ -0,0 +1,3 @@ +import { defineProject } from 'vitest/config'; + +export default defineProject({ test: { environment: 'node', include: ['test/**/*.test.ts'], name: 'unit-node:devframe' } }); diff --git a/packages/extension/README.md b/packages/extension/README.md index 0f2fdd3..a7ce807 100644 --- a/packages/extension/README.md +++ b/packages/extension/README.md @@ -11,3 +11,15 @@ import { createBirpcAgentBootstrap } from '@dvcol/cdb-extension/bootstrap'; ``` The embedding extension owns Chrome selection policy and approval UI. See the [repository documentation](https://github.com/dvcol/chrome-debugger-bridge#readme) for the provider trust boundary. + +## Optional Chrome adapter + +Import `createChromeProvider` and `getChromeProviderIdentity` from `@dvcol/cdb-extension/chrome` to compose debugger, tab, navigation, alarm and storage bindings. Supply a `connect` callback returning a provider connection, `maximumLevel`, and an `authorizeApproval` callback that validates the final approval source and tab-selection policy. A page message alone does not establish a human decision. + +Use `provider.approve(requestId, selector, approvalContext)` from the trusted approval channel. The adapter shares one publisher per tab across overlapping scopes, reconciles live group/window membership, renews target generations on top-level navigation, and recovers through its supplied connection factory. `provider.dispose()` revokes its scopes and detaches publishers before closing its CDB channel. + +The host chooses installation, recovery and pairing storage keys. Preserve those keys when adopting the adapter. Chrome session storage supports publication recovery but is never itself proof of approved authority. + +The `@dvcol/cdb-extension/notifications` entry supplies a headless notification controller and an optional neutral shadow-root renderer. Both derive state from the broker; local dismissal never changes authority. Review actions open the host's final approval UI. Branding and CSS are optional renderer inputs. + +See the [Devframe example](../../examples/devframe/README.md) for a complete composition over an existing RPC peer. diff --git a/packages/extension/package.json b/packages/extension/package.json index 7a91c62..83c83c6 100644 --- a/packages/extension/package.json +++ b/packages/extension/package.json @@ -39,6 +39,14 @@ "./testing": { "types": "./dist/testing.d.ts", "import": "./dist/testing.js" + }, + "./chrome": { + "types": "./dist/chrome.d.ts", + "import": "./dist/chrome.js" + }, + "./notifications": { + "types": "./dist/notifications.d.ts", + "import": "./dist/notifications.js" } }, "files": [ @@ -54,7 +62,8 @@ "typecheck": "tsc --noEmit --project tsconfig.json" }, "dependencies": { - "@dvcol/cdb": "workspace:^" + "@dvcol/cdb": "workspace:^", + "@dvcol/cdb-broker": "workspace:^" }, "publishConfig": { "access": "public" diff --git a/packages/extension/src/agent-recovery.ts b/packages/extension/src/agent-recovery.ts index a472759..c641f00 100644 --- a/packages/extension/src/agent-recovery.ts +++ b/packages/extension/src/agent-recovery.ts @@ -13,6 +13,8 @@ export interface CreateAgentRecoveryOptions void; + /** Reports connection, reconciliation and heartbeat failures to the embedding host. */ + readonly onError?: (error: unknown) => void; /** Completes agent.hello and returns its negotiated liveness parameters when heartbeats are enabled. */ readonly reconcile: (connection: Connection, connectionGeneration: number) => Promise; readonly schedule?: (task: () => void, delayMilliseconds: number) => ReturnType; @@ -62,8 +64,9 @@ export function createAgentRecovery { scheduleHeartbeat(connection, generation, parameters); - }).catch(() => { + }).catch((error) => { connection.close(3001, 'Agent heartbeat failed'); + options.onError?.(error); }); }, parameters.intervalMilliseconds); } @@ -71,9 +74,10 @@ export function createAgentRecovery { if (stopped) return; setState('locating'); + let candidateConnection: Connection | undefined; try { setState('connecting'); - const candidateConnection = await options.connect(++connectionGeneration); + candidateConnection = await options.connect(++connectionGeneration); if (stopped) { candidateConnection.close(1000, 'Agent recovery stopped'); return; @@ -98,9 +102,11 @@ export function createAgentRecovery PublicChildSession; detach: (chromeSessionId: string) => PublicChildSession | undefined; list: () => readonly PublicChildSession[]; publicSessionForChromeId: (chromeSessionId: string) => PublicChildSession | undefined; resolve: (publicSessionId: string) => string | undefined; + /** Invalidates public references while preserving Chrome sessions that have not detached. */ + renew: () => void; revoke: () => readonly PublicChildSession[]; } @@ -22,10 +25,11 @@ export interface ChildSessionRouter { export function createChildSessionRouter(): ChildSessionRouter { const chromeSessionIdByPublicId = new Map(); const publicSessionByChromeId = new Map(); + const parentByChromeId = new Map(); let generation = 0; return { - attach(chromeSessionId, metadata = {}) { + attach(chromeSessionId, metadata = {}, parentChromeSessionId) { const existing = publicSessionByChromeId.get(chromeSessionId); if (existing !== undefined) return existing; const sessionMetadata = typeof metadata === 'string' ? { type: metadata } : metadata; @@ -38,13 +42,21 @@ export function createChildSessionRouter(): ChildSessionRouter { }; chromeSessionIdByPublicId.set(session.id, chromeSessionId); publicSessionByChromeId.set(chromeSessionId, session); + if (parentChromeSessionId !== undefined) parentByChromeId.set(chromeSessionId, parentChromeSessionId); return session; }, detach(chromeSessionId) { const session = publicSessionByChromeId.get(chromeSessionId); if (session === undefined) return undefined; - publicSessionByChromeId.delete(chromeSessionId); - chromeSessionIdByPublicId.delete(session.id); + const detached = [chromeSessionId]; + while (detached.length > 0) { + const current = detached.pop()!; + const child = publicSessionByChromeId.get(current); + if (child !== undefined) chromeSessionIdByPublicId.delete(child.id); + publicSessionByChromeId.delete(current); + parentByChromeId.delete(current); + for (const [childId, parentId] of parentByChromeId) if (parentId === current) detached.push(childId); + } return session; }, list() { @@ -56,10 +68,19 @@ export function createChildSessionRouter(): ChildSessionRouter { resolve(publicSessionId) { return chromeSessionIdByPublicId.get(publicSessionId); }, + renew() { + chromeSessionIdByPublicId.clear(); + for (const [chromeSessionId, previous] of publicSessionByChromeId) { + const session = { ...previous, generation: ++generation, id: globalThis.crypto.randomUUID() }; + publicSessionByChromeId.set(chromeSessionId, session); + chromeSessionIdByPublicId.set(session.id, chromeSessionId); + } + }, revoke() { const sessions = [...publicSessionByChromeId.values()]; chromeSessionIdByPublicId.clear(); publicSessionByChromeId.clear(); + parentByChromeId.clear(); return sessions; }, }; diff --git a/packages/extension/src/chrome.ts b/packages/extension/src/chrome.ts new file mode 100644 index 0000000..4b933af --- /dev/null +++ b/packages/extension/src/chrome.ts @@ -0,0 +1,496 @@ +import type { AgentToBrokerMessage, BrokerToAgentMessage, HeartbeatParameters, JsonObject, PublishedTarget } from '@dvcol/cdb'; +import type { AccessLevel, BrokerRequest, BrokerState, ProviderTarget } from '@dvcol/cdb-broker/contract'; + +import type { AgentRecoveryState } from './agent-recovery.js'; +import type { AgentControlPresentationEvent } from './presentation.js'; +import type { ProviderConnection } from './provider-connection.js'; +import type { CommandAuthorizationPolicy, SelectedTab, SelectedTabPublisher } from './selected-tab-publisher.js'; +import type { ChromeTabScopeLifecyclePort } from './tab-scope-lifecycle.js'; +import type { TabScopeSelector } from './tab-scope.js'; + +import { sendAgentHeartbeat } from './agent-heartbeat.js'; +import { createAgentRecovery } from './agent-recovery.js'; +import { createSelectedTabPublisher } from './selected-tab-publisher.js'; +import { createTabScopeManager } from './tab-scope-manager.js'; +import { parseTabScopeSelector } from './tab-scope.js'; + +interface ChromeEvent { + addListener: (listener: (...arguments_: Arguments) => void) => void; + removeListener: (listener: (...arguments_: Arguments) => void) => void; +} + +function projectEvent(event: ChromeEvent, project: (...arguments_: SourceArguments) => ResultArguments): ChromeEvent { + const listeners = new Map<(...arguments_: ResultArguments) => void, (...arguments_: SourceArguments) => void>(); + return { + addListener(listener) { + if (listeners.has(listener)) return; + const projected = (...arguments_: SourceArguments): void => { + listener(...project(...arguments_)); + }; + listeners.set(listener, projected); + event.addListener(projected); + }, + removeListener(listener) { + const projected = listeners.get(listener); + if (projected !== undefined) { + event.removeListener(projected); + listeners.delete(listener); + } + }, + }; +} + +function selectedTab(tab: chrome.tabs.Tab): SelectedTab { + if (tab.id === undefined) throw new Error('The Chrome tab has no stable identifier.'); + return { + tabId: tab.id, + incognito: tab.incognito, + active: tab.active, + windowId: tab.windowId, + ...(tab.groupId === undefined ? {} : { groupId: tab.groupId }), + ...(tab.title === undefined ? {} : { title: tab.title }), + ...(tab.url === undefined ? {} : { url: tab.url }), + }; +} + +/** Converts Chrome's tab events to the shared publication contract. */ +export function createChromeTabBindings(platform: typeof chrome = chrome): ChromeTabScopeLifecyclePort { + return { + tabs: { + query: async query => (await platform.tabs.query(query)).filter(tab => tab.id !== undefined).map(selectedTab), + onCreated: projectEvent(platform.tabs.onCreated, (tab): [SelectedTab] => [selectedTab(tab)]), + onUpdated: projectEvent(platform.tabs.onUpdated, (tabId, change, tab): [number, unknown, SelectedTab] => [tabId, change, selectedTab(tab)]), + onRemoved: platform.tabs.onRemoved, + }, + tabGroups: platform.tabGroups, + windows: platform.windows, + }; +} + +export interface ChromeProviderOptions { + readonly chrome?: typeof chrome; + readonly connect: () => Promise; + readonly maximumLevel: AccessLevel; + /** Must validate the host's final approval source and tab-selection policy. Page messages alone are insufficient. */ + readonly authorizeApproval: (request: BrokerRequest, selector: TabScopeSelector, context: ApprovalContext) => boolean | Promise; + readonly isExposureAllowed?: (tab: Omit) => boolean; + readonly commandAuthorizationPolicy?: CommandAuthorizationPolicy; + readonly targetMetadata?: (tab: SelectedTab) => ProviderTarget['metadata']; + readonly onState?: (state: BrokerState) => void; + readonly onError?: (error: unknown) => void; + readonly onPresentation?: (tabId: number, event: AgentControlPresentationEvent) => void; + /** Host-selected alarm name. Omit when the host already keeps the background process alive. */ + readonly recoveryAlarmName?: string; + /** Uses Chrome session storage for publication continuity, never as proof of approval. */ + readonly recoveryStorageKey?: string; +} + +interface ProviderRecoveryState { + readonly version: 1; + readonly brokerId: string; + readonly scopes: readonly { readonly requestId: string; readonly selector: TabScopeSelector }[]; + readonly targets: readonly { readonly tabId: number; readonly id: string; readonly generation: number; readonly scopeId: string; readonly documentId?: string }[]; +} + +interface ControlledTab { + readonly publisher: SelectedTabPublisher; + tab: SelectedTab; + target?: PublishedTarget; + documentId?: string; +} + +export interface ChromeProvider { + readonly state: AgentRecoveryState; + start: () => void; + approve: (requestId: string, selector: TabScopeSelector, context: ApprovalContext) => Promise; + revoke: (requestId: string) => Promise; + dispose: () => Promise; +} + +/** Composes shared publications, claims, navigation renewal, Chrome events and recoverable provider transport. */ +export function createChromeProvider(options: ChromeProviderOptions): ChromeProvider { + const platform = options.chrome ?? chrome; + const tabs = new Map(); + const scopes = new Map(); + const approved = new Set(); + const commands = new Map(); + const tabOperations = new Map>(); + const renewingTabs = new Set(); + const restoredTargets = new Map(); + let restored = false; + let storageUpdates = Promise.resolve(); + let connection: ProviderConnection | undefined; + let stopWatching: (() => void) | undefined; + let stopMessages: (() => void) | undefined; + let reconciling = false; + let disposed = false; + let started = false; + let stateUpdates = Promise.resolve(); + + function report(operation: Promise): void { + void operation.catch(error => options.onError?.(error)); + } + function current(): ProviderConnection { + if (disposed || connection === undefined) throw new Error('The browser provider is disconnected.'); + return connection; + } + async function send(message: AgentToBrokerMessage): Promise { + if (connection === undefined) throw new Error('The browser provider is disconnected.'); + return connection.send(message); + } + async function serialize(tabId: number, operation: () => Promise): Promise { + const pending = (tabOperations.get(tabId) ?? Promise.resolve()).catch(() => {}).then(operation); + tabOperations.set(tabId, pending); + void pending.finally(() => { + if (tabOperations.get(tabId) === pending) tabOperations.delete(tabId); + }).catch(() => {}); + return pending; + } + function projection(target: PublishedTarget): ProviderTarget { + const controlled = [...tabs.values()].find(tab => tab.target?.id === target.id); + const metadata = controlled === undefined ? undefined : options.targetMetadata?.(controlled.tab); + return { id: target.id, generation: target.generation, scopeId: target.scopeId, ...(target.title === undefined ? {} : { title: target.title }), ...(target.url === undefined ? {} : { url: target.url }), ...(metadata === undefined ? {} : { metadata }) }; + } + + const manager = createTabScopeManager({ + chrome: createChromeTabBindings(platform), + ...(options.onError === undefined ? {} : { onError: options.onError }), + async onTargetsChanged(requestId, targets) { + if (approved.has(requestId) && !reconciling && connection !== undefined) await connection.reconcileScope(requestId, targets.map(projection)); + }, + createPublisher(tab) { + const restoredTarget = restoredTargets.get(tab.tabId); + const scopeId = restoredTarget?.scopeId ?? crypto.randomUUID(); + let controlled: ControlledTab; + const publisher = createSelectedTabPublisher({ + capabilities: { level: options.maximumLevel }, + scopeId, + ...(restoredTarget === undefined ? {} : { targetIdentity: { id: restoredTarget.id, generation: restoredTarget.generation + 1 } }), + ...(options.isExposureAllowed === undefined ? {} : { isExposureAllowed: options.isExposureAllowed }), + ...(options.commandAuthorizationPolicy === undefined ? {} : { commandAuthorizationPolicy: options.commandAuthorizationPolicy }), + chromeDebugger: { + attach: async (target, version) => platform.debugger.attach(target, version), + detach: async target => platform.debugger.detach(target), + sendCommand: async (target, method, parameters) => (await platform.debugger.sendCommand(target, method, parameters)) as JsonObject ?? {}, + }, + metadataPolicy: candidate => ({ ...(candidate.title === undefined ? {} : { title: candidate.title }), ...(candidate.url === undefined ? {} : { url: candidate.url }) }), + async publishTarget(target) { + await send({ kind: 'notification', method: 'targets.publish', protocolVersion: 1, parameters: { target } }); + controlled.target = target; + manager.updateTarget(tab.tabId, target); + }, + async updateTarget(target) { + await send({ kind: 'notification', method: 'targets.update', protocolVersion: 1, parameters: { target } }); + controlled.target = target; + manager.updateTarget(tab.tabId, target); + }, + async revokeTarget(target, reason) { + if (connection !== undefined) await send({ kind: 'notification', method: 'targets.revoke', protocolVersion: 1, parameters: { targetId: target.id, targetGeneration: target.generation, reason } }); + if (!renewingTabs.has(tab.tabId)) { + delete controlled.target; + manager.updateTarget(tab.tabId, undefined); + } + }, + publishEvent(target, method, parameters, sessionId) { + report(send({ kind: 'notification', method: 'cdp.event', protocolVersion: 1, parameters: { targetId: target.id, targetGeneration: target.generation, method, parameters, ...(sessionId === undefined ? {} : { sessionId }) } })); + }, + publishPresentationEvent: event => options.onPresentation?.(tab.tabId, event), + }); + const wrapped: SelectedTabPublisher = { + ...publisher, + publish: async nextTab => serialize(tab.tabId, async () => { + controlled.tab = nextTab; + return publisher.publish(nextTab); + }), + refresh: async nextTab => serialize(tab.tabId, async () => { + controlled.tab = nextTab; + await publisher.refresh(nextTab); + }), + revoke: async reason => serialize(tab.tabId, async () => { + await publisher.revoke(reason); + tabs.delete(tab.tabId); + }), + renewAuthority: async () => serialize(tab.tabId, async () => { + renewingTabs.add(tab.tabId); + try { + return await publisher.renewAuthority(); + } finally { + renewingTabs.delete(tab.tabId); + } + }), + }; + controlled = { tab, publisher: wrapped, ...(restoredTarget?.documentId === undefined ? {} : { documentId: restoredTarget.documentId }) }; + tabs.set(tab.tabId, controlled); + return wrapped; + }, + }); + + async function persist(): Promise { + const storageKey = options.recoveryStorageKey; + if (storageKey === undefined || connection === undefined || reconciling) return Promise.resolve(); + const value: ProviderRecoveryState = { + version: 1, + brokerId: connection.brokerId, + scopes: [...scopes].filter(([requestId]) => approved.has(requestId)).map(([requestId, selector]) => ({ requestId, selector })), + targets: [...tabs.values()].flatMap(tab => tab.target === undefined ? [] : [{ tabId: tab.tab.tabId, id: tab.target.id, generation: tab.target.generation, scopeId: tab.target.scopeId, ...(tab.documentId === undefined ? {} : { documentId: tab.documentId }) }]), + }; + storageUpdates = storageUpdates.catch(() => {}).then(async () => platform.storage.session.set({ [storageKey]: value })); + return storageUpdates; + } + + async function restore(candidate: ProviderConnection): Promise { + if (restored || options.recoveryStorageKey === undefined) return; + const stored = (await platform.storage.session.get(options.recoveryStorageKey))[options.recoveryStorageKey] as ProviderRecoveryState | undefined; + const authority = await candidate.snapshot(); + if (stored?.version === 1 && stored.brokerId === candidate.brokerId) { + for (const target of stored.targets) restoredTargets.set(target.tabId, target); + for (const scope of stored.scopes) { + if (!authority.scopes.some(granted => granted.id === scope.requestId && granted.providerId === candidate.registration.id)) continue; + if (approved.has(scope.requestId)) continue; + await manager.addScope(scope.requestId, scope.selector); + scopes.set(scope.requestId, scope.selector); + approved.add(scope.requestId); + } + restoredTargets.clear(); + } + restored = true; + } + + function consumeState(state: BrokerState): void { + stateUpdates = stateUpdates.catch(() => {}).then(async () => { + if (disposed) return; + for (const requestId of approved) { + if (state.scopes.some(scope => scope.id === requestId)) continue; + approved.delete(requestId); + scopes.delete(requestId); + await manager.removeScope(requestId); + } + await persist(); + options.onState?.(state); + }); + report(stateUpdates); + } + + function receive(message: BrokerToAgentMessage): void { + if (disposed) return; + if (message.kind === 'notification' && message.method === 'cdp.cancel') { + commands.get(message.parameters.operationId)?.abort(); + return; + } + if (message.kind === 'notification' && message.method === 'cdp.subscription-demand') { + const { targetId, targetGeneration, methodPrefix, active, sessionId } = message.parameters; + const controlled = [...tabs.values()].find(tab => tab.target?.id === targetId && tab.target.generation === targetGeneration); + if (controlled !== undefined) report(controlled.publisher.setSubscriptionDemand(methodPrefix, active, sessionId)); + return; + } + if (message.kind !== 'request' || message.method !== 'cdp.execute') return; + const source = current(); + const { command, lease } = message.parameters; + const controlled = [...tabs.values()].find(tab => tab.target?.id === command.targetId && tab.target.generation === command.targetGeneration); + const controller = new AbortController(); + commands.set(command.operationId, controller); + report((async () => { + try { + if (controlled === undefined) throw new Error('The controlled target generation was replaced.'); + const value = await controlled.publisher.executeCommand(command, controller.signal, lease); + await source.send({ kind: 'response', method: 'cdp.execute', requestId: message.requestId, protocolVersion: 1, result: { operationId: command.operationId, value } }); + } catch (error) { + await source.send({ kind: 'error', method: 'cdp.execute', requestId: message.requestId, protocolVersion: 1, error: { code: 'CDP_COMMAND_FAILED', message: error instanceof Error ? error.message : String(error), retryable: false } }); + } finally { + commands.delete(command.operationId); + } + })()); + } + + async function hello(candidate: ProviderConnection): Promise { + const requestId = crypto.randomUUID(); + const ready = Promise.withResolvers(); + const unsubscribe = candidate.onMessage((message) => { + if (message.kind === 'notification' || message.requestId !== requestId || message.method !== 'agent.hello') return; + if (message.kind === 'response') ready.resolve(message.result.heartbeat); + else if (message.kind === 'error') ready.reject(new Error(message.error.message)); + }); + const timeout = setTimeout(() => ready.reject(new Error('The CDB broker hello timed out.')), 5_000); + try { + await candidate.send({ kind: 'request', method: 'agent.hello', requestId, protocolVersion: 1, parameters: { + connectionGeneration: candidate.generation, + protocolVersions: { minimum: 1, maximum: 1 }, + features: [], + implementation: { instanceId: candidate.registration.instanceId, name: candidate.registration.name, role: 'agent', version: candidate.registration.version }, + heartbeat: { intervalMilliseconds: 15_000, timeoutMilliseconds: 45_000 }, + limits: { maximumArtifactBytes: 16 * 1_024 * 1_024, maximumInlineResultBytes: 64 * 1_024, maximumMessageBytes: 64 * 1_024 * 1_024 }, + } }); + return await ready.promise; + } finally { + clearTimeout(timeout); + unsubscribe(); + } + } + + const recovery = createAgentRecovery({ + connect: options.connect, + ...(options.onError === undefined ? {} : { onError: options.onError }), + heartbeat: async (candidate, _generation, parameters) => sendAgentHeartbeat(candidate, candidate.generation, parameters.timeoutMilliseconds), + async reconcile(candidate) { + reconciling = true; + try { + connection = candidate; + stopMessages?.(); + stopMessages = candidate.onMessage(receive); + void candidate.closed.then(() => { + if (connection !== candidate) return; + connection = undefined; + for (const controller of commands.values()) controller.abort(); + commands.clear(); + }); + const heartbeat = await hello(candidate); + for (const tab of tabs.values()) if (tab.target !== undefined) await tab.publisher.renewAuthority(); + await restore(candidate); + await candidate.send({ kind: 'notification', method: 'targets.reconcile', protocolVersion: 1, parameters: { targets: [...tabs.values()].flatMap(tab => tab.target === undefined ? [] : [tab.target]) } }); + consumeState(await candidate.reconcile([...tabs.values()].flatMap(tab => tab.target === undefined ? [] : [projection(tab.target)]))); + reconciling = false; + stopWatching?.(); + stopWatching = await candidate.watch(consumeState); + await persist(); + return heartbeat; + } finally { + reconciling = false; + } + }, + onStateChange(state) { + if (state === 'revoked') report((async () => { + connection = undefined; + approved.clear(); + scopes.clear(); + await manager.dispose(); + })()); + }, + }); + + function debuggerEvent(source: chrome.debugger.Debuggee & { readonly sessionId?: string }, method: string, parameters?: object): void { + if (source.tabId !== undefined) tabs.get(source.tabId)?.publisher.debuggerEvent(source, method, parameters as JsonObject ?? {}); + } + function detached(source: chrome.debugger.Debuggee, reason: `${chrome.debugger.DetachReason}`): void { + const tabId = source.tabId; + if (tabId === undefined) return; + const controlled = tabs.get(tabId); + if (controlled?.target === undefined) return; + report(serialize(tabId, async () => { + if (reason !== 'target_closed') return controlled.publisher.debuggerDetached(tabId); + renewingTabs.add(tabId); + try { + /** A renderer replacement can detach Chrome's debugger without closing its tab. */ + await platform.tabs.get(tabId); + await controlled.publisher.debuggerDetached(tabId, { recover: true }); + if (connection !== undefined) await connection.reconcile([...tabs.values()].flatMap(tab => tab.target === undefined ? [] : [projection(tab.target)])); + await persist(); + } catch (error) { + renewingTabs.delete(tabId); + await controlled.publisher.debuggerDetached(tabId); + delete controlled.target; + manager.updateTarget(tabId, undefined); + throw error; + } finally { + renewingTabs.delete(tabId); + } + })); + } + function committed(details: chrome.webNavigation.WebNavigationFramedCallbackDetails & { readonly documentId?: string }): void { + const tab = tabs.get(details.tabId); + if (details.frameId !== 0 || tab === undefined || (details.documentId !== undefined && details.documentId === tab.documentId)) return; + tab.documentId = details.documentId; + report((async () => { + await tab.publisher.refresh(selectedTab(await platform.tabs.get(details.tabId))); + if (tab.target !== undefined) await tab.publisher.renewAuthority(); + if (connection !== undefined) await connection.reconcile([...tabs.values()].flatMap(value => value.target === undefined ? [] : [projection(value.target)])); + })()); + } + function alarm(value: chrome.alarms.Alarm): void { + if (value.name === options.recoveryAlarmName && !disposed) recovery.start(); + } + + return { + get state() { + return recovery.state; + }, + start() { + if (started || disposed) return; + started = true; + platform.debugger.onEvent.addListener(debuggerEvent); + platform.debugger.onDetach.addListener(detached); + platform.webNavigation.onCommitted.addListener(committed); + if (options.recoveryAlarmName !== undefined) { + platform.alarms.onAlarm.addListener(alarm); + report(platform.alarms.create(options.recoveryAlarmName, { periodInMinutes: 0.5 })); + } + recovery.start(); + }, + async approve(requestId: string, selectorInput: TabScopeSelector, approvalContext: ApprovalContext) { + const source = current(); + const selector = parseTabScopeSelector(selectorInput); + if (selector === undefined) throw new Error('A valid tab-selection scope is required.'); + const { claim, request } = await source.claim(requestId); + try { + if (!await options.authorizeApproval(request, selector, approvalContext)) throw new Error('The final approval source or selection policy was rejected.'); + scopes.set(requestId, selector); + const targets = await manager.addScope(requestId, selector); + const state = await source.approve(claim, targets.map(projection)); + approved.add(requestId); + await source.reconcileScope(requestId, manager.getTargets(requestId).map(projection)); + consumeState(state); + return state; + } catch (error) { + approved.delete(requestId); + scopes.delete(requestId); + await manager.removeScope(requestId).catch(() => {}); + await source.release(claim).catch(() => {}); + throw error; + } + }, + async revoke(requestId: string) { + await current().revokeScope(requestId); + approved.delete(requestId); + scopes.delete(requestId); + await manager.removeScope(requestId); + }, + async dispose() { + if (disposed) return; + disposed = true; + stopWatching?.(); + stopMessages?.(); + for (const controller of commands.values()) controller.abort(); + commands.clear(); + platform.debugger.onEvent.removeListener(debuggerEvent); + platform.debugger.onDetach.removeListener(detached); + platform.webNavigation.onCommitted.removeListener(committed); + platform.alarms.onAlarm.removeListener(alarm); + const source = connection; + const results = await Promise.allSettled(Array.from(approved, async requestId => source?.revokeScope(requestId))); + approved.clear(); + scopes.clear(); + results.push(...await Promise.allSettled([stateUpdates, storageUpdates])); + try { + await manager.dispose(); + } finally { + recovery.stop(); + connection = undefined; + if (options.recoveryAlarmName !== undefined) await platform.alarms.clear(options.recoveryAlarmName); + if (options.recoveryStorageKey !== undefined) await platform.storage.session.remove(options.recoveryStorageKey); + } + const failures = results.filter(result => result.status === 'rejected').map((result): unknown => result.reason); + if (failures.length > 0) throw new AggregateError(failures, 'The Chrome provider stopped, but some authority updates failed.'); + }, + }; +} + +/** Preserves a host-chosen installation storage key and serializes first creation across extension contexts. */ +export async function getChromeProviderIdentity(storageKey: string, platform: typeof chrome = chrome): Promise { + return navigator.locks.request(`cdb:${storageKey}`, async () => { + const values = await platform.storage.local.get(storageKey); + const existing: unknown = values[storageKey]; + if (typeof existing === 'string' && existing.length > 0) return existing; + const identity = crypto.randomUUID(); + await platform.storage.local.set({ [storageKey]: identity }); + return identity; + }); +} diff --git a/packages/extension/src/index.ts b/packages/extension/src/index.ts index 4a53856..27d2668 100644 --- a/packages/extension/src/index.ts +++ b/packages/extension/src/index.ts @@ -33,6 +33,7 @@ export { type IndexedDbPairingStore, type StoredBrokerPairing, } from './pairing-store.js'; +export type { ProviderConnection } from './provider-connection.js'; export { type ChromeSelectedTabLifecyclePort, createSelectedTabLifecycle, diff --git a/packages/extension/src/notifications.ts b/packages/extension/src/notifications.ts new file mode 100644 index 0000000..c08a0a8 --- /dev/null +++ b/packages/extension/src/notifications.ts @@ -0,0 +1,205 @@ +import type { BrokerGrant, BrokerRequest, BrokerState } from '@dvcol/cdb-broker/contract'; + +export interface BrowserControlNotification { + readonly grants: readonly BrokerGrant[]; + readonly requests: readonly BrokerRequest[]; +} + +export interface BrowserControlNotificationController { + dismiss: (requestId: string) => void; + dispose: () => void; + review: (requestId: string) => Promise; + revoke: (requestId: string) => Promise; + snapshot: () => BrowserControlNotification; + subscribe: (listener: (notification: BrowserControlNotification) => void) => () => void; + update: (state: Pick) => void; +} + +export interface BrowserControlNotificationOptions { + readonly targetId?: string; + /** Opens the embedding application's approval UI. This callback does not itself grant authority. */ + readonly onReview: (request: BrokerRequest) => void | Promise; + readonly onRevoke: (requestId: string) => Promise; +} + +/** Derives notification state from the broker. Dismissal is presentation-only and never rejects a request. */ +export function createBrowserControlNotificationController(options: BrowserControlNotificationOptions): BrowserControlNotificationController { + const dismissed = new Set(); + const listeners = new Set<(value: BrowserControlNotification) => void>(); + let state: Pick | undefined; + let disposed = false; + let expiration: ReturnType | undefined; + + function snapshot(): BrowserControlNotification { + return { + grants: disposed ? [] : state?.grants.filter(grant => options.targetId === undefined || grant.targetId === options.targetId) ?? [], + requests: disposed + ? [] + : state?.requests.filter(request => !dismissed.has(request.id) + && (request.expiresAt === null || request.expiresAt > Date.now()) + && (options.targetId === undefined || request.requestedTargetId === undefined || request.requestedTargetId === options.targetId)) ?? [], + }; + } + function notify(): void { + if (expiration !== undefined) clearTimeout(expiration); + const notification = snapshot(); + const deadlines = notification.requests.flatMap(request => request.expiresAt === null ? [] : [request.expiresAt]); + if (deadlines.length > 0) expiration = setTimeout(notify, Math.min(2_147_483_647, Math.max(0, Math.min(...deadlines) - Date.now()))); + for (const listener of listeners) listener(notification); + } + return { + snapshot, + dismiss(requestId) { + dismissed.add(requestId); + notify(); + }, + dispose() { + disposed = true; + if (expiration !== undefined) clearTimeout(expiration); + state = undefined; + listeners.clear(); + dismissed.clear(); + }, + async review(requestId) { + const request = snapshot().requests.find(candidate => candidate.id === requestId); + if (request === undefined) throw new Error('The browser access request is no longer pending.'); + await options.onReview(request); + }, + async revoke(requestId) { + if (!snapshot().grants.some(grant => grant.requestId === requestId)) throw new Error('The approved scope is no longer present.'); + await options.onRevoke(requestId); + }, + subscribe(listener) { + listeners.add(listener); + return () => listeners.delete(listener); + }, + update(nextState) { + if (disposed) return; + state = nextState; + for (const requestId of dismissed) if (!state.requests.some(request => request.id === requestId)) dismissed.delete(requestId); + notify(); + }, + }; +} + +export interface BrowserControlNotificationRendererOptions { + readonly controller: BrowserControlNotificationController; + readonly container: HTMLElement; + readonly branding?: { readonly title?: string; readonly accent?: string }; + /** Describe the host's action, including direct approval when the host provides that policy. */ + readonly reviewLabel?: (request: BrokerRequest) => string; + readonly clientLabel?: (request: BrokerRequest) => string; + /** Rejects the pending request through the host; dismissal remains local. */ + readonly onReject?: (request: BrokerRequest) => Promise; + /** Host-supplied CSS, applied inside the isolated notification root. */ + readonly css?: string; +} + +/** Optional neutral presentation. Review buttons delegate to the host's trusted final approval UI. */ +export function renderBrowserControlNotifications(options: BrowserControlNotificationRendererOptions): { dispose: () => void } { + const document = options.container.ownerDocument; + const host = document.createElement('section'); + host.dataset.cdbNotifications = ''; + const root = host.attachShadow({ mode: 'open' }); + const style = document.createElement('style'); + style.textContent = ` + :host { display: block; font: 14px/1.5 system-ui, sans-serif; color: #172033; color-scheme: light dark; } + article { background: Canvas; color: CanvasText; padding: 12px 16px; margin: 8px 0; border: 1px solid color-mix(in srgb, CanvasText 20%, transparent); border-radius: 8px; } + h2, p { margin: 0 0 8px; } h2 { font-size: 15px; } p { overflow-wrap: anywhere; } + button { padding: 6px 10px; margin: 4px 6px 0 0; cursor: pointer; border-radius: 4px; border: 1px solid currentColor; background: transparent; color: var(--cdb-accent, #7356c8); font: inherit; } + article { position: relative; } h2 { padding-right: 28px; } + .dismiss { position: absolute; top: 5px; right: 6px; margin: 0; border: 0; } + dl { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; text-align: left; } + dt { font-size: 12px; opacity: .75; } dd { margin: 4px 0 0; overflow-wrap: anywhere; } + code { font: 12px/1.5 ui-monospace, monospace; } + button:disabled { opacity: .6; cursor: wait; } [role=alert] { color: #b42318; } + ${options.css ?? ''} + `; + if (options.branding?.accent !== undefined) host.style.setProperty('--cdb-accent', options.branding.accent); + const content = document.createElement('div'); + root.append(style, content); + options.container.append(host); + let disposed = false; + let renderedNotification: string | undefined; + + function button(article: HTMLElement, label: string, action: () => void | Promise): HTMLButtonElement { + const element = document.createElement('button'); + element.type = 'button'; + element.textContent = label; + element.onclick = () => { + element.disabled = true; + void Promise.resolve().then(action).catch((error: unknown) => { + if (disposed) return; + const message = document.createElement('p'); + message.setAttribute('role', 'alert'); + message.textContent = error instanceof Error ? error.message : 'The browser request could not be completed.'; + article.append(message); + }).finally(() => { + element.disabled = false; + }); + }; + article.append(element); + return element; + } + function article(title: string, description?: string): HTMLElement { + const element = document.createElement('article'); + const heading = document.createElement('h2'); + heading.textContent = title; + element.append(heading); + if (description !== undefined) { + const text = document.createElement('p'); + text.textContent = description; + element.append(text); + } + return element; + } + function render(notification: BrowserControlNotification): void { + if (disposed) return; + /** Unrelated broker publications must not replace focused or pending controls. */ + const serializedNotification = JSON.stringify(notification); + if (serializedNotification === renderedNotification) return; + renderedNotification = serializedNotification; + content.replaceChildren(); + for (const request of notification.requests) { + const element = article(options.branding?.title ?? 'Browser control requested'); + const dismiss = button(element, '×', () => options.controller.dismiss(request.id)); + dismiss.className = 'dismiss'; + dismiss.setAttribute('aria-label', 'Dismiss'); + dismiss.title = 'Dismiss notification'; + const details = document.createElement('dl'); + for (const [label, value] of [['Client', options.clientLabel?.(request) ?? request.principalLabel], ['Grant', request.level], ['Navigation', request.navigation]] as const) { + const column = document.createElement('div'); + const term = document.createElement('dt'); + term.textContent = label; + const definition = document.createElement('dd'); + const badge = document.createElement('code'); + badge.textContent = value; + if (label === 'Grant') badge.dataset.level = request.level; + definition.append(badge); + column.append(term, definition); + details.append(column); + } + element.append(details); + const actions = document.createElement('footer'); + element.append(actions); + button(actions, options.reviewLabel?.(request) ?? 'Review request', async () => options.controller.review(request.id)); + const reject = options.onReject; + if (reject !== undefined) button(actions, 'Reject', async () => reject(request)); + content.append(element); + } + const grouped = Map.groupBy(notification.grants, grant => grant.requestId); + for (const [requestId, grants] of grouped) { + const grant = grants[0]!; + const element = article(options.branding?.title ?? 'Browser control', `${grant.principalLabel}: ${grant.level}, ${grant.navigation}. ${grants.length} approved ${grants.length === 1 ? 'tab' : 'tabs'}.`); + button(element, 'Stop control', async () => options.controller.revoke(requestId)); + content.append(element); + } + } + const unsubscribe = options.controller.subscribe(render); + render(options.controller.snapshot()); + return { dispose() { + disposed = true; + unsubscribe(); + host.remove(); + } }; +} diff --git a/packages/extension/src/pairing-store.ts b/packages/extension/src/pairing-store.ts index 38142d0..a3b0a77 100644 --- a/packages/extension/src/pairing-store.ts +++ b/packages/extension/src/pairing-store.ts @@ -8,6 +8,8 @@ export interface StoredBrokerPairing { export interface IndexedDbPairingStore { load: (endpoint: string) => Promise; + /** Finds a stored credential when the same broker moves to another transport endpoint. */ + findByIdentity?: (brokerId: string, agentId: string) => Promise; remove: (credentialId: string) => Promise; save: (pairing: StoredBrokerPairing) => Promise; } @@ -78,6 +80,19 @@ export function createIndexedDbPairingStore( const databaseName = options.databaseName ?? 'chrome-debugger-bridge'; return { + async findByIdentity(brokerId, agentId) { + const database = await openPairingDatabase(databaseName); + try { + const transaction = database.transaction(pairingObjectStoreName, 'readonly'); + const values = await requestResult(transaction.objectStore(pairingObjectStoreName).getAll() as IDBRequest); + await transactionComplete(transaction); + const matching = values.filter(isValidPairing).filter(pairing => pairing.brokerId === brokerId && pairing.agentId === agentId); + if (matching.length > 1) throw new Error('Multiple stored credentials match this broker and provider. Select the original pairing endpoint.'); + return matching[0]; + } finally { + database.close(); + } + }, async load(endpoint) { const database = await openPairingDatabase(databaseName); try { diff --git a/packages/extension/src/provider-connection.ts b/packages/extension/src/provider-connection.ts new file mode 100644 index 0000000..2c7bd89 --- /dev/null +++ b/packages/extension/src/provider-connection.ts @@ -0,0 +1,21 @@ +import type { AgentToBrokerMessage, BrokerToAgentMessage, GrantRequestClaim } from '@dvcol/cdb'; +import type { BrokerRequest, BrokerState, ProviderRegistration, ProviderTarget } from '@dvcol/cdb-broker/contract'; + +/** An authenticated provider channel. Closing it never implies closing the host transport. */ +export interface ProviderConnection { + readonly registration: ProviderRegistration; + readonly brokerId: string; + readonly generation: number; + readonly closed: Promise<{ readonly code: number; readonly reason: string }>; + close: (code?: number, reason?: string) => void; + onMessage: (listener: (message: BrokerToAgentMessage) => void) => () => void; + send: (message: AgentToBrokerMessage) => Promise; + snapshot: () => Promise; + claim: (requestId: string) => Promise<{ readonly claim: GrantRequestClaim; readonly request: BrokerRequest }>; + release: (claim: GrantRequestClaim) => Promise; + approve: (claim: GrantRequestClaim, targets: readonly ProviderTarget[]) => Promise; + reconcile: (targets: readonly ProviderTarget[]) => Promise; + reconcileScope: (requestId: string, targets: readonly ProviderTarget[]) => Promise; + revokeScope: (requestId: string) => Promise; + watch: (listener: (state: BrokerState) => void) => Promise<() => void>; +} diff --git a/packages/extension/src/selected-tab-publisher.ts b/packages/extension/src/selected-tab-publisher.ts index a46426d..1418f23 100644 --- a/packages/extension/src/selected-tab-publisher.ts +++ b/packages/extension/src/selected-tab-publisher.ts @@ -79,7 +79,7 @@ export interface SelectedTabPublisher { setSubscriptionDemand: (methodPrefix: string, active: boolean, sessionId?: string) => Promise; revoke: (reason?: 'closed' | 'detached' | 'explicit' | 'policy-invalid') => Promise; tabClosed: (tabId: number) => Promise; - debuggerDetached: (tabId: number) => Promise; + debuggerDetached: (tabId: number, options?: { readonly recover?: boolean }) => Promise; } const domainNamePattern = /^[A-Za-z]+$/u; @@ -165,13 +165,13 @@ export function createSelectedTabPublisher(options: SelectedTabPublisherOptions) && eligibleChildTargetTypes.has(targetInfo.type); } - function handleAttachedChild(parameters: JsonObject): PublicChildSession | undefined { + function handleAttachedChild(parameters: JsonObject, parentChromeSessionId?: string): PublicChildSession | undefined { if (!isEligibleChildAttachment(parameters)) return undefined; const childSession = childSessionRouter.attach(parameters.sessionId, { ...(typeof parameters.targetInfo.targetId === 'string' ? { frameId: parameters.targetInfo.targetId } : {}), type: parameters.targetInfo.type, ...(typeof parameters.targetInfo.url === 'string' ? { url: parameters.targetInfo.url } : {}), - }); + }, parentChromeSessionId); void (async () => { await configureFlatSessions(parameters.sessionId); await enableActiveRootDomains(parameters.sessionId); @@ -203,7 +203,8 @@ export function createSelectedTabPublisher(options: SelectedTabPublisherOptions) if (method !== 'Page.frameNavigated') return; const frame = parameters.frame; if (frame === null || typeof frame !== 'object' || Array.isArray(frame) || frame.parentId !== undefined) return; - childSessionRouter.revoke(); + /** Chrome may attach the new document's frames before delivering root navigation. */ + childSessionRouter.renew(); activeChildSubscriptionDemands.clear(); } @@ -354,7 +355,7 @@ export function createSelectedTabPublisher(options: SelectedTabPublisherOptions) ...priorTarget, generation: priorTarget.generation + 1, }; - childSessionRouter.revoke(); + childSessionRouter.renew(); activeRootSubscriptionDomains.clear(); activeRootSubscriptionDemands.clear(); activeChildSubscriptionDemands.clear(); @@ -382,7 +383,7 @@ export function createSelectedTabPublisher(options: SelectedTabPublisherOptions) debuggerEvent(source, method, parameters) { if (source.tabId !== undefined && source.tabId !== selectedTabId) return; if (method === 'Target.attachedToTarget') { - const childSession = handleAttachedChild(parameters); + const childSession = handleAttachedChild(parameters, source.sessionId); if (childSession !== undefined && isEligibleChildAttachment(parameters)) { publishEvent('Bridge.childSessionAttached', { type: parameters.targetInfo.type }, childSession.id); } @@ -480,8 +481,28 @@ export function createSelectedTabPublisher(options: SelectedTabPublisherOptions) async tabClosed(tabId) { if (tabId === selectedTabId) await revoke('closed'); }, - async debuggerDetached(tabId) { - if (tabId === selectedTabId) await revoke('detached'); + async debuggerDetached(tabId, recovery) { + if (tabId !== selectedTabId || publishedTarget === undefined) return; + if (recovery?.recover !== true) return revoke('detached'); + const renewedTarget = { ...publishedTarget, generation: publishedTarget.generation + 1 }; + publicationReady = false; + pendingEvents.length = 0; + childSessionRouter.revoke(); + activeRootSubscriptionDomains.clear(); + activeRootSubscriptionDemands.clear(); + activeChildSubscriptionDemands.clear(); + try { + await options.revokeTarget(publishedTarget, 'explicit'); + await options.chromeDebugger.attach({ tabId }, '1.3'); + publishedTarget = renewedTarget; + await configureFlatSessions(); + await options.publishTarget(renewedTarget); + finishPublication(); + options.registerTargetExecutor?.(renewedTarget, { execute: executeCommand, setSubscriptionDemand }); + } catch (error) { + await revoke('detached'); + throw error; + } }, executeCommand, detachChildSession(chromeSessionId) { diff --git a/packages/extension/test/agent-recovery.test.ts b/packages/extension/test/agent-recovery.test.ts index b2f11ca..65dbe49 100644 --- a/packages/extension/test/agent-recovery.test.ts +++ b/packages/extension/test/agent-recovery.test.ts @@ -2,6 +2,29 @@ import { expect, it } from 'vitest'; import { createAgentRecovery } from '../src/agent-recovery.js'; +it('reports the original connection failure while retaining reconnect behavior', async () => { + expect.assertions(3); + const failure = new Error('Stored pairing does not match the broker'); + const errors: unknown[] = []; + const recovery = createAgentRecovery({ + connect: async () => { + throw failure; + }, + reconcile: async () => {}, + onError: error => errors.push(error), + minimumBackoffMilliseconds: 60_000, + }); + try { + recovery.start(); + await Promise.resolve(); + expect(errors).toEqual([failure]); + expect(recovery.state).toBe('reconnecting'); + expect(recovery.connection).toBeUndefined(); + } finally { + recovery.stop(); + } +}); + function createConnection(): { close: (code?: number, reason?: string) => void; closed: Promise<{ readonly code: number; readonly reason: string }>; resolveClose: (result: { readonly code: number; readonly reason: string }) => void } { let resolveClose: ((result: { readonly code: number; readonly reason: string }) => void) | undefined; const closed = new Promise<{ readonly code: number; readonly reason: string }>(resolve => resolveClose = resolve); @@ -137,3 +160,20 @@ it('cancels a connection attempt when recovery stops before authentication finis expect(recovery.state).toBe('stopped'); expect((await connection.closed).code).toBe(1000); }); + +it('closes a candidate channel when reconciliation fails before recovery becomes ready', async () => { + expect.assertions(3); + const connection = createConnection(); + const recovery = createAgentRecovery({ + connect: async () => connection, + reconcile: async () => { + throw new Error('Handshake failed'); + }, + minimumBackoffMilliseconds: 60_000, + }); + recovery.start(); + expect(await connection.closed).toEqual({ code: 3001, reason: 'Agent reconciliation failed' }); + expect(recovery.connection).toBeUndefined(); + expect(recovery.state).toBe('reconnecting'); + recovery.stop(); +}); diff --git a/packages/extension/test/child-session-router.test.ts b/packages/extension/test/child-session-router.test.ts index e6796ab..54982bd 100644 --- a/packages/extension/test/child-session-router.test.ts +++ b/packages/extension/test/child-session-router.test.ts @@ -23,3 +23,31 @@ it('maps private Chrome sessions to opaque lifecycle-bound public identities', ( expect(router.revoke()).toEqual([second]); expect(router.list()).toEqual([]); }); + +it('fences references across renewal without forgetting frames already attached by Chrome', () => { + expect.assertions(6); + const router = createChildSessionRouter(); + const original = router.attach('chrome-child', { frameId: 'frame-child', type: 'iframe' }); + router.renew(); + const current = router.publicSessionForChromeId('chrome-child')!; + expect(router.resolve(original.id)).toBeUndefined(); + expect(current.id).not.toBe(original.id); + expect(current.generation).toBeGreaterThan(original.generation); + expect(current.frameId).toBe('frame-child'); + expect(router.resolve(current.id)).toBe('chrome-child'); + router.detach('chrome-child'); + expect(router.list()).toEqual([]); +}); + +it('removes nested sessions when Chrome detaches their ancestor', () => { + expect.assertions(4); + const router = createChildSessionRouter(); + const parent = router.attach('parent', 'iframe'); + const child = router.attach('child', 'iframe', 'parent'); + const grandchild = router.attach('grandchild', 'iframe', 'child'); + const sibling = router.attach('sibling', 'iframe'); + expect(router.detach('parent')).toEqual(parent); + expect(router.resolve(child.id)).toBeUndefined(); + expect(router.resolve(grandchild.id)).toBeUndefined(); + expect(router.list()).toEqual([sibling]); +}); diff --git a/packages/extension/test/notifications.test.ts b/packages/extension/test/notifications.test.ts new file mode 100644 index 0000000..8cd2f94 --- /dev/null +++ b/packages/extension/test/notifications.test.ts @@ -0,0 +1,75 @@ +// @vitest-environment jsdom +import type { BrokerRequest, BrokerState } from '@dvcol/cdb-broker/contract'; + +import { afterEach, expect, it, vi } from 'vitest'; + +import { createBrowserControlNotificationController, renderBrowserControlNotifications } from '../src/notifications.js'; + +afterEach(() => { + vi.useRealTimers(); + document.body.replaceChildren(); +}); + +const request: BrokerRequest = { id: 'request', principalId: 'principal', principalLabel: '', level: 'interact', navigation: 'same-origin', state: 'pending', createdAt: 0, expiresAt: 2_000 }; +const state: BrokerState = { revision: 1, providers: [], principals: [], requests: [request], grants: [], scopes: [], targets: [], leases: [] }; + +it('keeps dismissal local and expires requests without another broker update', async () => { + expect.assertions(5); + vi.useFakeTimers(); + vi.setSystemTime(1_000); + const review = vi.fn(); + const revoke = vi.fn(); + const controller = createBrowserControlNotificationController({ onReview: review, onRevoke: revoke }); + controller.update(state); + await controller.review(request.id); + expect(review).toHaveBeenCalledWith(request); + controller.dismiss(request.id); + expect(controller.snapshot().requests).toEqual([]); + expect(revoke).not.toHaveBeenCalled(); + controller.update({ ...state, requests: [] }); + controller.update(state); + expect(controller.snapshot().requests).toEqual([request]); + await vi.advanceTimersByTimeAsync(1_000); + expect(controller.snapshot().requests).toEqual([]); + controller.dispose(); +}); + +it.each(['Review request', 'Accept INTERACT'])('renders untrusted labels as text and delegates "%s" to the embedding application', async (label) => { + expect.assertions(4); + const review = vi.fn(); + const controller = createBrowserControlNotificationController({ onReview: review, onRevoke: async () => {} }); + controller.update({ ...state, requests: [{ ...request, expiresAt: null }] }); + const renderer = renderBrowserControlNotifications({ controller, container: document.body, branding: { title: 'Browser access' }, ...(label === 'Review request' ? {} : { reviewLabel: (request: BrokerRequest) => `Accept ${request.level.toUpperCase()}` }) }); + const root = document.querySelector('section')!.shadowRoot!; + expect(root.querySelector('script')).toBeNull(); + expect(root.textContent).toContain(request.principalLabel); + [...root.querySelectorAll('button')].find(button => button.textContent === label)!.click(); + await vi.waitUntil(() => review.mock.calls.length === 1); + expect(review).toHaveBeenCalledOnce(); + renderer.dispose(); + expect(document.body.children).toHaveLength(0); + controller.dispose(); +}); + +it('preserves focused notification controls across equivalent broker publications', () => { + expect.assertions(4); + const controller = createBrowserControlNotificationController({ onReview: async () => {}, onRevoke: async () => {} }); + const pendingRequest = { ...request, expiresAt: null }; + controller.update({ requests: [pendingRequest], grants: [] }); + const renderer = renderBrowserControlNotifications({ controller, container: document.body }); + const root = document.querySelector('section')!.shadowRoot!; + const review = root.querySelectorAll('button')[1]!; + review.focus(); + + /** Transport publications contain fresh objects even when the pending request has not changed. */ + controller.update({ requests: [{ ...pendingRequest }], grants: [] }); + expect(root.querySelectorAll('button')[1]).toBe(review); + expect(root.activeElement).toBe(review); + + controller.update({ requests: [{ ...pendingRequest, principalLabel: 'Updated client' }], grants: [] }); + expect(root.textContent).toContain('Updated client'); + controller.update({ requests: [], grants: [] }); + expect(root.querySelector('article')).toBeNull(); + renderer.dispose(); + controller.dispose(); +}); diff --git a/packages/extension/test/selected-tab-publisher.test.ts b/packages/extension/test/selected-tab-publisher.test.ts index 5701aa2..a0dac48 100644 --- a/packages/extension/test/selected-tab-publisher.test.ts +++ b/packages/extension/test/selected-tab-publisher.test.ts @@ -6,6 +6,52 @@ import { createSelectedTabPublisher } from '../src/selected-tab-publisher.js'; const scopeId = '40000000-0000-4000-8000-000000000001'; +it('recovers a detached renderer with the same target identity and a new generation', async () => { + expect.assertions(5); + const attach = vi.fn(); + const publishTarget = vi.fn(); + const revokeTarget = vi.fn(); + const publisher = createSelectedTabPublisher({ + capabilities: { level: 'interact' }, + scopeId, + chromeDebugger: { attach, detach() {}, async sendCommand() { + return {}; + } }, + publishTarget, + revokeTarget, + updateTarget() {}, + }); + const target = await publisher.publish({ incognito: false, tabId: 42, url: 'https://example.test' }); + const child = publisher.attachChildSession('previous-frame'); + await publisher.debuggerDetached(42, { recover: true }); + expect(attach).toHaveBeenCalledTimes(2); + expect(revokeTarget).toHaveBeenCalledWith(target, 'explicit'); + expect(publishTarget).toHaveBeenLastCalledWith({ ...target, generation: 2 }); + expect(publisher.detachChildSession('previous-frame')).toBeUndefined(); + expect(publisher.attachChildSession('previous-frame').id).not.toBe(child.id); + await publisher.revoke(); +}); + +it('revokes a target when reattaching its renderer fails', async () => { + expect.assertions(3); + const attach = vi.fn().mockResolvedValueOnce(undefined).mockRejectedValueOnce(new Error('Tab closed')); + const revokeTarget = vi.fn(); + const publisher = createSelectedTabPublisher({ + capabilities: { level: 'interact' }, + scopeId, + chromeDebugger: { attach, detach() {}, async sendCommand() { + return {}; + } }, + publishTarget() {}, + revokeTarget, + updateTarget() {}, + }); + const target = await publisher.publish({ incognito: false, tabId: 42, url: 'https://example.test' }); + await expect(publisher.debuggerDetached(42, { recover: true })).rejects.toThrow('Tab closed'); + expect(revokeTarget).toHaveBeenLastCalledWith(target, 'detached'); + await expect(publisher.renewAuthority()).rejects.toThrow('not available'); +}); + it('publishes the target before forwarding child attachment events raised during debugger setup', async () => { expect.assertions(3); const publicationStarted = Promise.withResolvers(); @@ -431,7 +477,7 @@ it('invalidates child-session routing when the published root is revoked', async expect(() => publisher.attachChildSession('private-child-session')).toThrow('not available'); }); -it('renews a published target with stable logical identity, a higher generation, and no child sessions', async () => { +it('renews target and child references while retaining Chrome attachments', async () => { expect.assertions(7); const publishedTargets: unknown[] = []; const revokedTargets: unknown[] = []; @@ -458,7 +504,7 @@ it('renews a published target with stable logical identity, a higher generation, expect(revokedTargets).toEqual([{ reason: 'explicit', target }]); expect(publishedTargets).toEqual([target, renewedTarget]); await expect(publisher.executeCommand({ leaseId: '20000000-0000-4000-8000-000000000001', method: 'Runtime.evaluate', operationId: '30000000-0000-4000-8000-000000000001', targetGeneration: target.generation, targetId: target.id }, new AbortController().signal)).rejects.toThrow('not permitted'); - expect(publisher.detachChildSession('private-child-session')).toBeUndefined(); + expect(publisher.detachChildSession('private-child-session')?.id).not.toBe(child.id); expect(child.id).toMatch(/^[0-9a-f-]{36}$/u); }); @@ -572,7 +618,7 @@ it('routes demanded root and child events through public session identities', as expect(events.some(event => event.method === 'Runtime.consoleAPICalled' && event.sessionId === child.id)).toBe(true); expect(events.some(event => event.method === 'Runtime.consoleAPICalled' && event.sessionId === 'private-frame-session')).toBe(false); expect(events.some(event => event.method === 'Page.frameNavigated' && event.sessionId === undefined)).toBe(true); - expect(publisher.detachChildSession('private-frame-session')).toBeUndefined(); + expect(publisher.detachChildSession('private-frame-session')?.id).not.toBe(child.id); }); it('replays active root domain demand for an eligible child session', async () => { diff --git a/packages/extension/tsdown.config.ts b/packages/extension/tsdown.config.ts index aaba960..5d47f2d 100644 --- a/packages/extension/tsdown.config.ts +++ b/packages/extension/tsdown.config.ts @@ -9,6 +9,8 @@ export default defineConfig({ }, entry: [ 'src/index.ts', + 'src/chrome.ts', + 'src/notifications.ts', 'src/bootstrap.ts', 'src/presentation.ts', 'src/testing.ts', diff --git a/packages/mcp/src/index.ts b/packages/mcp/src/index.ts index ad34105..b33a104 100644 --- a/packages/mcp/src/index.ts +++ b/packages/mcp/src/index.ts @@ -28,6 +28,7 @@ import type { import { AsyncLocalStorage } from 'node:async_hooks'; import { Buffer } from 'node:buffer'; import { randomUUID } from 'node:crypto'; +import { channel } from 'node:diagnostics_channel'; import { createAgentSession } from '@dvcol/cdb/agent'; import { requiredLeaseMode } from '@dvcol/cdb/cdp-catalogue'; @@ -142,6 +143,7 @@ export interface RegisterCdbToolsOptions { export interface McpTimingPolicy { readonly defaultEventTimeoutMilliseconds: number; readonly defaultToolTimeoutMilliseconds: number; + readonly defaultInputActionTimeoutMilliseconds?: number; readonly maximumInputActionDurationMilliseconds: number; readonly maximumToolTimeoutMilliseconds: number; readonly semanticLeaseDurationMilliseconds: number; @@ -150,16 +152,18 @@ export interface McpTimingPolicy { export const defaultMcpTimingPolicy: Readonly = Object.freeze({ defaultEventTimeoutMilliseconds: 5_000, defaultToolTimeoutMilliseconds: 10_000, + defaultInputActionTimeoutMilliseconds: 2_000, maximumInputActionDurationMilliseconds: 10_000, maximumToolTimeoutMilliseconds: 30_000, semanticLeaseDurationMilliseconds: 30_000, }); -function resolveMcpTimingPolicy(timing: Partial | undefined): McpTimingPolicy { - const resolved = { ...defaultMcpTimingPolicy, ...timing }; +function resolveMcpTimingPolicy(timing: Partial | undefined): Required { + const resolved = { ...defaultMcpTimingPolicy, ...timing, defaultInputActionTimeoutMilliseconds: timing?.defaultInputActionTimeoutMilliseconds ?? 2_000 }; for (const [name, value] of Object.entries(resolved)) { if (!Number.isSafeInteger(value) || value < 1) throw new TypeError(`${name} must be a positive safe integer.`); } + if (resolved.defaultInputActionTimeoutMilliseconds > resolved.maximumToolTimeoutMilliseconds) throw new TypeError('defaultInputActionTimeoutMilliseconds cannot exceed maximumToolTimeoutMilliseconds.'); if (resolved.defaultToolTimeoutMilliseconds > resolved.maximumToolTimeoutMilliseconds) { throw new TypeError('defaultToolTimeoutMilliseconds cannot exceed maximumToolTimeoutMilliseconds.'); } @@ -243,6 +247,79 @@ function semanticLifecycleResult(value: unknown, targetRef: string): JsonObject }; } +type InputPhase = 'discovery' | 'actionability' | 'input' | 'verification'; +interface InputActionScope { + readonly deadline: number; + readonly signal: AbortSignal; + dispatched: boolean; + renewLocator?: boolean; + readinessError?: McpToolError; + phase: InputPhase; + phaseStarted: number; + readonly phaseMilliseconds: Record; + commandCount: number; + requestCharacters: number; + responseCharacters: number; + retries: number; + readonly commands: { method: string; milliseconds: number }[]; +} + +const inputActionScope = new AsyncLocalStorage(); +const inputActionDiagnostics = channel('cdb.mcp.action'); +const elementActionNames = new Set(['browser.click', 'browser.fill', 'browser.type', 'browser.press', 'browser.check', 'browser.uncheck', 'browser.focus', 'browser.hover', 'browser.scroll_into_view', 'browser.drag', 'browser.select_option']); + +function inputPhase(phase: InputPhase): void { + const scope = inputActionScope.getStore(); + if (scope === undefined || scope.phase === phase) return; + const now = performance.now(); + scope.phaseMilliseconds[scope.phase] += now - scope.phaseStarted; + scope.phaseStarted = now; + scope.phase = phase; +} + +function inputDeadline(timeoutMilliseconds: number): number { + return inputActionScope.getStore()?.deadline ?? Date.now() + timeoutMilliseconds; +} + +function inputReadiness(error: McpToolError): void { + const scope = inputActionScope.getStore(); + if (scope === undefined) return; + scope.readinessError = error; + scope.retries += 1; +} + +async function invokeElementAction(name: string, timeoutMilliseconds: number, parentSignal: AbortSignal, action: (signal: AbortSignal) => Promise): Promise { + const cancellation = new AbortController(); + const signal = AbortSignal.any([parentSignal, cancellation.signal]); + const started = performance.now(); + const scope: InputActionScope = { deadline: Date.now() + timeoutMilliseconds, signal, dispatched: false, phase: 'discovery', phaseStarted: started, phaseMilliseconds: { discovery: 0, actionability: 0, input: 0, verification: 0 }, commandCount: 0, requestCharacters: 0, responseCharacters: 0, retries: 0, commands: [] }; + const timeoutError = new McpToolError('MCP_ACTION_TIMEOUT', 'The element action exceeded its total deadline.', undefined, true); + const timer = setTimeout(() => cancellation.abort(timeoutError), timeoutMilliseconds); + timer.unref?.(); + const aborted = Promise.withResolvers(); + const abort = (): void => { + const error: unknown = scope.dispatched + ? new McpToolError('MCP_ACTION_OUTCOME_UNKNOWN', 'Input may have been dispatched before the action ended. Inspect the control before another action; input was not replayed.') + : cancellation.signal.aborted ? scope.readinessError ?? timeoutError : parentSignal.reason; + aborted.resolve(toolError(error)); + }; + signal.addEventListener('abort', abort, { once: true }); + try { + if (signal.aborted) { + abort(); + return await aborted.promise; + } + return await inputActionScope.run(scope, async () => Promise.race([action(signal), aborted.promise])); + } finally { + clearTimeout(timer); + signal.removeEventListener('abort', abort); + if (inputActionDiagnostics.hasSubscribers) { + scope.phaseMilliseconds[scope.phase] += performance.now() - scope.phaseStarted; + inputActionDiagnostics.publish({ name, milliseconds: performance.now() - started, phaseMilliseconds: { ...scope.phaseMilliseconds }, commandCount: scope.commandCount, requestCharacters: scope.requestCharacters, responseCharacters: scope.responseCharacters, retries: scope.retries, commands: [...scope.commands], dispatched: scope.dispatched, timedOut: cancellation.signal.aborted }); + } + } +} + async function withLease( client: McpChromeDebuggerBridgeClient, input: { readonly targetGeneration: number; readonly targetId: string }, @@ -833,7 +910,7 @@ async function executeInputAction( }; const executeCleanup = async (method: string, parameters: JsonObject): Promise => { try { - await client.executeCommand({ + await inputActionScope.exit(async () => client.executeCommand({ leaseId: lease.id, method, operationId: randomUUID(), @@ -841,7 +918,7 @@ async function executeInputAction( ...(input.sessionId === undefined ? {} : { sessionId: input.sessionId }), targetGeneration: input.targetGeneration, targetId: input.targetId, - }); + })); } catch { /** The original cancellation or target fence is the actionable failure. */ } @@ -1317,6 +1394,7 @@ interface AccessibilityCandidate extends NativeElementReference { } interface LocatorContext { + readonly sessionRoot?: boolean; readonly frameId?: string; readonly rootBackendNodeId?: number; readonly sessionId?: string; @@ -1703,7 +1781,7 @@ async function accessibilityFrameContexts( if (output.length >= 128) throw new McpToolError('MCP_SEARCH_INCOMPLETE', 'Frame discovery exceeded 128 frames. Narrow the scope.'); const current = pending.pop(); const frameId = stringValue(property(property(current, 'frame'), 'id')); - if (frameId !== undefined) output.push({ ...context, frameId }); + if (frameId !== undefined) output.push({ ...context, frameId, sessionRoot: frameId === rootFrameId }); pending.push(...arrayValue(property(current, 'childFrames'))); } } @@ -1750,20 +1828,15 @@ async function collectAccessibilityCandidates( const exactAccessibleName = [locator.name, locator.text, locator.label].find( matcher => matcher?.match === 'exact', )?.value; - const canQueryAccessibilityTree = context.frameId === undefined && (locator.role !== undefined || exactAccessibleName !== undefined); + /** Refresh the full AX view after failed readiness so locator renewal drops detached cached nodes. */ + const canQueryAccessibilityTree = inputActionScope.getStore()?.readinessError === undefined && (context.frameId === undefined || context.sessionRoot === true) && (locator.role !== undefined || exactAccessibleName !== undefined); const commandValue = canQueryAccessibilityTree ? await (async (): Promise => { - const documentValue = commandResultValue(await executeSemanticCommand( - client, - sessionAuthority, - 'shared-read', - 'DOM.getDocument', - { depth: 0, pierce: true }, - signal, - )); - const documentBackendNodeId = numberValue(property(property(documentValue, 'root'), 'backendNodeId')); - const backendNodeId = context.rootBackendNodeId ?? documentBackendNodeId; - if (backendNodeId === undefined) return { nodes: [] }; + const rootValue = context.rootBackendNodeId === undefined + ? commandResultValue(await executeSemanticCommand(client, sessionAuthority, 'shared-read', 'Accessibility.getRootAXNode', context.frameId === undefined ? {} : { frameId: context.frameId }, signal)) + : undefined; + const backendNodeId = context.rootBackendNodeId ?? numberValue(property(property(rootValue, 'node'), 'backendDOMNodeId')); + if (backendNodeId === undefined) throw new McpToolError('MCP_SEARCH_INCOMPLETE', 'The accessibility document root could not be resolved.'); return executeArtifactCommand( client, sessionAuthority, @@ -2454,7 +2527,8 @@ async function executeElementClick( ], signal, async (execute, executeCleanup) => { - const deadline = Date.now() + input.timeoutMilliseconds; + inputPhase('actionability'); + const deadline = inputDeadline(input.timeoutMilliseconds); let point: ViewportPoint | undefined; while (point === undefined) { let retryableError: McpToolError | undefined; @@ -2495,9 +2569,11 @@ async function executeElementClick( await assertAncestorFrameHit(element, candidatePoint, execute); point = candidatePoint; } catch (error) { - if (error instanceof McpToolError && error.retryable) + if (error instanceof McpToolError && error.retryable) { retryableError = error; - else if (property(error, 'code') === 'CDP_COMMAND_FAILED' && detachedNodeMessagePattern.test(error instanceof Error ? error.message : String(property(error, 'message')))) + inputReadiness(error); + if (inputActionScope.getStore()?.renewLocator) throw error; + } else if (property(error, 'code') === 'CDP_COMMAND_FAILED' && detachedNodeMessagePattern.test(error instanceof Error ? error.message : String(property(error, 'message')))) throw new McpToolError('MCP_ELEMENT_DETACHED', 'The element is detached from the current document.'); else if (error instanceof McpToolError || property(error, 'code') !== undefined) throw error; @@ -2604,7 +2680,8 @@ async function resolveSemanticElementWithRetry( }, signal: AbortSignal, ): Promise { - const deadline = Date.now() + input.timeoutMilliseconds; + inputPhase('discovery'); + const deadline = inputDeadline(input.timeoutMilliseconds); while (true) { try { return await resolveSemanticElement(client, state, target, input, signal); @@ -2614,6 +2691,7 @@ async function resolveSemanticElementWithRetry( || property(error, 'code') !== 'MCP_LOCATOR_NOT_FOUND' || Date.now() >= deadline ) throw error; + if (error instanceof McpToolError) inputReadiness(error); await waitForInputStep(Math.min(100, Math.max(0, deadline - Date.now())), signal); } } @@ -2625,7 +2703,9 @@ async function executeWithRenewedLocatorRetry( input: { readonly locator?: SemanticLocator | undefined; readonly ref?: string | undefined; readonly targetRef: string; readonly timeoutMilliseconds?: number }, action: (target: PublishedTarget) => Promise, ): Promise { - const deadline = Date.now() + (input.timeoutMilliseconds ?? 10_000); + const deadline = inputDeadline(input.timeoutMilliseconds ?? 2_000); + const scope = inputActionScope.getStore(); + if (scope !== undefined) scope.renewLocator = input.locator !== undefined; while (true) { try { return await action(await resolveSemanticTarget(client, state, input.targetRef)); @@ -2635,7 +2715,9 @@ async function executeWithRenewedLocatorRetry( state.elementReferences.delete(input.ref); throw new McpToolError('MCP_ELEMENT_REF_STALE', `Element reference ${input.ref} is stale. Capture a new snapshot.`); } - if (input.locator === undefined || !['TARGET_GENERATION_STALE', 'MCP_ELEMENT_DETACHED'].includes(String(code)) || Date.now() >= deadline) throw error; + const retryable = ['TARGET_GENERATION_STALE', 'MCP_ELEMENT_DETACHED', 'MCP_ELEMENT_HIDDEN', 'MCP_ELEMENT_DISABLED', 'MCP_ELEMENT_COVERED', 'MCP_ELEMENT_UNSTABLE', 'MCP_ELEMENT_NOT_EDITABLE'].includes(String(code)); + if (input.locator === undefined || !retryable || scope?.dispatched || Date.now() >= deadline) throw error; + if (scope !== undefined) await waitForInputStep(Math.min(100, Math.max(0, deadline - Date.now())), scope.signal); } } } @@ -2689,7 +2771,8 @@ async function executeElementInteraction( methods, signal, async (execute, executeCleanup) => { - const deadline = Date.now() + timeoutMilliseconds; + inputPhase('actionability'); + const deadline = inputDeadline(timeoutMilliseconds); let point: ViewportPoint | undefined; while (point === undefined) { let retryableError: McpToolError | undefined; @@ -2740,8 +2823,11 @@ async function executeElementInteraction( await assertAncestorFrameHit(element, candidatePoint, execute); point = candidatePoint; } catch (error) { - if (error instanceof McpToolError && error.retryable) retryableError = error; - else if (property(error, 'code') === 'CDP_COMMAND_FAILED' && detachedNodeMessagePattern.test(error instanceof Error ? error.message : String(property(error, 'message')))) + if (error instanceof McpToolError && error.retryable) { + retryableError = error; + inputReadiness(error); + if (inputActionScope.getStore()?.renewLocator) throw error; + } else if (property(error, 'code') === 'CDP_COMMAND_FAILED' && detachedNodeMessagePattern.test(error instanceof Error ? error.message : String(property(error, 'message')))) throw new McpToolError('MCP_ELEMENT_DETACHED', 'The element is detached from the current document.'); else if (error instanceof McpToolError || property(error, 'code') !== undefined) throw error; else throw new McpToolError('MCP_ELEMENT_DETACHED', 'The element is detached from the current document.'); @@ -3004,6 +3090,7 @@ function createCdbToolDefinitionsForSession( sessionState: CdbToolSessionState, ): CdbToolDefinition[] { const definitions: CdbToolDefinition[] = []; + const timing = resolveMcpTimingPolicy(options.timing); const register = ( name: string, config: { readonly description: string; readonly inputSchema: InputSchema }, @@ -3022,19 +3109,21 @@ function createCdbToolDefinitionsForSession( if (sessionState.disposed) return toolError(new McpToolError('MCP_TOOL_SESSION_DISPOSED', 'The browser tool session has been disposed.')); const parsedInput = await config.inputSchema.parseAsync(input); + if (elementActionNames.has(name)) return invokeElementAction(name, numberValue(property(parsedInput, 'timeoutMilliseconds')) ?? timing.defaultInputActionTimeoutMilliseconds, context.signal, async signal => handler(parsedInput, { mcpReq: { signal } })); return handler(parsedInput, { mcpReq: { signal: context.signal } }); }, mcpInputSchema: config.inputSchema, name, }); }; - const timing = resolveMcpTimingPolicy(options.timing); const rawClient = options.client; const batchLease = new AsyncLocalStorage(); const cancelAutomation = rawClient.cancelAutomation; const executeAutomation = rawClient.executeAutomation; const client: McpChromeDebuggerBridgeClient = { async acquireLease(request) { + const scope = inputActionScope.getStore(); + scope?.signal.throwIfAborted(); const activeLease = batchLease.getStore(); if (activeLease !== undefined) { if (request.targetId !== activeLease.targetId || request.targetGeneration !== activeLease.targetGeneration) @@ -3043,13 +3132,51 @@ function createCdbToolDefinitionsForSession( throw new McpToolError('MCP_BATCH_METHOD_UNAVAILABLE', 'The batch action requested an unavailable command.'); return activeLease; } - return rawClient.acquireLease({ ...request, durationMilliseconds: timing.semanticLeaseDurationMilliseconds }); + const lease = await rawClient.acquireLease({ ...request, durationMilliseconds: timing.semanticLeaseDurationMilliseconds }); + if (scope?.signal.aborted) { + await rawClient.releaseLease({ targetId: lease.targetId, targetGeneration: lease.targetGeneration, leaseId: lease.id }); + scope.signal.throwIfAborted(); + } + return lease; }, ...(cancelAutomation === undefined ? {} : { cancelAutomation: async request => cancelAutomation(request) }), cancelCommand: async request => rawClient.cancelCommand(request), - ...(executeAutomation === undefined ? {} : { executeAutomation: async request => executeAutomation(request) }), - executeCommand: async command => rawClient.executeCommand(command), - listTargets: async () => rawClient.listTargets(), + ...(executeAutomation === undefined + ? {} + : { executeAutomation: async (request) => { + const scope = inputActionScope.getStore(); + scope?.signal.throwIfAborted(); + if (scope !== undefined && request.operation.kind === 'action') { + scope.dispatched = true; + inputPhase('input'); + } + return executeAutomation(request); + } }), + executeCommand: async (command) => { + const scope = inputActionScope.getStore(); + scope?.signal.throwIfAborted(); + if (scope !== undefined) { + if (command.method.startsWith('Input.') || command.method === 'DOM.focus') { + inputPhase('input'); + scope.dispatched = true; + } else if (scope.dispatched) inputPhase('verification'); + scope.commandCount += 1; + if (inputActionDiagnostics.hasSubscribers) scope.requestCharacters += JSON.stringify(command).length; + } + const started = performance.now(); + const result = await rawClient.executeCommand(command); + if (scope !== undefined && inputActionDiagnostics.hasSubscribers) scope.commands.push({ method: command.method, milliseconds: performance.now() - started }); + if (scope !== undefined && inputActionDiagnostics.hasSubscribers) scope.responseCharacters += JSON.stringify(result).length; + scope?.signal.throwIfAborted(); + return result; + }, + listTargets: async () => { + const scope = inputActionScope.getStore(); + scope?.signal.throwIfAborted(); + const targets = await rawClient.listTargets(); + scope?.signal.throwIfAborted(); + return targets; + }, readArtifact: async (request, signal) => rawClient.readArtifact(request, signal), releaseArtifact: async request => rawClient.releaseArtifact(request), releaseLease: async (request) => { @@ -3122,7 +3249,7 @@ function createCdbToolDefinitionsForSession( locator: locatorSchema.optional(), ref: z.string().regex(elementReferencePattern).optional(), targetRef: z.string().regex(targetReferencePattern), - timeoutMilliseconds: z.number().int().positive().max(timing.maximumToolTimeoutMilliseconds).default(timing.defaultToolTimeoutMilliseconds), + timeoutMilliseconds: z.number().int().positive().max(timing.maximumToolTimeoutMilliseconds).default(timing.defaultInputActionTimeoutMilliseconds), }; const hasExactlyOneElementTarget = (input: { readonly locator?: unknown; readonly ref?: unknown }): boolean => (input.ref === undefined) !== (input.locator === undefined); @@ -3886,7 +4013,7 @@ function createCdbToolDefinitionsForSession( modifiers: inputModifiersSchema, ref: z.string().regex(elementReferencePattern).optional(), targetRef: z.string().regex(targetReferencePattern), - timeoutMilliseconds: z.number().int().positive().max(timing.maximumToolTimeoutMilliseconds).default(timing.defaultToolTimeoutMilliseconds), + timeoutMilliseconds: z.number().int().positive().max(timing.maximumToolTimeoutMilliseconds).default(timing.defaultInputActionTimeoutMilliseconds), }).refine(input => (input.ref === undefined) !== (input.locator === undefined), { message: 'Provide exactly one of ref or locator.', }), @@ -4218,7 +4345,7 @@ function createCdbToolDefinitionsForSession( modifiers: inputModifiersSchema, source: nestedElementTargetSchema, targetRef: z.string().regex(targetReferencePattern), - timeoutMilliseconds: z.number().int().positive().max(timing.maximumToolTimeoutMilliseconds).default(timing.defaultToolTimeoutMilliseconds), + timeoutMilliseconds: z.number().int().positive().max(timing.maximumToolTimeoutMilliseconds).default(timing.defaultInputActionTimeoutMilliseconds), }), }, async (input, context) => { @@ -4539,6 +4666,7 @@ function createCdbToolDefinitionsForSession( clickCount: z.number().int().min(1).max(3).optional(), }).refine(hasExactlyOneElementTarget, { message: 'Each action requires exactly one ref or locator.' })).min(1).max(20), observe: z.boolean().default(false), + actionTimeoutMilliseconds: z.number().int().positive().max(timing.maximumToolTimeoutMilliseconds).default(timing.defaultInputActionTimeoutMilliseconds), timeoutMilliseconds: z.number().int().min(1).max(30_000).default(30_000), }), }, @@ -4582,7 +4710,7 @@ function createCdbToolDefinitionsForSession( })); const target = await resolveSemanticTarget(client, sessionState, input.targetRef); const authority = { targetId: target.id, targetGeneration: target.generation }; - acquiredLease = await rawClient.acquireLease({ ...authority, mode: 'exclusive-control', durationMilliseconds: input.timeoutMilliseconds, requestedMethods: [...new Set([...ancestorFrameMethods, 'Accessibility.getPartialAXTree', 'Accessibility.getFullAXTree', 'Accessibility.queryAXTree', 'DOM.describeNode', 'DOM.getContentQuads', 'DOM.getDocument', 'DOM.getFlattenedDocument', 'DOM.querySelectorAll', 'DOM.performSearch', 'DOM.getSearchResults', 'DOM.discardSearchResults', 'DOM.scrollIntoViewIfNeeded', 'DOM.focus', 'Input.dispatchMouseEvent', 'Input.dispatchKeyEvent', 'Input.insertText', 'Page.frameNavigated', 'DOM.documentUpdated'])] }); + acquiredLease = await rawClient.acquireLease({ ...authority, mode: 'exclusive-control', durationMilliseconds: input.timeoutMilliseconds, requestedMethods: [...new Set([...ancestorFrameMethods, 'Accessibility.getPartialAXTree', 'Accessibility.getFullAXTree', 'Accessibility.getRootAXNode', 'Accessibility.queryAXTree', 'DOM.describeNode', 'DOM.getContentQuads', 'DOM.getDocument', 'DOM.getFlattenedDocument', 'DOM.querySelectorAll', 'DOM.performSearch', 'DOM.getSearchResults', 'DOM.discardSearchResults', 'DOM.scrollIntoViewIfNeeded', 'DOM.focus', 'Input.dispatchMouseEvent', 'Input.dispatchKeyEvent', 'Input.insertText', 'Page.frameNavigated', 'DOM.documentUpdated'])] }); const lease = acquiredLease; return await batchLease.run(lease, async () => { const sessions = childSessionReferences(await executeSemanticCommand(client, authority, 'shared-read', 'Bridge.listChildSessions', {}, signal)).filter(session => session.type === 'iframe'); @@ -4599,7 +4727,7 @@ function createCdbToolDefinitionsForSession( if (signal.aborted) throw signal.reason; const currentTarget = await resolveSemanticTarget(client, sessionState, input.targetRef); if (currentTarget.generation !== target.generation) throw new McpToolError('MCP_BATCH_AUTHORITY_REPLACED', 'Target authority changed during the batch.'); - const result = await step.definition.invoke({ ...step.parameters, timeoutMilliseconds: input.timeoutMilliseconds }, { signal }); + const result = await step.definition.invoke({ ...step.parameters, timeoutMilliseconds: Math.max(1, Math.min(input.actionTimeoutMilliseconds, deadline - Date.now())) }, { signal }); if (result.isError) { if (!signal.aborted && Date.now() >= deadline) cancellation.abort(timeoutError); return batchFailure(result, index); diff --git a/packages/mcp/test/locator-session.test.ts b/packages/mcp/test/locator-session.test.ts index 7fefb04..3740cc5 100644 --- a/packages/mcp/test/locator-session.test.ts +++ b/packages/mcp/test/locator-session.test.ts @@ -347,6 +347,7 @@ it('supports the core Playwright-style locator strategies and XPath through DOM if (command.method === 'Bridge.listChildSessions') return { value: { sessions: [] } }; if (command.method === 'Accessibility.queryAXTree' || command.method === 'Accessibility.getFullAXTree') return { value: { nodes: [{ backendDOMNodeId: 7, childIds: [], ignored: false, name: { value: 'Save' }, nodeId: 'save', role: { value: 'button' } }] } }; + if (command.method === 'Accessibility.getRootAXNode') return { value: { node: { backendDOMNodeId: 1 } } }; if (command.method === 'DOM.getDocument') return { value: { root: { backendNodeId: 1 } } }; if (command.method === 'DOM.getFlattenedDocument') return { value: { nodes: [{ nodeId: 1, nodeType: 9, backendNodeId: 1, children: [{ nodeId: 70, backendNodeId: 7, nodeName: 'BUTTON', attributes: ['placeholder', 'Search', 'alt', 'Hero', 'title', 'Save title', 'data-testid', 'save'] }], shadowRoots: [{ nodeId: 2, shadowRootType: 'open' }, { nodeId: 3, shadowRootType: 'closed' }] }] } }; @@ -409,6 +410,7 @@ it('scopes locators through an OOPIF frame chain', async () => { commands.push(command); if (command.method === 'Bridge.listChildSessions') return { value: { sessions: [{ frameId: 'frame-target', generation: 1, id: childSessionId, type: 'iframe' }] } }; + if (command.method === 'Accessibility.getRootAXNode') return { value: { node: { backendDOMNodeId: 1 } } }; if (command.method === 'DOM.getDocument') return { value: { root: { backendNodeId: command.sessionId === undefined ? 1 : 2 } } }; if (command.method === 'Accessibility.queryAXTree' && command.sessionId === undefined) @@ -455,6 +457,7 @@ it('scopes locators through a same-process iframe document', async () => { async cancelCommand() {}, async executeCommand(command: CdpCommand) { if (command.method === 'Bridge.listChildSessions') return { value: { sessions: [] } }; + if (command.method === 'Accessibility.getRootAXNode') return { value: { node: { backendDOMNodeId: 1 } } }; if (command.method === 'DOM.getDocument') return { value: { root: { backendNodeId: 1 } } }; if (command.method === 'Accessibility.queryAXTree') { accessibilityTreeCalls += 1; @@ -501,6 +504,7 @@ it('applies descendant, has-text, exclusion, and nth filters against DOM ancestr }, async cancelCommand() {}, async executeCommand(command: CdpCommand) { + if (command.method === 'Accessibility.getRootAXNode') return { value: { node: { backendDOMNodeId: 1 } } }; if (command.method === 'DOM.getDocument') return { value: { root: { backendNodeId: 1 } } }; if (command.method === 'Accessibility.queryAXTree') return { @@ -573,6 +577,7 @@ it('re-resolves a locator once when authority renews before input dispatch', asy async cancelCommand() {}, async executeCommand(command: CdpCommand) { commands.push(command); + if (command.method === 'Accessibility.getRootAXNode') return { value: { node: { backendDOMNodeId: 1 } } }; if (command.method === 'DOM.getDocument') return { value: { root: { backendNodeId: 1 } } }; if (command.method === 'Accessibility.queryAXTree' && !staleThrown) { staleThrown = true; @@ -620,6 +625,7 @@ it('never replays a locator action after pointer input may have dispatched', asy async cancelCommand() {}, async executeCommand(command: CdpCommand) { if (command.method === 'Bridge.listChildSessions') return { value: { sessions: [] } }; + if (command.method === 'Accessibility.getRootAXNode') return { value: { node: { backendDOMNodeId: 1 } } }; if (command.method === 'DOM.getDocument') return { value: { root: { backendNodeId: 1 } } }; if (command.method === 'Accessibility.queryAXTree') return { value: { nodes: [{ backendDOMNodeId: 7, childIds: [], ignored: false, name: { value: 'Save' }, nodeId: 'save', role: { value: 'button' } }] } }; diff --git a/packages/mcp/test/mvp-session.test.ts b/packages/mcp/test/mvp-session.test.ts index 94a53d4..c05f03f 100644 --- a/packages/mcp/test/mvp-session.test.ts +++ b/packages/mcp/test/mvp-session.test.ts @@ -4,7 +4,7 @@ import type { McpChromeDebuggerBridgeClient } from '../src/index.js'; import { Buffer } from 'node:buffer'; -import { expect, it } from 'vitest'; +import { expect, it, vi } from 'vitest'; import { createCdbToolSession } from '../src/index.js'; @@ -182,6 +182,7 @@ it('re-resolves a locator after replacement before input, while a disposable ref let searches = 0; let inputCount = 0; const harness = toolHarness(async (command) => { + if (command.method === 'Accessibility.getRootAXNode') return { value: { node: { backendDOMNodeId: 1 } } }; if (command.method === 'DOM.getDocument') return { value: { root: { backendNodeId: 1 } } }; if (command.method === 'Accessibility.getFullAXTree') return { value: { nodes: [{ backendDOMNodeId: 41, nodeId: 'old', name: { value: 'Save' }, role: { value: 'button' } }] } }; if (command.method === 'Accessibility.queryAXTree') { @@ -307,7 +308,7 @@ it('executes ordered element actions in one batch lease and stops after an uncer expect(inputCount).toBe(4); }); -it.each(['Page.getFrameTree', 'Accessibility.getFullAXTree'])('cancels stalled %s discovery at the batch deadline without dispatching input', async (stalledMethod) => { +it.each(['Page.getFrameTree', 'Accessibility.queryAXTree'])('cancels stalled %s discovery at the batch deadline without dispatching input', async (stalledMethod) => { expect.assertions(4); const pendingDiscovery = Promise.withResolvers(); const discoveryStarted = Promise.withResolvers(); @@ -318,6 +319,8 @@ it.each(['Page.getFrameTree', 'Accessibility.getFullAXTree'])('cancels stalled % discoveryStarted.resolve(); return pendingDiscovery.promise; } + if (command.method === 'Accessibility.getRootAXNode') return { value: { node: { backendDOMNodeId: 1 } } }; + if (command.method === 'DOM.getDocument') return { value: { root: { backendNodeId: 1 } } }; if (command.method === 'Page.getFrameTree') return { value: { frameTree: { frame: { id: 'root-frame' } } } }; if (command.method.startsWith('Input.')) inputCount += 1; return { value: { sessions: [] } }; @@ -348,6 +351,7 @@ it('reports the batch deadline when locator polling is cancelled before input', expect.assertions(3); let inputCount = 0; const harness = toolHarness(async (command) => { + if (command.method === 'Accessibility.getRootAXNode') return { value: { node: { backendDOMNodeId: 1 } } }; if (command.method.startsWith('Input.')) inputCount += 1; return { value: {} }; }); @@ -374,6 +378,7 @@ it('keeps raw authority and debug execution out of the default compact catalogue it('reports the complete match count when limiting returned references', async () => { expect.assertions(3); const harness = toolHarness(async (command) => { + if (command.method === 'Accessibility.getRootAXNode') return { value: { node: { backendDOMNodeId: 1 } } }; if (command.method === 'DOM.getDocument') return { value: { root: { backendNodeId: 1 } } }; if (command.method === 'Accessibility.queryAXTree') return { value: { nodes: Array.from({ length: 3 }, (_unused, index) => ({ backendDOMNodeId: index + 1, nodeId: `button-${index}`, name: { value: 'Save' }, role: { value: 'button' } })) } }; return { value: { sessions: [] } }; @@ -433,7 +438,7 @@ it.each([ }); await harness.invoke('browser.snapshot'); - const result = await harness.invoke(tool, { ref: 'e1', text: 'Sample', timeoutMilliseconds: 1 }); + const result = await harness.invoke(tool, { ref: 'e1', text: 'Sample', timeoutMilliseconds: 150 }); expect(result.text).toContain(code); expect(inputCount).toBe(0); @@ -458,3 +463,158 @@ it('checks an indeterminate control and verifies the resulting checked state', a expect(result.error).toBeUndefined(); expect(JSON.parse(result.text)).toEqual({ changed: true, checked: true }); }); + +it('defaults element actions to two seconds without changing navigation deadlines', () => { + expect.assertions(3); + const harness = toolHarness(async () => ({ value: {} })); + expect(harness.session.definitions.find(definition => definition.name === 'browser.click')!.mcpInputSchema.parse({ targetRef: 't1', ref: 'e1' })).toMatchObject({ timeoutMilliseconds: 2_000 }); + expect(harness.session.definitions.find(definition => definition.name === 'browser.navigate')!.mcpInputSchema.parse({ targetRef: 't1', url: 'https://example.test' })).toMatchObject({ timeoutMilliseconds: 10_000 }); + expect(harness.session.definitions.find(definition => definition.name === 'browser.batch')!.mcpInputSchema.parse({ targetRef: 't1', actions: [{ action: 'click', ref: 'e1' }] })).toMatchObject({ actionTimeoutMilliseconds: 2_000, timeoutMilliseconds: 30_000 }); + harness.session.dispose(); +}); + +it('uses one deadline across locator discovery and disabled-element retries', async () => { + expect.assertions(3); + vi.useFakeTimers(); + let inputCount = 0; + const harness = toolHarness(async (command) => { + if (command.method === 'Accessibility.getRootAXNode') return { value: { node: { backendDOMNodeId: 1 } } }; + if (command.method === 'DOM.getDocument') return { value: { root: { backendNodeId: 1 } } }; + if (command.method === 'Accessibility.queryAXTree' || command.method === 'Accessibility.getFullAXTree') { + if (command.method === 'Accessibility.queryAXTree') await new Promise(resolve => setTimeout(resolve, 1_000)); + return { value: { nodes: [{ backendDOMNodeId: 1, nodeId: 'control', name: { value: 'Control' }, role: { value: 'button' } }] } }; + } + if (command.method === 'Accessibility.getPartialAXTree') return { value: { nodes: [{ properties: [{ name: 'disabled', value: { value: true } }] }] } }; + if (command.method.startsWith('Input.')) inputCount += 1; + return { value: { sessions: [] } }; + }); + try { + let completed = false; + const action = harness.invoke('browser.click', { locator: { role: 'button', name: { match: 'exact', value: 'Control' } } }).then((result) => { + completed = true; + return result; + }); + await vi.advanceTimersByTimeAsync(2_000); + expect(completed).toBe(true); + await vi.advanceTimersByTimeAsync(2_000); + expect((await action).text).toContain('MCP_ELEMENT_DISABLED'); + expect(inputCount).toBe(0); + } finally { + harness.session.dispose(); + vi.useRealTimers(); + } +}); + +it('bounds a stalled lease acquisition and releases authority arriving after the deadline', async () => { + expect.assertions(3); + vi.useFakeTimers(); + const harness = toolHarness(async () => ({ value: {} })); + const acquire = harness.client.acquireLease.bind(harness.client); + const release = vi.fn(async () => {}); + harness.client.releaseLease = release; + harness.client.acquireLease = async (request) => { + await new Promise(resolve => setTimeout(resolve, 5_000)); + return acquire(request); + }; + try { + let completed = false; + const action = harness.invoke('browser.click', { locator: { role: 'button' } }).then((result) => { + completed = true; + return result; + }); + await vi.advanceTimersByTimeAsync(2_000); + expect(completed).toBe(true); + await vi.advanceTimersByTimeAsync(8_000); + expect((await action).text).toContain('MCP_ACTION_TIMEOUT'); + expect(release).toHaveBeenCalledOnce(); + } finally { + harness.session.dispose(); + vi.useRealTimers(); + } +}); + +it('reports uncertain input at the deadline and never replays a stalled dispatch', async () => { + expect.assertions(3); + vi.useFakeTimers(); + let dispatches = 0; + const harness = toolHarness(async (command) => { + if (command.method === 'Accessibility.getFullAXTree') return { value: { nodes: [{ backendDOMNodeId: 1, nodeId: 'control', name: { value: 'Control' }, role: { value: 'button' } }] } }; + if (command.method === 'DOM.describeNode') return { value: { node: { backendNodeId: 1 } } }; + if (command.method === 'DOM.getContentQuads') return { value: { quads: [[0, 0, 100, 0, 100, 50, 0, 50]] } }; + if (command.method === 'DOM.getNodeForLocation') return { value: { backendNodeId: 1 } }; + if (command.method.startsWith('Input.')) { + dispatches += 1; + await new Promise(resolve => setTimeout(resolve, 5_000)); + } + return { value: { sessions: [] } }; + }); + const cancel = vi.fn(async () => {}); + harness.client.cancelCommand = cancel; + try { + await harness.invoke('browser.snapshot'); + const action = harness.invoke('browser.click', { ref: 'e1' }); + await vi.advanceTimersByTimeAsync(2_000); + expect((await action).text).toContain('MCP_ACTION_OUTCOME_UNKNOWN'); + expect(cancel).toHaveBeenCalledOnce(); + await vi.advanceTimersByTimeAsync(5_000); + expect(dispatches).toBe(1); + } finally { + harness.session.dispose(); + vi.useRealTimers(); + } +}); + +it.each([ + { name: 'default action deadline', tool: 'browser.click', input: { ref: 'e1' }, succeeds: false }, + { name: 'explicit longer action deadline', tool: 'browser.click', input: { ref: 'e1', timeoutMilliseconds: 4_000 }, succeeds: true }, + { name: 'default batch step deadline', tool: 'browser.batch', input: { actions: [{ action: 'click', ref: 'e1' }] }, succeeds: false }, + { name: 'explicit longer batch step deadline', tool: 'browser.batch', input: { actions: [{ action: 'click', ref: 'e1' }], actionTimeoutMilliseconds: 4_000 }, succeeds: true }, +])('honors $name for a transient disabled control', async ({ tool, input, succeeds }) => { + expect.assertions(2); + vi.useFakeTimers(); + const readyAt = Date.now() + 2_500; + let dispatches = 0; + const harness = toolHarness(async (command) => { + if (command.method === 'Accessibility.getFullAXTree') return { value: { nodes: [{ backendDOMNodeId: 1, nodeId: 'control', name: { value: 'Control' }, role: { value: 'button' } }] } }; + if (command.method === 'Accessibility.getPartialAXTree') return { value: { nodes: [{ properties: [{ name: 'disabled', value: { value: Date.now() < readyAt } }] }] } }; + if (command.method === 'DOM.describeNode') return { value: { node: { backendNodeId: 1 } } }; + if (command.method === 'DOM.getContentQuads') return { value: { quads: [[0, 0, 100, 0, 100, 50, 0, 50]] } }; + if (command.method === 'DOM.getNodeForLocation') return { value: { backendNodeId: 1 } }; + if (command.method.startsWith('Input.')) dispatches += 1; + return { value: { sessions: [] } }; + }); + try { + await harness.invoke('browser.snapshot'); + const action = harness.invoke(tool, input); + await vi.advanceTimersByTimeAsync(5_000); + const result = await action; + expect(result.error === true).toBe(!succeeds); + expect(dispatches > 0).toBe(succeeds); + } finally { + harness.session.dispose(); + vi.useRealTimers(); + } +}); + +it('queries session roots while retaining in-process frame candidates and ambiguity', async () => { + expect.assertions(3); + const methods: string[] = []; + const node = (backendDOMNodeId: number) => ({ backendDOMNodeId, nodeId: String(backendDOMNodeId), name: { value: 'Save' }, role: { value: 'button' } }); + const harness = toolHarness(async (command) => { + methods.push(command.method); + if (command.method === 'Page.getFrameTree') return { value: { frameTree: { frame: { id: 'root' }, childFrames: [{ frame: { id: 'child' } }] } } }; + if (command.method === 'Accessibility.getRootAXNode') return { value: { node: { backendDOMNodeId: 10 } } }; + if (command.method === 'DOM.getDocument') return { value: { root: { backendNodeId: 10 } } }; + if (command.method === 'Accessibility.queryAXTree') return { value: { nodes: [node(1)] } }; + if (command.method === 'Accessibility.getFullAXTree') return { value: { nodes: [node(2)] } }; + return { value: { sessions: [] } }; + }); + try { + const result = await harness.invoke('browser.click', { locator: { role: 'button', name: { match: 'exact', value: 'Save' } } }); + expect(result.text).toContain('MCP_LOCATOR_AMBIGUOUS'); + expect(methods).toContain('Accessibility.queryAXTree'); + expect(methods).toContain('Accessibility.getFullAXTree'); + } finally { + harness.session.dispose(); + } +}); diff --git a/packages/mcp/test/tool-session.test.ts b/packages/mcp/test/tool-session.test.ts index abeb259..d0d4d23 100644 --- a/packages/mcp/test/tool-session.test.ts +++ b/packages/mcp/test/tool-session.test.ts @@ -178,6 +178,7 @@ it('finds by semantic locator and clicks through a fresh ref without exposing au commands.push(command); if (command.method === 'Bridge.listChildSessions') return { operationId: command.operationId, value: { sessions: [] } }; + if (command.method === 'Accessibility.getRootAXNode') return { value: { node: { backendDOMNodeId: 1 } } }; if (command.method === 'DOM.getDocument') { return { operationId: command.operationId, value: { root: { backendNodeId: 1 } } }; } diff --git a/packages/websocket/src/authentication.ts b/packages/websocket/src/authentication.ts index f66758d..65c0a45 100644 --- a/packages/websocket/src/authentication.ts +++ b/packages/websocket/src/authentication.ts @@ -1,275 +1 @@ -import { agentWebSocketProtocol } from './protocols.js'; - -const authenticationDomain = 'chrome-debugger-bridge/auth/v1'; -const authenticatedFrameDomain = 'chrome-debugger-bridge/frame/v1'; -const base64PaddingPattern = /=+$/u; -const textEncoder = new TextEncoder(); - -export type AuthenticatedFrameDirection = 'agent-to-broker' | 'broker-to-agent'; - -export interface AgentAuthenticationTranscript { - readonly agentId: string; - readonly brokerId: string; - readonly clientNonce: string; - readonly connectionId: string; - readonly credentialId: string; - readonly endpointPath: string; - readonly expiresAt: string; - readonly origin: string; - readonly protocolVersion: number; - readonly serverNonce: string; -} - -export interface AuthenticatedFrame { - readonly authenticationCode: string; - readonly connectionId: string; - readonly direction: AuthenticatedFrameDirection; - readonly kind: 'authenticated'; - readonly payload: string; - readonly sequence: number; -} - -export interface BrokerAuthenticationClaims { - readonly connectionGeneration: number; - readonly principalId: string; -} - -function encodeBase64UrlBytes(bytes: Uint8Array): string { - let binary = ''; - for (const byte of bytes) { - binary += String.fromCharCode(byte); - } - return globalThis.btoa(binary).replaceAll('+', '-').replaceAll('/', '_').replace(base64PaddingPattern, ''); -} - -export function decodeBase64UrlBytes(value: string): Uint8Array { - const paddingLength = (4 - (value.length % 4)) % 4; - const base64 = `${value.replaceAll('-', '+').replaceAll('_', '/')}${'='.repeat(paddingLength)}`; - const binary = globalThis.atob(base64); - const bytes = Uint8Array.from(binary, character => character.charCodeAt(0)); - if (bytes.byteLength !== 32 || encodeBase64UrlBytes(bytes) !== value) { - throw new Error('Expected a canonical 32-byte base64url value'); - } - return bytes; -} - -export function generateRandomBase64Url(byteLength: number): string { - const bytes = new Uint8Array(byteLength); - globalThis.crypto.getRandomValues(bytes); - return encodeBase64UrlBytes(bytes); -} - -export function createRandomIdentifier(): string { - return globalThis.crypto.randomUUID(); -} - -export async function importAgentCredential(credential: Uint8Array): Promise { - if (credential.byteLength !== 32) { - throw new Error('Agent credentials must contain exactly 32 bytes'); - } - return globalThis.crypto.subtle.importKey('raw', Uint8Array.from(credential), 'HKDF', false, ['deriveKey']); -} - -function createBaseAuthenticationTranscript(transcript: AgentAuthenticationTranscript): readonly unknown[] { - return [ - authenticationDomain, - agentWebSocketProtocol, - 'agent', - transcript.endpointPath, - transcript.origin, - transcript.brokerId, - transcript.agentId, - transcript.credentialId, - transcript.connectionId, - transcript.protocolVersion, - transcript.clientNonce, - transcript.serverNonce, - transcript.expiresAt, - ]; -} - -function createAuthenticationTranscriptBytes( - transcript: AgentAuthenticationTranscript, - proofDirection: 'agent' | 'broker', - brokerClaims?: BrokerAuthenticationClaims, -): Uint8Array { - /** A fixed primitive tuple is already in RFC 8785 canonical order and avoids object-key ambiguity. */ - return textEncoder.encode(JSON.stringify([ - ...createBaseAuthenticationTranscript(transcript), - proofDirection, - ...(brokerClaims === undefined ? [] : [brokerClaims.connectionGeneration, brokerClaims.principalId]), - ])); -} - -async function createKeyDerivationSalt(transcript: AgentAuthenticationTranscript): Promise { - return globalThis.crypto.subtle.digest( - 'SHA-256', - textEncoder.encode(JSON.stringify(createBaseAuthenticationTranscript(transcript))), - ); -} - -async function deriveAuthenticationKey( - credential: CryptoKey, - transcript: AgentAuthenticationTranscript, - purpose: 'agent' | 'broker' | AuthenticatedFrameDirection, -): Promise { - return globalThis.crypto.subtle.deriveKey( - { - hash: 'SHA-256', - info: textEncoder.encode(`${authenticationDomain}/${purpose}`), - name: 'HKDF', - salt: await createKeyDerivationSalt(transcript), - }, - credential, - { hash: 'SHA-256', length: 256, name: 'HMAC' }, - false, - ['sign', 'verify'], - ); -} - -export async function createAgentAuthenticationProof( - credential: CryptoKey, - transcript: AgentAuthenticationTranscript, -): Promise { - const proofKey = await deriveAuthenticationKey(credential, transcript, 'agent'); - const proof = await globalThis.crypto.subtle.sign( - 'HMAC', - proofKey, - createAuthenticationTranscriptBytes(transcript, 'agent'), - ); - return encodeBase64UrlBytes(new Uint8Array(proof)); -} - -export async function verifyAgentAuthenticationProof( - credential: CryptoKey, - transcript: AgentAuthenticationTranscript, - proof: string, -): Promise { - const proofKey = await deriveAuthenticationKey(credential, transcript, 'agent'); - return globalThis.crypto.subtle.verify( - 'HMAC', - proofKey, - decodeBase64UrlBytes(proof), - createAuthenticationTranscriptBytes(transcript, 'agent'), - ); -} - -export async function createBrokerAuthenticationProof( - credential: CryptoKey, - transcript: AgentAuthenticationTranscript, - claims: BrokerAuthenticationClaims, -): Promise { - const proofKey = await deriveAuthenticationKey(credential, transcript, 'broker'); - const proof = await globalThis.crypto.subtle.sign( - 'HMAC', - proofKey, - createAuthenticationTranscriptBytes(transcript, 'broker', claims), - ); - return encodeBase64UrlBytes(new Uint8Array(proof)); -} - -export async function verifyBrokerAuthenticationProof( - credential: CryptoKey, - transcript: AgentAuthenticationTranscript, - claims: BrokerAuthenticationClaims, - proof: string, -): Promise { - const proofKey = await deriveAuthenticationKey(credential, transcript, 'broker'); - return globalThis.crypto.subtle.verify( - 'HMAC', - proofKey, - decodeBase64UrlBytes(proof), - createAuthenticationTranscriptBytes(transcript, 'broker', claims), - ); -} - -function createAuthenticatedFrameBytes( - frame: Omit, -): Uint8Array { - return textEncoder.encode(JSON.stringify([ - authenticatedFrameDomain, - frame.connectionId, - frame.direction, - frame.sequence, - frame.payload, - ])); -} - -export async function createAuthenticatedFrame( - credential: CryptoKey, - transcript: AgentAuthenticationTranscript, - direction: AuthenticatedFrameDirection, - sequence: number, - payload: unknown, -): Promise { - const frameWithoutAuthenticationCode = { - connectionId: transcript.connectionId, - direction, - payload: JSON.stringify(payload), - sequence, - }; - const sessionKey = await deriveAuthenticationKey(credential, transcript, direction); - const authenticationCode = await globalThis.crypto.subtle.sign( - 'HMAC', - sessionKey, - createAuthenticatedFrameBytes(frameWithoutAuthenticationCode), - ); - return { - authenticationCode: encodeBase64UrlBytes(new Uint8Array(authenticationCode)), - ...frameWithoutAuthenticationCode, - kind: 'authenticated', - }; -} - -export async function openAuthenticatedFrame( - credential: CryptoKey, - transcript: AgentAuthenticationTranscript, - expectedDirection: AuthenticatedFrameDirection, - expectedSequence: number, - frameValue: unknown, -): Promise { - const frame = parseAuthenticatedFrame(frameValue); - if ( - frame.connectionId !== transcript.connectionId - || frame.direction !== expectedDirection - || frame.sequence !== expectedSequence - ) { - throw new Error('Authenticated frame context is invalid'); - } - - const sessionKey = await deriveAuthenticationKey(credential, transcript, expectedDirection); - const valid = await globalThis.crypto.subtle.verify( - 'HMAC', - sessionKey, - decodeBase64UrlBytes(frame.authenticationCode), - createAuthenticatedFrameBytes(frame), - ); - if (!valid) { - throw new Error('Authenticated frame proof is invalid'); - } - return JSON.parse(frame.payload) as unknown; -} - -function parseAuthenticatedFrame(value: unknown): AuthenticatedFrame { - if (value === null || typeof value !== 'object' || Array.isArray(value)) { - throw new Error('Authenticated frame envelope is invalid'); - } - const frame = value as Partial; - const keys = Object.keys(value).sort(); - const expectedKeys = ['authenticationCode', 'connectionId', 'direction', 'kind', 'payload', 'sequence']; - if ( - keys.length !== expectedKeys.length - || keys.some((key, index) => key !== expectedKeys[index]) - || frame.kind !== 'authenticated' - || typeof frame.authenticationCode !== 'string' - || typeof frame.connectionId !== 'string' - || (frame.direction !== 'agent-to-broker' && frame.direction !== 'broker-to-agent') - || typeof frame.payload !== 'string' - || !Number.isSafeInteger(frame.sequence) - || (frame.sequence ?? 0) < 1 - ) { - throw new Error('Authenticated frame envelope is invalid'); - } - decodeBase64UrlBytes(frame.authenticationCode); - return frame as AuthenticatedFrame; -} +export * from '@dvcol/cdb/authentication'; diff --git a/packages/websocket/test/authentication.test.ts b/packages/websocket/test/authentication.test.ts index c090bad..e5e2eb4 100644 --- a/packages/websocket/test/authentication.test.ts +++ b/packages/websocket/test/authentication.test.ts @@ -28,7 +28,7 @@ const transcript: AgentAuthenticationTranscript = { }; it('binds mutual proofs to their direction and one handshake transcript', async () => { - expect.assertions(3); + expect.assertions(5); const credential = await importAgentCredential(new Uint8Array(32).fill(7)); const agentProof = await createAgentAuthenticationProof(credential, transcript); const brokerClaims = { @@ -37,6 +37,8 @@ it('binds mutual proofs to their direction and one handshake transcript', async } as const; expect(await verifyAgentAuthenticationProof(credential, transcript, agentProof)).toBe(true); + expect(await verifyAgentAuthenticationProof(credential, { ...transcript, transportProtocol: 'chrome-debugger-bridge.rpc.v1' }, agentProof)).toBe(false); + expect(await verifyAgentAuthenticationProof(credential, { ...transcript, transportProtocol: 'chrome-debugger-bridge.agent.v1' }, agentProof)).toBe(true); expect(await verifyBrokerAuthenticationProof(credential, transcript, brokerClaims, agentProof)).toBe(false); expect(await verifyAgentAuthenticationProof( credential, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5263037..f273606 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -57,15 +57,24 @@ catalogs: specifier: 4.11.4 version: 4.11.4 devtools: + '@devframes/hub': + specifier: 0.9.10 + version: 0.9.10 + '@devframes/json-render': + specifier: 0.9.10 + version: 0.9.10 + '@devframes/json-render-ui': + specifier: 0.9.10 + version: 0.9.10 '@vitejs/devtools-kit': - specifier: 0.4.12 - version: 0.4.12 + specifier: 0.6.1 + version: 0.6.1 devframe: - specifier: 0.8.2 - version: 0.8.2 + specifier: 0.9.10 + version: 0.9.10 vite: - specifier: 8.2.0 - version: 8.2.0 + specifier: 8.1.5 + version: 8.1.5 protocol: '@standard-schema/spec': specifier: 1.1.0 @@ -123,6 +132,10 @@ catalogs: specifier: 5.9.3 version: 5.9.3 +overrides: + '@vitejs/devtools-kit>@devframes/hub': 0.9.10 + '@vitejs/devtools-kit>@devframes/json-render': 0.9.10 + importers: .: @@ -136,6 +149,12 @@ importers: '@dvcol/cdb-birpc': specifier: workspace:* version: link:packages/birpc + '@dvcol/cdb-broker': + specifier: workspace:* + version: link:packages/broker + '@dvcol/cdb-devframe': + specifier: workspace:* + version: link:packages/devframe '@dvcol/cdb-extension': specifier: workspace:* version: link:packages/extension @@ -165,7 +184,7 @@ importers: version: 26.1.2 '@vitest/browser-playwright': specifier: catalog:testing - version: 4.1.10(playwright@1.62.1)(vite@8.2.0(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0))(vitest@4.1.10) + version: 4.1.10(playwright@1.62.1)(vite@8.1.5(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0))(vitest@4.1.10) '@vitest/coverage-v8': specifier: catalog:testing version: 4.1.10(@vitest/browser@4.1.10)(vitest@4.1.10) @@ -175,6 +194,9 @@ importers: changelogithub: specifier: catalog:release version: 15.0.5 + devframe: + specifier: catalog:devtools + version: 0.9.10(@modelcontextprotocol/client@2.0.0)(cac@7.0.0)(srvx@1.0.3) eslint: specifier: catalog:build version: 10.8.0(jiti@2.7.0)(supports-color@7.2.0) @@ -207,10 +229,10 @@ importers: version: 5.9.3 vite: specifier: catalog:devtools - version: 8.2.0(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0) + version: 8.1.5(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0) vitest: specifier: catalog:testing - version: 4.1.10(@types/node@26.1.2)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(jsdom@30.0.1)(vite@8.2.0(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0)) + version: 4.1.10(@types/node@26.1.2)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(jsdom@30.0.1)(vite@8.1.5(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0)) examples/birpc: dependencies: @@ -235,21 +257,27 @@ importers: examples/devframe: dependencies: - '@dvcol/cdb': + '@dvcol/cdb-broker': specifier: workspace:* - version: link:../../packages/core - '@modelcontextprotocol/client': + version: link:../../packages/broker + '@dvcol/cdb-devframe': + specifier: workspace:* + version: link:../../packages/devframe + '@dvcol/cdb-extension': + specifier: workspace:* + version: link:../../packages/extension + '@modelcontextprotocol/server': specifier: 'catalog:' version: 2.0.0 '@vitejs/devtools-kit': specifier: catalog:devtools - version: 0.4.12(@modelcontextprotocol/client@2.0.0)(@modelcontextprotocol/server@2.0.0)(cac@7.0.0)(srvx@0.12.5)(vite@8.2.0(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0)) + version: 0.6.1(@modelcontextprotocol/client@2.0.0)(cac@7.0.0)(srvx@1.0.3)(vite@8.1.5(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0)) devframe: specifier: catalog:devtools - version: 0.8.2(@modelcontextprotocol/client@2.0.0)(@modelcontextprotocol/server@2.0.0)(cac@7.0.0)(srvx@0.12.5) + version: 0.9.10(@modelcontextprotocol/client@2.0.0)(cac@7.0.0)(srvx@1.0.3) vite: specifier: catalog:devtools - version: 8.2.0(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0) + version: 8.1.5(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0) examples/embedded: dependencies: @@ -271,7 +299,7 @@ importers: devDependencies: vite: specifier: catalog:devtools - version: 8.2.0(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0) + version: 8.1.5(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0) examples/mcp: dependencies: @@ -320,7 +348,7 @@ importers: version: 0.22.14(oxc-resolver@11.24.2)(publint@0.3.22)(typescript@5.9.3) vitest: specifier: catalog:testing - version: 4.1.10(@types/node@26.1.2)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(jsdom@30.0.1)(vite@8.2.0(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0)) + version: 4.1.10(@types/node@26.1.2)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(jsdom@30.0.1)(vite@8.1.5(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0)) packages/birpc: dependencies: @@ -339,7 +367,23 @@ importers: version: 0.22.14(oxc-resolver@11.24.2)(publint@0.3.22)(typescript@5.9.3) vitest: specifier: catalog:testing - version: 4.1.10(@types/node@26.1.2)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(jsdom@30.0.1)(vite@8.2.0(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0)) + version: 4.1.10(@types/node@26.1.2)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(jsdom@30.0.1)(vite@8.1.5(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0)) + + packages/broker: + dependencies: + '@dvcol/cdb': + specifier: workspace:^ + version: link:../core + '@dvcol/cdb-mcp': + specifier: workspace:^ + version: link:../mcp + devDependencies: + tsdown: + specifier: catalog:build + version: 0.22.14(oxc-resolver@11.24.2)(publint@0.3.22)(typescript@5.9.3) + vitest: + specifier: catalog:testing + version: 4.1.10(@types/node@26.1.2)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(jsdom@30.0.1)(vite@8.1.5(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0)) packages/core: dependencies: @@ -364,20 +408,57 @@ importers: version: 0.22.14(oxc-resolver@11.24.2)(publint@0.3.22)(typescript@5.9.3) vitest: specifier: catalog:testing - version: 4.1.10(@types/node@26.1.2)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(jsdom@30.0.1)(vite@8.2.0(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0)) + version: 4.1.10(@types/node@26.1.2)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(jsdom@30.0.1)(vite@8.1.5(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0)) + + packages/devframe: + dependencies: + '@devframes/hub': + specifier: catalog:devtools + version: 0.9.10(crossws@0.4.12(srvx@1.0.3))(devframe@0.9.10(@modelcontextprotocol/client@2.0.0)(cac@7.0.0)(srvx@1.0.3)) + '@devframes/json-render': + specifier: catalog:devtools + version: 0.9.10(@devframes/hub@0.9.10(devframe@0.9.10(@modelcontextprotocol/client@2.0.0)(cac@7.0.0)(srvx@1.0.3)))(devframe@0.9.10(@modelcontextprotocol/client@2.0.0)(cac@7.0.0)(srvx@1.0.3)) + '@devframes/json-render-ui': + specifier: catalog:devtools + version: 0.9.10(@devframes/hub@0.9.10(crossws@0.4.12(srvx@1.0.3))(devframe@0.9.10(@modelcontextprotocol/client@2.0.0)(cac@7.0.0)(srvx@1.0.3)))(devframe@0.9.10(@modelcontextprotocol/client@2.0.0)(cac@7.0.0)(srvx@1.0.3)) + '@dvcol/cdb': + specifier: workspace:^ + version: link:../core + '@dvcol/cdb-broker': + specifier: workspace:^ + version: link:../broker + '@dvcol/cdb-extension': + specifier: workspace:^ + version: link:../extension + devframe: + specifier: catalog:devtools + version: 0.9.10(@modelcontextprotocol/client@2.0.0)(cac@7.0.0)(srvx@1.0.3) + devDependencies: + tsdown: + specifier: catalog:build + version: 0.22.14(oxc-resolver@11.24.2)(publint@0.3.22)(typescript@5.9.3) + vite: + specifier: catalog:devtools + version: 8.1.5(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0) + vitest: + specifier: catalog:testing + version: 4.1.10(@types/node@26.1.2)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(jsdom@30.0.1)(vite@8.1.5(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0)) packages/extension: dependencies: '@dvcol/cdb': specifier: workspace:^ version: link:../core + '@dvcol/cdb-broker': + specifier: workspace:^ + version: link:../broker devDependencies: tsdown: specifier: catalog:build version: 0.22.14(oxc-resolver@11.24.2)(publint@0.3.22)(typescript@5.9.3) vitest: specifier: catalog:testing - version: 4.1.10(@types/node@26.1.2)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(jsdom@30.0.1)(vite@8.2.0(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0)) + version: 4.1.10(@types/node@26.1.2)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(jsdom@30.0.1)(vite@8.1.5(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0)) packages/mcp: dependencies: @@ -405,7 +486,7 @@ importers: version: 0.22.14(oxc-resolver@11.24.2)(publint@0.3.22)(typescript@5.9.3) vitest: specifier: catalog:testing - version: 4.1.10(@types/node@26.1.2)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(jsdom@30.0.1)(vite@8.2.0(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0)) + version: 4.1.10(@types/node@26.1.2)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(jsdom@30.0.1)(vite@8.1.5(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0)) packages/websocket: dependencies: @@ -424,7 +505,7 @@ importers: version: 0.22.14(oxc-resolver@11.24.2)(publint@0.3.22)(typescript@5.9.3) vitest: specifier: catalog:testing - version: 4.1.10(@types/node@26.1.2)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(jsdom@30.0.1)(vite@8.2.0(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0)) + version: 4.1.10(@types/node@26.1.2)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(jsdom@30.0.1)(vite@8.1.5(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0)) packages: @@ -575,16 +656,27 @@ packages: resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} engines: {node: '>=20.19.0'} - '@devframes/hub@0.8.2': - resolution: {integrity: sha512-3SE+aUy+u1LeMClVK0S9FDmfA3j+kvzgyiIEz7gA+bek0chMuVB0A05ZBdLCZ6E/Tq/CKD7svI7L7ntgStCVew==} + '@devframes/hub@0.9.10': + resolution: {integrity: sha512-BFjO0iTEIazdNJbqvJkixV6d7DdUGaCpIHEsAZob2Lc59usOa8g4+jslRepbLhIraXm7SNUzP3w8Li9OBFHcdw==} + peerDependencies: + devframe: 0.9.10 + + '@devframes/json-render-ui@0.9.10': + resolution: {integrity: sha512-X/TQfmWINCstof3EjDjiwm0sBEhSWRmIhwt/TiwP/7jnAb+IkvFqrZSgjjQ/FVaHQMN0oZr1yutW6ih+7ka1og==} peerDependencies: - devframe: 0.8.2 + '@devframes/hub': 0.9.10 + devframe: 0.9.10 + peerDependenciesMeta: + '@devframes/hub': + optional: true + devframe: + optional: true - '@devframes/json-render@0.8.2': - resolution: {integrity: sha512-LpIMMXDZGHsIlrNDWo1PbLenZcYRlxZ4lZ/vR/5j8bEh9FsSSY5Sd6RVPL93lyl4jz+2Sc+S2j4BKKzk9Zi8Fg==} + '@devframes/json-render@0.9.10': + resolution: {integrity: sha512-IWFJo1KEwsI4VcAEB3akuFLnqMopKsXQSIdOyB7ojnmHMua5X1eBSJkv7hMlyER4kFLCGrWl+U6Hz2aNArws9g==} peerDependencies: - '@devframes/hub': 0.8.2 - devframe: 0.8.2 + '@devframes/hub': 0.9.10 + devframe: 0.9.10 peerDependenciesMeta: '@devframes/hub': optional: true @@ -625,12 +717,18 @@ packages: oxlint: optional: true + '@emnapi/core@1.11.1': + resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} + '@emnapi/core@1.11.2': resolution: {integrity: sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA==} '@emnapi/core@2.0.0-alpha.3': resolution: {integrity: sha512-AZypUeJ/yByuxyS7BlSNRDOMLMlROYtjYdIAuBmJssVz1UJDSeYxLrdizhXCFYhedC5bqd/ASy8EuNXbVVXp9g==} + '@emnapi/runtime@1.11.1': + resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} + '@emnapi/runtime@1.11.2': resolution: {integrity: sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==} @@ -761,8 +859,8 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - '@json-render/core@0.19.0': - resolution: {integrity: sha512-vvcyZ+10EDZKbEyB1J2kXOGfDaiZR2LurZGSqi2r5STHyKr+Te85DWaBxTwRGgM7U1LtIvNx85BzzjElRKoAIg==} + '@json-render/core@0.20.0': + resolution: {integrity: sha512-cXAXn7h3QaylefQgh85AEGPuVq4C8SJ3k9JqigWLJhaBSozF1Fmsrm43DP16RmkqNiVgj5pBRdmB48lEO5qlow==} '@modelcontextprotocol/client@2.0.0': resolution: {integrity: sha512-8f1OghQ2rjzIOfqgUCP+8GiUWqRs89njoWLNqAe8kWmDePv3s1fZXseej+QXemssEuuOvLLmLO/kqM3IQHtISw==} @@ -919,6 +1017,9 @@ packages: cpu: [x64] os: [win32] + '@oxc-project/types@0.139.0': + resolution: {integrity: sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==} + '@oxc-project/types@0.142.0': resolution: {integrity: sha512-7W+2q5AKQVU36fkaryontrHn3YDt1RyUYXatw9i5H8ocYe2sPKSFB6eS8WNPeRKiN1qAWWZUPm7gwFzJGrccqQ==} @@ -1169,36 +1270,73 @@ packages: '@quansync/fs@1.0.0': resolution: {integrity: sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==} + '@rolldown/binding-android-arm64@1.1.5': + resolution: {integrity: sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + '@rolldown/binding-android-arm64@1.2.1': resolution: {integrity: sha512-02hOeOSryYxVrOIphmLAsqnCJWxwlzFk+pEt/N/i6OgT3lShHO7xGCU5cpgchRDHboAEbSjzgGh+O/u1GswQmA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] + '@rolldown/binding-darwin-arm64@1.1.5': + resolution: {integrity: sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + '@rolldown/binding-darwin-arm64@1.2.1': resolution: {integrity: sha512-fMsTOnN0OjFm3CyppWPitKnc8UlliVARUULW6cfU6AIqjdtgmSFWSk9vecHzZduv/yMWIHDlRhM1e8Iff9uAfA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] + '@rolldown/binding-darwin-x64@1.1.5': + resolution: {integrity: sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + '@rolldown/binding-darwin-x64@1.2.1': resolution: {integrity: sha512-1wjKdz/XLGKHaTNHjQveQ/B23TKx4ItAqm1JbyVuvNPc4Ze0Fb48s49TAd/2zcplPl8okE/UbTgmlVfwT7eFeQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] + '@rolldown/binding-freebsd-x64@1.1.5': + resolution: {integrity: sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + '@rolldown/binding-freebsd-x64@1.2.1': resolution: {integrity: sha512-Fa0jHR07E7YBN4vOEsbVf2briYNsuOowfLJaXULZM0ldMlaCaj2LJgLMbMe4iacRyZmvR8efFhgR9wKuGclQUg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] + '@rolldown/binding-linux-arm-gnueabihf@1.1.5': + resolution: {integrity: sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + '@rolldown/binding-linux-arm-gnueabihf@1.2.1': resolution: {integrity: sha512-pzkgu1SSHGgRRyRZ4fbmSgmajbVt+epaLP99NDjFft69v/ypfTi6swBMiVdh2EkQ0OSnHE1lZDM7DRGkyAzUpA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] + '@rolldown/binding-linux-arm64-gnu@1.1.5': + resolution: {integrity: sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + '@rolldown/binding-linux-arm64-gnu@1.2.1': resolution: {integrity: sha512-QI5SEDY8cbiYWHx0VO4vIc3UlS6a32vXHjU8Qy/17adEmZIPuByJg13UEvo9c/UCiUkdcVWY83C+b+JrwnNyUg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1206,6 +1344,13 @@ packages: os: [linux] libc: [glibc] + '@rolldown/binding-linux-arm64-musl@1.1.5': + resolution: {integrity: sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + '@rolldown/binding-linux-arm64-musl@1.2.1': resolution: {integrity: sha512-Sm41FyCeXqmYcERoYOCbGIL5hNfd8w9LQ7Y61Bev48HkcjaJqV/iiVOaiDxjVTRMS+QKrZmD8cfPt4uMVnvM+A==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1213,6 +1358,13 @@ packages: os: [linux] libc: [musl] + '@rolldown/binding-linux-ppc64-gnu@1.1.5': + resolution: {integrity: sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + '@rolldown/binding-linux-ppc64-gnu@1.2.1': resolution: {integrity: sha512-2x+WhXTGl9yJYPbltW/BSEPTVz9OIWQyER4N+gJEDWkkn904eRcBzELqh/Hf7K0w/ubGbKNMv0ZC+94QK/IFEg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1220,6 +1372,13 @@ packages: os: [linux] libc: [glibc] + '@rolldown/binding-linux-s390x-gnu@1.1.5': + resolution: {integrity: sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + '@rolldown/binding-linux-s390x-gnu@1.2.1': resolution: {integrity: sha512-eEjmQpuRQayHPWWnywaWHkFT3ToPbP3RYy42VVd/B9aBGDA+Ol25EIWHxKQST3IiWJjikCWUF7KtbfqwZrzVwQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1227,6 +1386,13 @@ packages: os: [linux] libc: [glibc] + '@rolldown/binding-linux-x64-gnu@1.1.5': + resolution: {integrity: sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + '@rolldown/binding-linux-x64-gnu@1.2.1': resolution: {integrity: sha512-/Orga1fZYkLc/56jBICcHrKchl8Z2UKdDSr3LG9ToWO1lQ6a4Livk9Xz+9WN91zsz5QR3XQz2NNoSDEvP6qadw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1234,6 +1400,13 @@ packages: os: [linux] libc: [glibc] + '@rolldown/binding-linux-x64-musl@1.1.5': + resolution: {integrity: sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + '@rolldown/binding-linux-x64-musl@1.2.1': resolution: {integrity: sha512-xxBJRL+0q0Kce7orznGWLuylHDY65vuARXZRpX+hPdv+DqK2c3NlCsVA98tlWzWNEE7yPqA/1NQ5nnCrj49Y5A==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1241,22 +1414,45 @@ packages: os: [linux] libc: [musl] + '@rolldown/binding-openharmony-arm64@1.1.5': + resolution: {integrity: sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + '@rolldown/binding-openharmony-arm64@1.2.1': resolution: {integrity: sha512-M6AdXIXw3s+/8XpKMzdGDEXGS1S7kwUsy+rcTIUIOx5Ge4nXKCtAFHFV9YKkXvGcC5WMoTjAteLzlsQROVI0Yw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] + '@rolldown/binding-wasm32-wasi@1.1.5': + resolution: {integrity: sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + '@rolldown/binding-wasm32-wasi@1.2.1': resolution: {integrity: sha512-/TX0SoRGojHzSAHpfVBbavRVSazg5U3h3Y3VXfcc0cdugq6kxdqw8LPGFiPr+/7gE/60zRcsOY2Vi9b9eT0jww==} engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0} + '@rolldown/binding-win32-arm64-msvc@1.1.5': + resolution: {integrity: sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + '@rolldown/binding-win32-arm64-msvc@1.2.1': resolution: {integrity: sha512-EvRrivJieyHG+AO9lleZWgq+g0+S7oV2C51yuqlcyU/R9net+sI4Pj0F+lUoP2bEr6TWX3SqFaaS0SzfLxSzkw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] + '@rolldown/binding-win32-x64-msvc@1.1.5': + resolution: {integrity: sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + '@rolldown/binding-win32-x64-msvc@1.2.1': resolution: {integrity: sha512-Z4eCmn5QJ/5+azF9knpLWKfVd9aidn0mAe9TpJgvBLId9Ax3t0+JVxBmT25Bv7NBbVW1TZyKjQjQReouMeH5UQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1416,8 +1612,8 @@ packages: resolution: {integrity: sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@vitejs/devtools-kit@0.4.12': - resolution: {integrity: sha512-3S2kT/ZwGy49zkEmOfXb/Z4fEuRzZgi4+CgL0tKvxxAFj526R5wGRT2jASWu6VaSsPVYqob9REbHvLJzEJtk6g==} + '@vitejs/devtools-kit@0.6.1': + resolution: {integrity: sha512-0+KY6TU5ukw/onaY3FdffSml/LZGuAauilbRqPYerS3GEkbgO3JKPYw4UsjlL5IahZ1AK9QpbPfoqhf5dhw8Og==} peerDependencies: vite: '*' @@ -1693,6 +1889,9 @@ packages: birpc@4.0.0: resolution: {integrity: sha512-LShSxJP0KTmd101b6DRyGBj57LZxSDYWKitQNW/mi8GRMvZb078Uf9+pveax1DrVL89vm7mWe+TovdI/UDOuPw==} + birpc@4.2.0: + resolution: {integrity: sha512-KxgKcZPfrtzJDDALHPguGpGJUrzdgpymyiQQgzFjWreHMOpWrnFNVREr5J48x2DBh8ZVioscrV1SBkDipGiX+Q==} + boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} @@ -1842,8 +2041,8 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} - crossws@0.4.10: - resolution: {integrity: sha512-pz3oubH/dt12KjqsUB0IuXW4nwRDQ583iDsP4555Cpdqx0NoU7pGlWBcayyFI8f/l/idRpgjMEfwuOxSWJYlIA==} + crossws@0.4.12: + resolution: {integrity: sha512-aypfsr6t0uNvkqaZc6zvBfXzC6pLI0/sIulpkV6RwCVtZqG5ebBzv4weImKK0VNCj91Wl9F5j7p5WU4MNrybng==} peerDependencies: srvx: '>=0.11.5' peerDependenciesMeta: @@ -1907,18 +2106,15 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} - devframe@0.8.2: - resolution: {integrity: sha512-oodQXEQnrvufA1so/E058Z3SG3bk9AlUE5+NIAsyx5ZJH9P/oX6WMxjC0bs5PPwtKbS7pBHjxja2AD8EwGE8/Q==} + devframe@0.9.10: + resolution: {integrity: sha512-pz/dYWrffiyXHeh/R10HhL3koamfQIr8Smu/JMDnxE9u6KaA60jM4Kh8Nd5el55CkDmjvbu+EFuApxP8gZHHWw==} hasBin: true peerDependencies: '@modelcontextprotocol/client': ^2.0.0 - '@modelcontextprotocol/server': ^2.0.0 cac: ^7.0.0 peerDependenciesMeta: '@modelcontextprotocol/client': optional: true - '@modelcontextprotocol/server': - optional: true cac: optional: true @@ -2345,15 +2541,18 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - h3@2.0.1-rc.26: - resolution: {integrity: sha512-GDxlvDsKxgjRvG5UBRJYyGJTMWLV30CJ4cV+e7QCTgftDHihrvio1fVPbNembhEr6J4WNm8IWy3fookgyTweLw==} + h3@2.0.1-rc.31: + resolution: {integrity: sha512-AG7qZzF99a0BSYoXT3evJgIhwSIUKow7R+hwkLRZS06WJ9nbSb7QXUDgs1ByBjp6svTvl++N/GKA7I9YPz9cQQ==} engines: {node: '>=20.11.1'} hasBin: true peerDependencies: - crossws: ^0.4.9 + crossws: ^0.4.12 + ocache: '>=0.3.0' peerDependenciesMeta: crossws: optional: true + ocache: + optional: true has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} @@ -3073,6 +3272,11 @@ packages: vue-tsc: optional: true + rolldown@1.1.5: + resolution: {integrity: sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + rolldown@1.2.1: resolution: {integrity: sha512-4FKJhg8d3OiyQOA6Q1Q0hoFFpW9/OoX+VsHzpECsdsIZoOArrAK90gl59YK/Z+gnDel45bgJZK03ozH/9bCqEw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -3144,8 +3348,8 @@ packages: spdx-license-ids@3.0.23: resolution: {integrity: sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==} - srvx@0.12.5: - resolution: {integrity: sha512-IuvtDNQg5EIwv3c6dleyau7u8hCyGQ7D6+V/QM799Aud07z0wCUcurKLTRfyG33C8oUY+UWcVBFkfHMcbtmRLA==} + srvx@1.0.3: + resolution: {integrity: sha512-aOuk+yNJA61yA6eJJvQJxpJLIxz090hKMqDGzd35tGsvwaodi1RPuYSFUp5EINaJp7snjVwLkO2RXVMPZBYnnA==} engines: {node: '>=20.16.0'} hasBin: true @@ -3368,13 +3572,13 @@ packages: resolution: {integrity: sha512-sXMwN6DMHeouPfCxkxWkKAmxphWKEenHYY5H1nIBzU3PmDsmJp6kBXJdshjVdpMZuWCmL9SH7KFRx29AylpP6g==} engines: {node: '>=18.12.0'} - vite@8.2.0: - resolution: {integrity: sha512-pn+CFpM0lwDeKwmOq1ZaBK/9sjorZcgqxki6MbY/jPEVd9vichIlmlD4HmQ5wdP5EgqQCFRaACBxMC7uEGc6lQ==} + vite@8.1.5: + resolution: {integrity: sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: '@types/node': ^20.19.0 || >=22.12.0 - '@vitejs/devtools': ^0.4.0 + '@vitejs/devtools': ^0.3.0 esbuild: ^0.27.0 || ^0.28.0 jiti: '>=1.21.0' less: ^4.0.0 @@ -3554,6 +3758,9 @@ packages: zod@4.4.3: resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + zod@4.5.4: + resolution: {integrity: sha512-sC95tT5iHHH9gtpj6A81kh+NEaRAUFN+qlUPDUbRfOMvNf5QCBqsb3WgvnpVtK5Y+4UfA6KqufotuTvMGiTlsA==} + zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -3688,25 +3895,34 @@ snapshots: '@csstools/css-tokenizer@4.0.0': {} - '@devframes/hub@0.8.2(devframe@0.8.2(@modelcontextprotocol/client@2.0.0)(@modelcontextprotocol/server@2.0.0)(cac@7.0.0)(srvx@0.12.5))': + '@devframes/hub@0.9.10(crossws@0.4.12(srvx@1.0.3))(devframe@0.9.10(@modelcontextprotocol/client@2.0.0)(cac@7.0.0)(srvx@1.0.3))': dependencies: '@standard-schema/spec': 1.1.0 destr: 2.0.5 - devframe: 0.8.2(@modelcontextprotocol/client@2.0.0)(@modelcontextprotocol/server@2.0.0)(cac@7.0.0)(srvx@0.12.5) - nostics: 1.2.0 + devframe: 0.9.10(@modelcontextprotocol/client@2.0.0)(cac@7.0.0)(srvx@1.0.3) + h3: 2.0.1-rc.31(crossws@0.4.12(srvx@1.0.3)) pathe: 2.0.3 perfect-debounce: 2.1.0 tinyexec: 1.3.0 + ufo: 1.6.4 zigpty: 0.2.1 + transitivePeerDependencies: + - crossws + - ocache + + '@devframes/json-render-ui@0.9.10(@devframes/hub@0.9.10(crossws@0.4.12(srvx@1.0.3))(devframe@0.9.10(@modelcontextprotocol/client@2.0.0)(cac@7.0.0)(srvx@1.0.3)))(devframe@0.9.10(@modelcontextprotocol/client@2.0.0)(cac@7.0.0)(srvx@1.0.3))': + optionalDependencies: + '@devframes/hub': 0.9.10(crossws@0.4.12(srvx@1.0.3))(devframe@0.9.10(@modelcontextprotocol/client@2.0.0)(cac@7.0.0)(srvx@1.0.3)) + devframe: 0.9.10(@modelcontextprotocol/client@2.0.0)(cac@7.0.0)(srvx@1.0.3) - '@devframes/json-render@0.8.2(@devframes/hub@0.8.2(devframe@0.8.2(@modelcontextprotocol/client@2.0.0)(@modelcontextprotocol/server@2.0.0)(cac@7.0.0)(srvx@0.12.5)))(devframe@0.8.2(@modelcontextprotocol/client@2.0.0)(@modelcontextprotocol/server@2.0.0)(cac@7.0.0)(srvx@0.12.5))': + '@devframes/json-render@0.9.10(@devframes/hub@0.9.10(devframe@0.9.10(@modelcontextprotocol/client@2.0.0)(cac@7.0.0)(srvx@1.0.3)))(devframe@0.9.10(@modelcontextprotocol/client@2.0.0)(cac@7.0.0)(srvx@1.0.3))': dependencies: - '@json-render/core': 0.19.0 - devframe: 0.8.2(@modelcontextprotocol/client@2.0.0)(@modelcontextprotocol/server@2.0.0)(cac@7.0.0)(srvx@0.12.5) - nostics: 1.2.0 - zod: 4.4.3 + '@json-render/core': 0.20.0 + '@standard-schema/spec': 1.1.0 + devframe: 0.9.10(@modelcontextprotocol/client@2.0.0)(cac@7.0.0)(srvx@1.0.3) + zod: 4.5.4 optionalDependencies: - '@devframes/hub': 0.8.2(devframe@0.8.2(@modelcontextprotocol/client@2.0.0)(@modelcontextprotocol/server@2.0.0)(cac@7.0.0)(srvx@0.12.5)) + '@devframes/hub': 0.9.10(crossws@0.4.12(srvx@1.0.3))(devframe@0.9.10(@modelcontextprotocol/client@2.0.0)(cac@7.0.0)(srvx@1.0.3)) '@dprint/formatter@0.5.1': {} @@ -3755,6 +3971,12 @@ snapshots: optionalDependencies: eslint: 10.8.0(jiti@2.7.0)(supports-color@7.2.0) + '@emnapi/core@1.11.1': + dependencies: + '@emnapi/wasi-threads': 1.2.2 + tslib: 2.8.1 + optional: true + '@emnapi/core@1.11.2': dependencies: '@emnapi/wasi-threads': 1.2.2 @@ -3767,6 +3989,11 @@ snapshots: tslib: 2.8.1 optional: true + '@emnapi/runtime@1.11.1': + dependencies: + tslib: 2.8.1 + optional: true + '@emnapi/runtime@1.11.2': dependencies: tslib: 2.8.1 @@ -3905,9 +4132,9 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@json-render/core@0.19.0': + '@json-render/core@0.20.0': dependencies: - zod: 4.4.3 + zod: 4.5.4 '@modelcontextprotocol/client@2.0.0': dependencies: @@ -3935,6 +4162,13 @@ snapshots: '@modelcontextprotocol/core': 2.0.0 zod: 4.4.3 + '@napi-rs/wasm-runtime@1.2.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@tybys/wasm-util': 0.10.3 + optional: true + '@napi-rs/wasm-runtime@1.2.2(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)': dependencies: '@emnapi/core': 1.11.2 @@ -4008,6 +4242,8 @@ snapshots: '@oxc-parser/binding-win32-x64-msvc@0.147.0': optional: true + '@oxc-project/types@0.139.0': {} + '@oxc-project/types@0.142.0': {} '@oxc-project/types@0.147.0': {} @@ -4147,42 +4383,85 @@ snapshots: dependencies: quansync: 1.0.0 + '@rolldown/binding-android-arm64@1.1.5': + optional: true + '@rolldown/binding-android-arm64@1.2.1': optional: true + '@rolldown/binding-darwin-arm64@1.1.5': + optional: true + '@rolldown/binding-darwin-arm64@1.2.1': optional: true + '@rolldown/binding-darwin-x64@1.1.5': + optional: true + '@rolldown/binding-darwin-x64@1.2.1': optional: true + '@rolldown/binding-freebsd-x64@1.1.5': + optional: true + '@rolldown/binding-freebsd-x64@1.2.1': optional: true + '@rolldown/binding-linux-arm-gnueabihf@1.1.5': + optional: true + '@rolldown/binding-linux-arm-gnueabihf@1.2.1': optional: true + '@rolldown/binding-linux-arm64-gnu@1.1.5': + optional: true + '@rolldown/binding-linux-arm64-gnu@1.2.1': optional: true + '@rolldown/binding-linux-arm64-musl@1.1.5': + optional: true + '@rolldown/binding-linux-arm64-musl@1.2.1': optional: true + '@rolldown/binding-linux-ppc64-gnu@1.1.5': + optional: true + '@rolldown/binding-linux-ppc64-gnu@1.2.1': optional: true + '@rolldown/binding-linux-s390x-gnu@1.1.5': + optional: true + '@rolldown/binding-linux-s390x-gnu@1.2.1': optional: true + '@rolldown/binding-linux-x64-gnu@1.1.5': + optional: true + '@rolldown/binding-linux-x64-gnu@1.2.1': optional: true + '@rolldown/binding-linux-x64-musl@1.1.5': + optional: true + '@rolldown/binding-linux-x64-musl@1.2.1': optional: true + '@rolldown/binding-openharmony-arm64@1.1.5': + optional: true + '@rolldown/binding-openharmony-arm64@1.2.1': optional: true + '@rolldown/binding-wasm32-wasi@1.1.5': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@napi-rs/wasm-runtime': 1.2.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) + optional: true + '@rolldown/binding-wasm32-wasi@1.2.1': dependencies: '@emnapi/core': 2.0.0-alpha.3 @@ -4190,9 +4469,15 @@ snapshots: '@napi-rs/wasm-runtime': 1.2.2(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3) optional: true + '@rolldown/binding-win32-arm64-msvc@1.1.5': + optional: true + '@rolldown/binding-win32-arm64-msvc@1.2.1': optional: true + '@rolldown/binding-win32-x64-msvc@1.1.5': + optional: true + '@rolldown/binding-win32-x64-msvc@1.2.1': optional: true @@ -4210,7 +4495,7 @@ snapshots: eslint-visitor-keys: 4.2.1 espree: 10.4.0 estraverse: 5.3.0 - picomatch: 4.0.5 + picomatch: 4.0.7 '@turbo/darwin-64@2.10.8': optional: true @@ -4372,45 +4657,46 @@ snapshots: '@typescript-eslint/types': 8.65.0 eslint-visitor-keys: 5.0.1 - '@vitejs/devtools-kit@0.4.12(@modelcontextprotocol/client@2.0.0)(@modelcontextprotocol/server@2.0.0)(cac@7.0.0)(srvx@0.12.5)(vite@8.2.0(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0))': + '@vitejs/devtools-kit@0.6.1(@modelcontextprotocol/client@2.0.0)(cac@7.0.0)(srvx@1.0.3)(vite@8.1.5(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0))': dependencies: - '@devframes/hub': 0.8.2(devframe@0.8.2(@modelcontextprotocol/client@2.0.0)(@modelcontextprotocol/server@2.0.0)(cac@7.0.0)(srvx@0.12.5)) - '@devframes/json-render': 0.8.2(@devframes/hub@0.8.2(devframe@0.8.2(@modelcontextprotocol/client@2.0.0)(@modelcontextprotocol/server@2.0.0)(cac@7.0.0)(srvx@0.12.5)))(devframe@0.8.2(@modelcontextprotocol/client@2.0.0)(@modelcontextprotocol/server@2.0.0)(cac@7.0.0)(srvx@0.12.5)) - devframe: 0.8.2(@modelcontextprotocol/client@2.0.0)(@modelcontextprotocol/server@2.0.0)(cac@7.0.0)(srvx@0.12.5) + '@devframes/hub': 0.9.10(crossws@0.4.12(srvx@1.0.3))(devframe@0.9.10(@modelcontextprotocol/client@2.0.0)(cac@7.0.0)(srvx@1.0.3)) + '@devframes/json-render': 0.9.10(@devframes/hub@0.9.10(devframe@0.9.10(@modelcontextprotocol/client@2.0.0)(cac@7.0.0)(srvx@1.0.3)))(devframe@0.9.10(@modelcontextprotocol/client@2.0.0)(cac@7.0.0)(srvx@1.0.3)) + devframe: 0.9.10(@modelcontextprotocol/client@2.0.0)(cac@7.0.0)(srvx@1.0.3) local-pkg: 1.2.1 mlly: 1.8.2 nostics: 1.2.0 nypm: 0.6.9 - vite: 8.2.0(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0) + vite: 8.1.5(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0) transitivePeerDependencies: - '@modelcontextprotocol/client' - - '@modelcontextprotocol/server' - cac + - crossws + - ocache - srvx - '@vitest/browser-playwright@4.1.10(playwright@1.62.1)(vite@8.2.0(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0))(vitest@4.1.10)': + '@vitest/browser-playwright@4.1.10(playwright@1.62.1)(vite@8.1.5(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0))(vitest@4.1.10)': dependencies: - '@vitest/browser': 4.1.10(vite@8.2.0(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0))(vitest@4.1.10) - '@vitest/mocker': 4.1.10(vite@8.2.0(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0)) + '@vitest/browser': 4.1.10(vite@8.1.5(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0))(vitest@4.1.10) + '@vitest/mocker': 4.1.10(vite@8.1.5(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0)) playwright: 1.62.1 tinyrainbow: 3.1.1 - vitest: 4.1.10(@types/node@26.1.2)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(jsdom@30.0.1)(vite@8.2.0(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0)) + vitest: 4.1.10(@types/node@26.1.2)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(jsdom@30.0.1)(vite@8.1.5(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0)) transitivePeerDependencies: - bufferutil - msw - utf-8-validate - vite - '@vitest/browser@4.1.10(vite@8.2.0(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0))(vitest@4.1.10)': + '@vitest/browser@4.1.10(vite@8.1.5(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0))(vitest@4.1.10)': dependencies: '@blazediff/core': 1.9.1 - '@vitest/mocker': 4.1.10(vite@8.2.0(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0)) + '@vitest/mocker': 4.1.10(vite@8.1.5(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0)) '@vitest/utils': 4.1.10 magic-string: 0.30.21 pngjs: 7.0.0 sirv: 3.0.2 tinyrainbow: 3.1.1 - vitest: 4.1.10(@types/node@26.1.2)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(jsdom@30.0.1)(vite@8.2.0(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0)) + vitest: 4.1.10(@types/node@26.1.2)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(jsdom@30.0.1)(vite@8.1.5(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0)) ws: 8.21.1 transitivePeerDependencies: - bufferutil @@ -4430,9 +4716,9 @@ snapshots: obug: 2.1.4 std-env: 4.2.0 tinyrainbow: 3.1.1 - vitest: 4.1.10(@types/node@26.1.2)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(jsdom@30.0.1)(vite@8.2.0(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0)) + vitest: 4.1.10(@types/node@26.1.2)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(jsdom@30.0.1)(vite@8.1.5(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0)) optionalDependencies: - '@vitest/browser': 4.1.10(vite@8.2.0(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0))(vitest@4.1.10) + '@vitest/browser': 4.1.10(vite@8.1.5(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0))(vitest@4.1.10) '@vitest/eslint-plugin@1.6.25(@typescript-eslint/eslint-plugin@8.65.0(@typescript-eslint/parser@8.65.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)(vitest@4.1.10)': dependencies: @@ -4442,7 +4728,7 @@ snapshots: optionalDependencies: '@typescript-eslint/eslint-plugin': 8.65.0(@typescript-eslint/parser@8.65.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) typescript: 5.9.3 - vitest: 4.1.10(@types/node@26.1.2)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(jsdom@30.0.1)(vite@8.2.0(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0)) + vitest: 4.1.10(@types/node@26.1.2)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(jsdom@30.0.1)(vite@8.1.5(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0)) transitivePeerDependencies: - supports-color @@ -4455,13 +4741,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.1 - '@vitest/mocker@4.1.10(vite@8.2.0(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0))': + '@vitest/mocker@4.1.10(vite@8.1.5(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0))': dependencies: '@vitest/spy': 4.1.10 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.2.0(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0) + vite: 8.1.5(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0) '@vitest/pretty-format@4.1.10': dependencies: @@ -4622,6 +4908,8 @@ snapshots: birpc@4.0.0: {} + birpc@4.2.0: {} + boolbase@1.0.0: {} brace-expansion@5.0.9: @@ -4801,9 +5089,9 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 - crossws@0.4.10(srvx@0.12.5): + crossws@0.4.12(srvx@1.0.3): optionalDependencies: - srvx: 0.12.5 + srvx: 1.0.3 css-tree@3.2.1: dependencies: @@ -4850,22 +5138,22 @@ snapshots: detect-libc@2.1.2: {} - devframe@0.8.2(@modelcontextprotocol/client@2.0.0)(@modelcontextprotocol/server@2.0.0)(cac@7.0.0)(srvx@0.12.5): + devframe@0.9.10(@modelcontextprotocol/client@2.0.0)(cac@7.0.0)(srvx@1.0.3): dependencies: + '@modelcontextprotocol/server': 2.0.0 '@standard-schema/spec': 1.1.0 - birpc: 4.0.0 - crossws: 0.4.10(srvx@0.12.5) - destr: 2.0.5 - h3: 2.0.1-rc.26(crossws@0.4.10(srvx@0.12.5)) + birpc: 4.2.0 + crossws: 0.4.12(srvx@1.0.3) + h3: 2.0.1-rc.31(crossws@0.4.12(srvx@1.0.3)) mrmime: 2.0.1 nostics: 1.2.0 pathe: 2.0.3 ufo: 1.6.4 optionalDependencies: '@modelcontextprotocol/client': 2.0.0 - '@modelcontextprotocol/server': 2.0.0 cac: 7.0.0 transitivePeerDependencies: + - ocache - srvx devlop@1.1.0: @@ -5333,12 +5621,12 @@ snapshots: graceful-fs@4.2.11: {} - h3@2.0.1-rc.26(crossws@0.4.10(srvx@0.12.5)): + h3@2.0.1-rc.31(crossws@0.4.12(srvx@1.0.3)): dependencies: rou3: 0.9.2 - srvx: 0.12.5 + srvx: 1.0.3 optionalDependencies: - crossws: 0.4.10(srvx@0.12.5) + crossws: 0.4.12(srvx@1.0.3) has-flag@4.0.0: {} @@ -6235,6 +6523,27 @@ snapshots: transitivePeerDependencies: - oxc-resolver + rolldown@1.1.5: + dependencies: + '@oxc-project/types': 0.139.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.1.5 + '@rolldown/binding-darwin-arm64': 1.1.5 + '@rolldown/binding-darwin-x64': 1.1.5 + '@rolldown/binding-freebsd-x64': 1.1.5 + '@rolldown/binding-linux-arm-gnueabihf': 1.1.5 + '@rolldown/binding-linux-arm64-gnu': 1.1.5 + '@rolldown/binding-linux-arm64-musl': 1.1.5 + '@rolldown/binding-linux-ppc64-gnu': 1.1.5 + '@rolldown/binding-linux-s390x-gnu': 1.1.5 + '@rolldown/binding-linux-x64-gnu': 1.1.5 + '@rolldown/binding-linux-x64-musl': 1.1.5 + '@rolldown/binding-openharmony-arm64': 1.1.5 + '@rolldown/binding-wasm32-wasi': 1.1.5 + '@rolldown/binding-win32-arm64-msvc': 1.1.5 + '@rolldown/binding-win32-x64-msvc': 1.1.5 + rolldown@1.2.1: dependencies: '@oxc-project/types': 0.142.0 @@ -6309,7 +6618,7 @@ snapshots: spdx-license-ids@3.0.23: {} - srvx@0.12.5: {} + srvx@1.0.3: {} stackback@0.0.2: {} @@ -6405,7 +6714,7 @@ snapshots: ts-declaration-location@1.0.7(typescript@5.9.3): dependencies: - picomatch: 4.0.5 + picomatch: 4.0.7 typescript: 5.9.3 tsdown@0.22.14(oxc-resolver@11.24.2)(publint@0.3.22)(typescript@5.9.3): @@ -6508,12 +6817,12 @@ snapshots: verkit@0.4.0: {} - vite@8.2.0(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0): + vite@8.1.5(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0): dependencies: lightningcss: 1.33.0 - picomatch: 4.0.5 + picomatch: 4.0.7 postcss: 8.5.25 - rolldown: 1.2.1 + rolldown: 1.1.5 tinyglobby: 0.2.17 optionalDependencies: '@types/node': 26.1.2 @@ -6521,10 +6830,10 @@ snapshots: jiti: 2.7.0 yaml: 2.9.0 - vitest@4.1.10(@types/node@26.1.2)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(jsdom@30.0.1)(vite@8.2.0(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0)): + vitest@4.1.10(@types/node@26.1.2)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(jsdom@30.0.1)(vite@8.1.5(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0)): dependencies: '@vitest/expect': 4.1.10 - '@vitest/mocker': 4.1.10(vite@8.2.0(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0)) + '@vitest/mocker': 4.1.10(vite@8.1.5(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0)) '@vitest/pretty-format': 4.1.10 '@vitest/runner': 4.1.10 '@vitest/snapshot': 4.1.10 @@ -6541,11 +6850,11 @@ snapshots: tinyexec: 1.3.0 tinyglobby: 0.2.17 tinyrainbow: 3.1.1 - vite: 8.2.0(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0) + vite: 8.1.5(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 26.1.2 - '@vitest/browser-playwright': 4.1.10(playwright@1.62.1)(vite@8.2.0(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0))(vitest@4.1.10) + '@vitest/browser-playwright': 4.1.10(playwright@1.62.1)(vite@8.1.5(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0))(vitest@4.1.10) '@vitest/coverage-v8': 4.1.10(@vitest/browser@4.1.10)(vitest@4.1.10) jsdom: 30.0.1 transitivePeerDependencies: @@ -6666,4 +6975,6 @@ snapshots: zod@4.4.3: {} + zod@4.5.4: {} + zwitch@2.0.4: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 3ae7773..602b8e8 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -13,6 +13,9 @@ packages: - packages/* - examples/* - tests/fixtures/* +overrides: + '@vitejs/devtools-kit>@devframes/hub': 0.9.10 + '@vitejs/devtools-kit>@devframes/json-render': 0.9.10 catalog: '@modelcontextprotocol/client': 2.0.0 '@modelcontextprotocol/node': 2.0.0 @@ -34,9 +37,12 @@ catalogs: tsdown: 0.22.14 turbo: 2.10.8 devtools: - '@vitejs/devtools-kit': 0.4.12 - devframe: 0.8.2 - vite: 8.2.0 + '@devframes/hub': 0.9.10 + '@devframes/json-render': 0.9.10 + '@devframes/json-render-ui': 0.9.10 + '@vitejs/devtools-kit': 0.6.1 + devframe: 0.9.10 + vite: 8.1.5 protocol: '@standard-schema/spec': 1.1.0 devtools-protocol: 0.0.1672245 diff --git a/tests/browser/authenticated-agent-websocket.test.ts b/tests/browser/authenticated-agent-websocket.test.ts index 97be8be..f177877 100644 --- a/tests/browser/authenticated-agent-websocket.test.ts +++ b/tests/browser/authenticated-agent-websocket.test.ts @@ -7,6 +7,22 @@ import { createIndexedDbPairingStore } from '../../packages/extension/src/index. import { connectAgentWebSocket } from '../../packages/websocket/src/browser.js'; import { agentWebSocketProtocol } from '../../packages/websocket/src/protocols.js'; +it('preserves the pairing key across transport changes and isolates broker identities', async () => { + expect.assertions(4); + const store = createIndexedDbPairingStore({ databaseName: `bridge-migration-${crypto.randomUUID()}` }); + const key = await crypto.subtle.importKey('raw', crypto.getRandomValues(new Uint8Array(32)), 'HKDF', false, ['deriveKey']); + const pairing = { brokerId: 'original-broker', agentId: 'provider-installation', credentialId: crypto.randomUUID(), endpoint: 'ws://127.0.0.1:43210/provider', key }; + await store.save(pairing); + try { + expect(await store.load('http://127.0.0.1:43211/')).toBeUndefined(); + expect(await store.findByIdentity!('another-broker', pairing.agentId)).toBeUndefined(); + expect(await store.findByIdentity!(pairing.brokerId, 'another-installation')).toBeUndefined(); + expect(await store.findByIdentity!(pairing.brokerId, pairing.agentId)).toMatchObject({ ...pairing, key: { extractable: false, algorithm: { name: 'HKDF' } } }); + } finally { + await store.remove(pairing.credentialId); + } +}); + async function waitForAgentMessage( connection: { onMessage: (listener: (message: BrokerToAgentMessage) => void) => () => void }, ): Promise { diff --git a/tests/e2e/README.md b/tests/e2e/README.md index a130834..fe16046 100644 --- a/tests/e2e/README.md +++ b/tests/e2e/README.md @@ -47,3 +47,16 @@ The [30-iteration report](measurements/native-normal-chromium-151.json) records The [preserved MCP baseline](measurements/mcp-baseline-811909f.json), revision `811909f17b83fda8ca198acd0cb96573cf205ab1`, exposed 371,342 catalogue characters (approximately 92,836 tokens). Its snapshot omitted the deep controls and its warmup workflow failed; there is no successful baseline action distribution to compare. The current catalogue is 82.3% smaller by serialized character count. These reports preserve measured values, not portable performance guarantees. These isolated Chromium results do not establish behavior in a user's main Chrome profile. That requires the same public workflow with the extension installed and the intended tab/window scope approved in that profile. + + +### Devframe workflow measurements + +Run the same public closed-shadow, three-level cross-origin fixture over the shared Devframe RPC provider connection: + +```sh +CDB_DEVFRAME_PERFORMANCE_GATE=1 CDB_DEVFRAME_BENCHMARK_OUTPUT=/tmp/cdb-devframe.json pnpm exec vitest run --project extension-e2e tests/e2e/devframe-native.test.ts +``` + +The default is 30 warm samples each for fill, click, compact snapshot, fill/click batch with observation, and a persistent parent overlay. `CDB_DEVFRAME_BENCHMARK_SAMPLES` accepts 5–50 for short diagnostic probes or longer runs. Reports separate blocked actions from successful actions, include individual workflow p95, catalogue/argument/response character counts, and collect the optional `cdb.mcp.action` phase diagnostics. Characters divided by four is only a token estimate. This benchmark uses exact role/name locators; the WebSocket benchmark's default fresh references measure a different discovery cost and cannot isolate transport differences. + +Both harnesses record starting and ending OS load averages. The WebSocket report separates individual command round trips, direct `chrome.debugger.sendCommand` durations, and artifact HTTP reads. Chrome durations include browser scheduling and CDP execution; subtracting them from command round trips also includes provider/broker serialization and processing, so the difference is not network latency alone. `CDB_DEVFRAME_PERFORMANCE_GATE=1` enforces the same 500 ms action and 1,000 ms snapshot p95 budgets after saving the complete report. diff --git a/tests/e2e/devframe-native.test.ts b/tests/e2e/devframe-native.test.ts new file mode 100644 index 0000000..1eb21dd --- /dev/null +++ b/tests/e2e/devframe-native.test.ts @@ -0,0 +1,200 @@ +import type { CdbDevframeService } from '@dvcol/cdb-devframe'; +import type { CallToolResult } from '@modelcontextprotocol/server'; + +import { mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { createServer } from 'node:http'; +import { tmpdir } from 'node:os'; +import { join, resolve } from 'node:path'; + +import { createCdbPanel, createCdbService, getCdbService } from '@dvcol/cdb-devframe'; +import { Client } from '@modelcontextprotocol/client'; +import { InMemoryTransport, Server } from '@modelcontextprotocol/server'; +import { defineDevframe } from 'devframe'; +import { createDevServer } from 'devframe/adapters/dev'; +import { createWsOriginRegistry } from 'devframe/rpc/transports/ws-server'; +import { chromium } from 'playwright'; +import { build } from 'vite'; +import { expect, it } from 'vitest'; + +import { deepDomPage } from './fixtures/deep-dom-page.js'; +import { benchmarkAgentWorkflow } from './fixtures/workflow-benchmark.js'; + +interface FixtureWorker { + startDevframeProvider: (baseURL: string) => Promise; + approveDevframeRequest: (requestId: string, scope?: 'tab' | 'group') => Promise; + changeFixtureMembership: (url: string, joined: boolean) => Promise; + disconnectDevframeProvider: () => void; + stopDevframeProvider: () => Promise; +} + +it('runs native MCP actions through Chrome extension and shared Devframe RPC across closed shadows and cross-origin frames', async () => { + expect.assertions(27); + const cleanups: (() => Promise)[] = []; + try { + const pageServer = createServer((request, response) => { + response.setHeader('content-type', 'text/html; charset=utf-8'); + response.end(deepDomPage(new URL(request.url ?? '/', `http://${request.headers.host}`))); + }); + await new Promise(resolve => pageServer.listen(0, '127.0.0.1', resolve)); + cleanups.push(async () => new Promise((resolve, reject) => pageServer.close(error => error === undefined ? resolve() : reject(error)))); + const address = pageServer.address(); + if (address === null || typeof address === 'string') throw new Error('The fixture server has no port.'); + let service: CdbDevframeService | undefined; + const allowedOrigins = createWsOriginRegistry(); + const panel = createCdbPanel({ client() { + const broker = service!.broker; + return { ...broker, watch(listener) { + listener(broker.snapshot()); + return broker.subscribe(listener); + } }; + } }); + const devframe = await createDevServer(defineDevframe({ + ...panel.definition, + services: [createCdbService()], + async setup(context) { + service = getCdbService(context); + await panel.definition.setup(context); + }, + }), { + host: '127.0.0.1', + port: 0, + openBrowser: false, + auth: false, + mcp: false, + allowedOrigins, + onPeerConnect: (connection, session) => service!.onPeerConnect(connection, session), + onPeerDisconnect: (connection) => { + void service!.onPeerDisconnect(connection); + }, + }); + cleanups.push(async () => { + panel.dispose(); + await devframe.close(); + await service?.dispose(); + }); + const broker = service!.broker; + const directory = await mkdtemp(join(tmpdir(), 'cdb-devframe-chromium-')); + cleanups.push(async () => rm(directory, { recursive: true, force: true })); + const extensionDirectory = join(directory, 'extension'); + await build({ configFile: false, logLevel: 'silent', build: { emptyOutDir: true, outDir: extensionDirectory, lib: { entry: resolve('tests/e2e/fixtures/devframe-service-worker.ts'), fileName: () => 'worker.js', formats: ['es'] } } }); + await writeFile(join(extensionDirectory, 'manifest.json'), JSON.stringify({ manifest_version: 3, name: 'Public CDB Devframe fixture', version: '1.0.0', background: { service_worker: 'worker.js', type: 'module' }, permissions: ['debugger', 'tabs', 'tabGroups', 'webNavigation', 'storage', 'alarms'], host_permissions: ['http://*/*'] })); + const browser = await chromium.launchPersistentContext(join(directory, 'profile'), { channel: 'chromium', headless: true, viewport: { width: 1280, height: 900 }, args: [`--disable-extensions-except=${extensionDirectory}`, `--load-extension=${extensionDirectory}`, '--site-per-process', '--host-resolver-rules=MAP *.test 127.0.0.1', '--no-proxy-server'] }); + cleanups.push(async () => browser.close()); + const page = await browser.newPage(); + await page.goto(`http://cdb-root.test:${address.port}/?profile=normal&shadow=closed&frames=cross-origin`); + const leaf = page.frames().find(frame => new URL(frame.url()).searchParams.get('frame') === '3'); + if (leaf === undefined) throw new Error('The nested fixture frame did not load.'); + await leaf.waitForSelector('html[data-fixture-ready="true"]'); + const worker = browser.serviceWorkers()[0] ?? await browser.waitForEvent('serviceworker'); + const workerUrl = new URL(worker.url()); + const registrationUrl = new URL(`http://127.0.0.1:${devframe.port}/__connection.json`); + registrationUrl.searchParams.set('devframe_viewer_origin', `${workerUrl.protocol}//${workerUrl.host}`); + registrationUrl.searchParams.set('devframe_viewer_origin_token', allowedOrigins.token); + allowedOrigins.registerFromUrl(registrationUrl.href); + await worker.evaluate(async baseURL => (globalThis as unknown as FixtureWorker).startDevframeProvider(baseURL), `http://127.0.0.1:${devframe.port}/`); + await expect.poll(() => broker.snapshot().providers.some(provider => provider.state === 'ready')).toBe(true); + const management = await browser.newPage(); + const pageErrors: string[] = []; + management.on('pageerror', error => pageErrors.push(error.message)); + await management.goto(`http://127.0.0.1:${devframe.port}/`); + await management.getByRole('tab', { name: /Providers/ }).click(); + await expect.poll(async () => management.getByText('Public Chrome fixture', { exact: true }).count()).toBe(1).catch(async (error) => { + throw new Error(JSON.stringify({ pageErrors, panel: await management.locator('#app').textContent() }), { cause: error }); + }); + + const mcpServer = new Server({ name: 'public-cdb-agent', version: '1.0.0' }, { capabilities: { tools: {} } }); + const principal = { id: 'fixture-agent' }; + mcpServer.setRequestHandler('tools/list', async () => ({ tools: broker.tools.map(tool => ({ ...tool, inputSchema: { ...tool.inputSchema, type: 'object' as const } })) })); + mcpServer.setRequestHandler('tools/call', async (request, context): Promise => { + try { + const value = await broker.invoke(principal, request.params.name, request.params.arguments, { signal: context.mcpReq.signal }); + return { content: [{ type: 'text', text: typeof value === 'string' ? value : JSON.stringify(value) }] }; + } catch (error) { + return { isError: true, content: [{ type: 'text', text: JSON.stringify({ ...(error as object), message: error instanceof Error ? error.message : String(error) }) }] }; + } + }); + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + const agent = new Client({ name: 'Public regression agent', version: '1.0.0' }); + await mcpServer.connect(serverTransport); + await agent.connect(clientTransport); + cleanups.push(async () => { + await agent.close(); + await mcpServer.close(); + }); + const requested = agent.callTool({ name: 'browser.request_access', arguments: { level: 'interact' } }); + await expect.poll(() => broker.snapshot().requests.length).toBe(1); + await worker.evaluate(async requestId => (globalThis as unknown as FixtureWorker).approveDevframeRequest(requestId), broker.snapshot().requests[0]!.id); + const access = await requested; + expect(access.isError).toBeUndefined(); + await benchmarkAgentWorkflow(agent, browser.browser()?.version() ?? 'unavailable', page); + await page.locator('#toggle-overlay').click(); + const blockedStarted = performance.now(); + const blocked = await agent.callTool({ name: 'browser.click', arguments: { targetRef: 't1', locator: { role: 'button', name: { match: 'exact', value: 'Save deep value' } } } }); + expect(JSON.stringify(blocked)).toContain('MCP_ELEMENT_COVERED'); + expect(performance.now() - blockedStarted).toBeLessThan(3_000); + await page.evaluate(() => { + setTimeout(() => document.getElementById('parent-overlay')?.remove(), 2_500); + }); + const delayedStarted = performance.now(); + const delayed = await agent.callTool({ name: 'browser.click', arguments: { targetRef: 't1', locator: { role: 'button', name: { match: 'exact', value: 'Save deep value' } }, timeoutMilliseconds: 4_000 } }); + expect(delayed.isError, JSON.stringify(delayed)).toBeUndefined(); + expect(performance.now() - delayedStarted).toBeGreaterThanOrEqual(2_000); + const result = await agent.callTool({ name: 'browser.batch', arguments: { + actionTimeoutMilliseconds: 10_000, + targetRef: 't1', + observe: true, + actions: [ + { action: 'fill', locator: { role: 'textbox', name: { match: 'exact', value: 'Deep value' } }, text: 'Shared RPC proof' }, + { action: 'click', locator: { role: 'button', name: { match: 'exact', value: 'Save deep value' } } }, + ], + } }); + expect(result.isError, JSON.stringify(result)).toBeUndefined(); + await expect.poll(async () => page.getByRole('status').textContent()).toBe('Saved: Shared RPC proof'); + const initialScope = broker.snapshot().scopes[0]!.id; + const initialOrigin = new URL(page.url()).origin; + const followPeer = { id: 'follow-tab-agent' }; + const additionalAccess = broker.invoke(followPeer, 'browser.request_access', { level: 'interact', navigation: 'follow-tab' }); + await expect.poll(() => broker.snapshot().requests.length).toBe(1); + await worker.evaluate(async requestId => (globalThis as unknown as FixtureWorker).approveDevframeRequest(requestId, 'group'), broker.snapshot().requests[0]!.id); + await additionalAccess; + expect(new Set(broker.snapshot().grants.map(grant => grant.targetId)).size).toBe(1); + const joining = await browser.newPage(); + await joining.goto(`http://cdb-root.test:${address.port}/?profile=normal`); + await worker.evaluate(async url => (globalThis as unknown as FixtureWorker).changeFixtureMembership(url, true), joining.url()); + await expect.poll(() => broker.snapshot().grants.length).toBe(3); + expect(await broker.invoke(followPeer, 'browser.list_targets', {})).toHaveLength(2); + await worker.evaluate(async url => (globalThis as unknown as FixtureWorker).changeFixtureMembership(url, false), joining.url()); + await expect.poll(() => broker.snapshot().grants.length).toBe(2); + await joining.close(); + const previousGeneration = broker.snapshot().grants[0]!.targetGeneration; + await worker.evaluate(() => (globalThis as unknown as FixtureWorker).disconnectDevframeProvider()); + await expect.poll(() => broker.snapshot().grants.length === 2 && broker.snapshot().grants.every(grant => grant.state === 'active' && grant.targetGeneration > previousGeneration)).toBe(true); + expect(await broker.invoke(followPeer, 'browser.list_targets', {})).toMatchObject([{ targetRef: 't1' }]); + await page.goto(`http://cdb-other.test:${address.port}/?profile=normal&shadow=closed&frames=cross-origin`); + await expect.poll(() => broker.snapshot().grants.map(grant => grant.state).sort()).toEqual(['active', 'out-of-scope']); + expect(broker.snapshot().grants.map(grant => grant.approvedOrigin)).toEqual([initialOrigin, initialOrigin]); + const denied = await agent.callTool({ name: 'browser.snapshot', arguments: { targetRef: 't1' } }); + expect(JSON.stringify(denied)).toContain('TARGET_OUT_OF_SCOPE'); + expect(await broker.invoke(followPeer, 'browser.list_targets', {})).toMatchObject([{ targetRef: 't1' }]); + await broker.revokeScope(initialScope); + await expect.poll(() => broker.snapshot().grants.length).toBe(1); + const continued = await broker.invoke(followPeer, 'browser.batch', { + actionTimeoutMilliseconds: 10_000, + targetRef: 't1', + actions: [ + { action: 'fill', locator: { role: 'textbox', name: { match: 'exact', value: 'Deep value' } }, text: 'Independent approval survived' }, + { action: 'click', locator: { role: 'button', name: { match: 'exact', value: 'Save deep value' } } }, + ], + }); + expect(continued).toMatchObject({ completed: [{ index: 0, action: 'fill' }, { index: 1, action: 'click' }] }); + await expect.poll(async () => page.getByRole('status').textContent()).toBe('Saved: Independent approval survived'); + await management.getByRole('tab', { name: /Access/ }).click(); + await management.getByRole('button', { name: 'Revoke target', exact: true }).click(); + await expect.poll(() => broker.snapshot().grants).toEqual([]); + await expect.poll(async () => management.getByText('No shared tabs. Approve an agent request in the browser to grant access.', { exact: true }).count()).toBe(1); + await worker.evaluate(async () => (globalThis as unknown as FixtureWorker).stopDevframeProvider()); + expect(broker.snapshot().grants).toEqual([]); + } finally { + for (const cleanup of cleanups.reverse()) await cleanup(); + } +}, process.env.CDB_DEVFRAME_BENCHMARK_OUTPUT === undefined ? 90_000 : 240_000); diff --git a/tests/e2e/fixtures/devframe-service-worker.ts b/tests/e2e/fixtures/devframe-service-worker.ts new file mode 100644 index 0000000..9f0c3ec --- /dev/null +++ b/tests/e2e/fixtures/devframe-service-worker.ts @@ -0,0 +1,53 @@ +import { createCdbClient } from '@dvcol/cdb-devframe/client'; +import { createIndexedDbPairingStore } from '@dvcol/cdb-extension'; +import { createChromeProvider, getChromeProviderIdentity } from '@dvcol/cdb-extension/chrome'; +import { connectDevframe } from 'devframe/client'; + +let provider: ReturnType> | undefined; +let connection: Awaited> | undefined; +let client: ReturnType | undefined; +let approvedGroupId: number | undefined; + +Object.assign(globalThis, { + async startDevframeProvider(baseURL: string) { + const registration = { id: 'public-chrome-provider', instanceId: await getChromeProviderIdentity('public-cdb-fixture'), name: 'Public Chrome fixture', version: '1.0.0', maximumLevel: 'debug' as const }; + provider = createChromeProvider({ + maximumLevel: registration.maximumLevel, + async connect() { + if (connection === undefined || connection.status !== 'connected') { + connection = await connectDevframe({ baseURL, simpleAuth: false }); + await connection.ensureTrusted(); + client = createCdbClient(connection); + } + return client!.connectProvider({ registration, pairingKey: baseURL, pairingStore: createIndexedDbPairingStore(), confirmPairing: () => true }); + }, + authorizeApproval: (_request, _selector, trusted: boolean) => trusted, + recoveryStorageKey: 'public-cdb-recovery', + }); + provider.start(); + }, + async approveDevframeRequest(requestId: string, scope: 'tab' | 'group' = 'tab') { + const tabs = await chrome.tabs.query({}); + const selected = tabs.find(tab => tab.url?.startsWith('http://cdb-root.test:')); + if (selected?.id === undefined || provider === undefined) throw new Error('The public fixture tab is unavailable.'); + if (scope === 'group') { + approvedGroupId = await chrome.tabs.group({ tabIds: [selected.id] }); + return provider.approve(requestId, { kind: 'group', groupId: approvedGroupId }, true); + } + return provider.approve(requestId, { kind: 'explicit-tabs', tabIds: [selected.id] }, true); + }, + async changeFixtureMembership(url: string, joined: boolean) { + const tab = (await chrome.tabs.query({})).find(candidate => candidate.url === url); + if (tab?.id === undefined || approvedGroupId === undefined) throw new Error('The fixture group or tab is unavailable.'); + if (joined) await chrome.tabs.group({ tabIds: [tab.id], groupId: approvedGroupId }); + else await chrome.tabs.ungroup(tab.id); + }, + disconnectDevframeProvider() { + client?.disconnected(); + connection?.close?.(); + connection = undefined; + }, + async stopDevframeProvider() { + await provider?.dispose(); + }, +}); diff --git a/tests/e2e/fixtures/workflow-benchmark.ts b/tests/e2e/fixtures/workflow-benchmark.ts new file mode 100644 index 0000000..f2b2d45 --- /dev/null +++ b/tests/e2e/fixtures/workflow-benchmark.ts @@ -0,0 +1,64 @@ +import type { Client } from '@modelcontextprotocol/client'; +import type { Page } from 'playwright'; + +import { channel } from 'node:diagnostics_channel'; +import { writeFile } from 'node:fs/promises'; +import { cpus, loadavg } from 'node:os'; +import { styleText } from 'node:util'; + +/** Measures the same public workflow over the embedding test's real provider transport. */ +export async function benchmarkAgentWorkflow(agent: Client, chromiumVersion: string, page: Page): Promise { + if (process.env.CDB_DEVFRAME_BENCHMARK_OUTPUT === undefined) return; + const samples = Number(process.env.CDB_DEVFRAME_BENCHMARK_SAMPLES ?? 30); + const initialLoad = loadavg(); + if (!Number.isInteger(samples) || samples < 5 || samples > 50) throw new Error('Benchmark samples must be between 5 and 50.'); + const actionMetrics: unknown[] = []; + const diagnostics = channel('cdb.mcp.action'); + const collect = (measurement: unknown): void => { + actionMetrics.push(measurement); + }; + diagnostics.subscribe(collect); + try { + const measurements: { name: string; milliseconds: number; argumentsCharacters: number; responseCharacters: number }[] = []; + const locator = (role: string, value: string): { role: string; name: { match: 'exact'; value: string } } => ({ role, name: { match: 'exact', value } }); + const fill = { targetRef: 't1', locator: locator('textbox', 'Deep value'), text: 'Benchmark value' }; + const click = { targetRef: 't1', locator: locator('button', 'Save deep value') }; + const batch = { targetRef: 't1', observe: true, actions: [{ action: 'fill', ...fill }, { action: 'click', ...click }].map(({ targetRef: _targetRef, ...action }) => action) }; + async function call(name: string, argumentsValue: Record, record = true, blocked = false): Promise { + const started = performance.now(); + const result = await agent.callTool({ name, arguments: argumentsValue }); + if (blocked ? !JSON.stringify(result).includes('MCP_ELEMENT_COVERED') : result.isError) throw new Error(`Benchmark ${name} failed: ${JSON.stringify(result)}`); + if (record) measurements.push({ name: blocked ? `${name}:blocked` : name, milliseconds: performance.now() - started, argumentsCharacters: JSON.stringify(argumentsValue).length, responseCharacters: JSON.stringify(result).length }); + } + await call('browser.fill', fill, false); + await call('browser.click', click, false); + await call('browser.batch', batch, false); + for (let sample = 0; sample < samples; sample += 1) { + await call('browser.fill', fill); + await call('browser.click', click); + await call('browser.snapshot', { targetRef: 't1' }); + await call('browser.batch', batch); + } + await page.locator('#toggle-overlay').click(); + try { + for (let sample = 0; sample < samples; sample += 1) await call('browser.click', click, true, true); + } finally { + await page.locator('#parent-overlay').evaluate(element => element.remove()); + } + const workflows = Array.from({ length: samples }, (_unused, index) => measurements.slice(index * 4, index * 4 + 3).reduce((total, measurement) => total + measurement.milliseconds, 0)); + const percentile95 = (values: number[]): number | undefined => values.sort((left, right) => left - right)[Math.ceil(values.length * 0.95) - 1]; + const summaries = Object.fromEntries(['browser.fill', 'browser.click', 'browser.snapshot', 'browser.batch', 'browser.click:blocked'].map(name => [name, { p95Milliseconds: percentile95(measurements.filter(measurement => measurement.name === name).map(measurement => measurement.milliseconds)) }])); + + const catalogue = await agent.listTools(); + const report = { environment: { chromiumVersion, node: process.version, architecture: process.arch, operatingSystem: process.platform, processor: cpus()[0]?.model, initialLoad, finalLoad: loadavg() }, transport: 'MCP → broker → authenticated Devframe RPC → Chrome extension → chrome.debugger', fixture: 'normal, closed shadow roots, three cross-origin frame levels', samples, individualWorkflowP95Milliseconds: percentile95(workflows), summaries, measurements, actionMetrics, catalogueCharacters: JSON.stringify(catalogue).length, tokenizerEstimate: 'Serialized JSON characters / 4; not measured tokenizer usage' }; + await writeFile(process.env.CDB_DEVFRAME_BENCHMARK_OUTPUT, `${JSON.stringify(report, null, 2)}\n`); + console.info(styleText('cyan', '📊 [devframe-benchmark]'), summaries); + if (process.env.CDB_DEVFRAME_PERFORMANCE_GATE === '1') { + for (const [name, maximumMilliseconds] of [['browser.fill', 500], ['browser.click', 500], ['browser.snapshot', 1_000]] as const) { + if ((summaries[name]?.p95Milliseconds ?? Infinity) > maximumMilliseconds) throw new Error(`${name} p95 exceeded ${maximumMilliseconds} ms.`); + } + } + } finally { + diagnostics.unsubscribe(collect); + } +} diff --git a/tests/e2e/native-agent-benchmark.test.ts b/tests/e2e/native-agent-benchmark.test.ts index 082295f..21942c3 100644 --- a/tests/e2e/native-agent-benchmark.test.ts +++ b/tests/e2e/native-agent-benchmark.test.ts @@ -1,7 +1,7 @@ import type { NativeMcpHarness } from './fixtures/native-mcp-harness.js'; import { writeFile } from 'node:fs/promises'; -import { cpus } from 'node:os'; +import { cpus, loadavg } from 'node:os'; import { styleText } from 'node:util'; import { afterEach, expect, it } from 'vitest'; @@ -12,6 +12,8 @@ import { createNativeMcpHarness, toolText } from './fixtures/native-mcp-harness. interface CallMeasurement { readonly argumentCharacters: number; readonly artifactBytes: number; + readonly artifactReadMilliseconds: number; + readonly commands: readonly { readonly method: string; readonly durationMilliseconds: number }[]; readonly commandCount: number; readonly commandRequestCharacters: number; readonly commandResponseCharacters: number; @@ -40,18 +42,38 @@ it.runIf(process.env.CDB_NATIVE_BENCHMARK === '1')('measures the public native a const mcpModule = process.env.CDB_MCP_BASELINE_MODULE; harness = await createNativeMcpHarness({ ...(mcpModule === undefined ? {} : { mcpModule }), profile }); const activeHarness = harness; + const worker = activeHarness.context.serviceWorkers()[0]!; + await worker.evaluate(() => { + const workerGlobal = globalThis as unknown as { + chrome: { debugger: { sendCommand: (...parameters: unknown[]) => Promise } }; + debuggerCommandMeasurements: Array<{ readonly method: string; readonly durationMilliseconds: number }>; + }; + workerGlobal.debuggerCommandMeasurements = []; + const sendCommand = workerGlobal.chrome.debugger.sendCommand.bind(workerGlobal.chrome.debugger); + workerGlobal.chrome.debugger.sendCommand = async (...parameters) => { + const startedAt = performance.now(); + const result = await sendCommand(...parameters); + workerGlobal.debuggerCommandMeasurements.push({ method: String(parameters[1]), durationMilliseconds: performance.now() - startedAt }); + return result; + }; + }); const measurements: CallMeasurement[] = []; - const commandMeasurements: Array<{ readonly method: string; readonly requestCharacters: number; readonly responseCharacters: number }> = []; + const initialLoad = loadavg(); + const commandMeasurements: Array<{ readonly method: string; readonly durationMilliseconds: number; readonly requestCharacters: number; readonly responseCharacters: number }> = []; let artifactBytes = 0; + let artifactReadMilliseconds = 0; const executeCommand = activeHarness.client.executeCommand.bind(activeHarness.client); activeHarness.client.executeCommand = async (command) => { + const startedAt = performance.now(); const result = await executeCommand(command); - commandMeasurements.push({ method: command.method, requestCharacters: JSON.stringify(command).length, responseCharacters: JSON.stringify(result).length }); + commandMeasurements.push({ method: command.method, durationMilliseconds: performance.now() - startedAt, requestCharacters: JSON.stringify(command).length, responseCharacters: JSON.stringify(result).length }); return result; }; const readArtifact = activeHarness.client.readArtifact.bind(activeHarness.client); activeHarness.client.readArtifact = async (request, signal) => { + const startedAt = performance.now(); const result = await readArtifact(request, signal); + artifactReadMilliseconds += performance.now() - startedAt; artifactBytes += result.byteLength; return result; }; @@ -62,10 +84,13 @@ it.runIf(process.env.CDB_NATIVE_BENCHMARK === '1')('measures the public native a const startedAt = performance.now(); const commandStart = commandMeasurements.length; const artifactStart = artifactBytes; + const artifactReadStart = artifactReadMilliseconds; const result = await activeHarness.mcpClient.callTool({ arguments: toolArguments, name }); if (include) measurements.push({ argumentCharacters: JSON.stringify(toolArguments).length, artifactBytes: artifactBytes - artifactStart, + artifactReadMilliseconds: artifactReadMilliseconds - artifactReadStart, + commands: commandMeasurements.slice(commandStart).map(({ method, durationMilliseconds }) => ({ method, durationMilliseconds })), commandCount: commandMeasurements.length - commandStart, commandRequestCharacters: commandMeasurements.slice(commandStart).reduce((total, command) => total + command.requestCharacters, 0), commandResponseCharacters: commandMeasurements.slice(commandStart).reduce((total, command) => total + command.responseCharacters, 0), @@ -111,6 +136,7 @@ it.runIf(process.env.CDB_NATIVE_BENCHMARK === '1')('measures the public native a const snapshot95 = snapshots.length === 0 ? null : percentile95(snapshots.map(measurement => measurement.durationMilliseconds)); const lastCompletedWorkflow = measurements.flatMap((measurement, index) => measurement.name === 'browser.click' && !measurement.error && index >= 2 ? [measurements.slice(index - 2, index + 1)] : []).at(-1); const workflowTokenEstimate = lastCompletedWorkflow === undefined ? null : Math.ceil(lastCompletedWorkflow.reduce((total, measurement) => total + measurement.argumentCharacters + measurement.responseCharacters, 0) / 4); + const debuggerCommandMeasurements = await worker.evaluate(() => (globalThis as typeof globalThis & { debuggerCommandMeasurements: Array<{ readonly method: string; readonly durationMilliseconds: number }> }).debuggerCommandMeasurements); await activeHarness.close(); harness = await createNativeMcpHarness({ profile, shadow: 'open' }); const referenceDurations: number[] = []; @@ -130,13 +156,14 @@ it.runIf(process.env.CDB_NATIVE_BENCHMARK === '1')('measures the public native a const report = { budgets: { nativeActionP95Milliseconds: 500, nativeSnapshotP95Milliseconds: 1_000 }, chromiumVersion, - environment: { architecture: process.arch, nodeVersion: process.version, operatingSystem: process.platform, processorModel: cpus()[0]?.model, processors: cpus().length }, + environment: { architecture: process.arch, nodeVersion: process.version, operatingSystem: process.platform, processorModel: cpus()[0]?.model, processors: cpus().length, initialLoad, finalLoad: loadavg() }, fixture: deepDomProfiles[profile], measuredAt: new Date().toISOString(), modelExcluded: true, native: { actionP95Milliseconds: action95, errors: measurements.filter(measurement => measurement.error).length, + debuggerCommandMeasurements, initialSnapshotIncludesDeepControls: toolText(initialSnapshot).includes('Save deep value'), initialSnapshotDurationMilliseconds, measurements, diff --git a/tests/e2e/native-agent-loop.test.ts b/tests/e2e/native-agent-loop.test.ts index 4d63a1c..ccae839 100644 --- a/tests/e2e/native-agent-loop.test.ts +++ b/tests/e2e/native-agent-loop.test.ts @@ -98,6 +98,7 @@ it.each(['css', 'accessible-name'] as const)('batches native $0 fill and click w { action: 'click', locator: strategy === 'css' ? { css: 'button#deep-save', frameChain } : { role: 'button', name: { match: 'exact', value: 'Save deep value' } } }, ], observe: true, + actionTimeoutMilliseconds: 10_000, targetRef: harness.targetRef, }, name: 'browser.batch' }); expect(result.isError, toolText(result)).toBeUndefined(); @@ -117,7 +118,8 @@ it('stops an active native batch when its target is revoked', async () => { { action: 'click', locator: { css: 'button#toggle-overlay' } }, ], targetRef: harness.targetRef, - timeoutMilliseconds: 5_000, + timeoutMilliseconds: 15_000, + actionTimeoutMilliseconds: 10_000, }, name: 'browser.batch' }); await harness.page.getByRole('button', { name: 'Save replacement', exact: true }).waitFor({ state: 'attached' }); await harness.revoke(); @@ -137,7 +139,8 @@ it('stops a native batch when a click replaces the root document', async () => { { action: 'click', locator: { css: 'button#prepare-replacement' } }, ], targetRef: harness.targetRef, - timeoutMilliseconds: 5_000, + timeoutMilliseconds: 15_000, + actionTimeoutMilliseconds: 10_000, }, name: 'browser.batch' }); await harness.page.waitForURL(url => url.searchParams.get('revision') === '1'); const output = JSON.parse(toolText(result)) as { readonly details: { readonly completed: readonly { readonly index: number }[] } }; @@ -172,9 +175,9 @@ it.each([ const snapshot = await harness.mcpClient.callTool({ arguments: { targetRef: harness.targetRef }, name: 'browser.snapshot' }); const elementReference = (action === 'click' ? /button "Save deep value"[^\n]*\[ref=(e\d+)\]/u : /textbox "Deep value"[^\n]*\[ref=(e\d+)\]/u).exec(toolText(snapshot))?.[1]; if (elementReference === undefined) throw new Error('The nested control has no snapshot reference.'); - const cover = await harness.mcpClient.callTool({ arguments: { locator: { css: 'button#toggle-overlay' }, targetRef: harness.targetRef }, name: 'browser.click' }); + const cover = await harness.mcpClient.callTool({ arguments: { locator: { css: 'button#toggle-overlay' }, targetRef: harness.targetRef, timeoutMilliseconds: 10_000 }, name: 'browser.click' }); if (cover.isError) throw new Error(toolText(cover)); - const actionResult = await harness.mcpClient.callTool({ arguments: { ref: elementReference, targetRef: harness.targetRef, timeoutMilliseconds: 300, ...(action === 'fill' ? { text: 'Must remain blocked' } : {}) }, name: `browser.${action}` }); + const actionResult = await harness.mcpClient.callTool({ arguments: { ref: elementReference, targetRef: harness.targetRef, timeoutMilliseconds: 2_000, ...(action === 'fill' ? { text: 'Must remain blocked' } : {}) }, name: `browser.${action}` }); expect(actionResult.isError, toolText(actionResult)).toBe(true); expect(toolText(actionResult)).toContain('MCP_ELEMENT_COVERED'); expect(await harness.page.getByRole('status').textContent()).toBe('Ready'); @@ -183,7 +186,7 @@ it.each([ it('re-resolves a locator when scrolling replaces its element before input', async () => { expect.assertions(2); harness = await createNativeMcpHarness(); - const prepare = await harness.mcpClient.callTool({ arguments: { locator: { css: 'button#prepare-replacement' }, targetRef: harness.targetRef }, name: 'browser.click' }); + const prepare = await harness.mcpClient.callTool({ arguments: { locator: { css: 'button#prepare-replacement' }, targetRef: harness.targetRef, timeoutMilliseconds: 10_000 }, name: 'browser.click' }); if (prepare.isError) throw new Error(toolText(prepare)); const click = await harness.mcpClient.callTool({ arguments: { locator: { name: { match: 'exact', value: 'Save replacement' }, role: 'button' }, diff --git a/tsconfig.json b/tsconfig.json index 52f0154..bc59d9a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,6 +2,8 @@ "references": [ { "path": "./packages/automation-playwright" }, { "path": "./packages/core" }, + { "path": "./packages/broker" }, + { "path": "./packages/devframe" }, { "path": "./packages/extension" }, { "path": "./packages/websocket" }, { "path": "./packages/birpc" }, diff --git a/tsconfig.scripts.json b/tsconfig.scripts.json index 91d3636..c8fad1c 100644 --- a/tsconfig.scripts.json +++ b/tsconfig.scripts.json @@ -17,6 +17,7 @@ "packages/*/test/**/*.ts", "packages/*/tsdown.config.ts", "packages/*/vitest.config.ts", + "packages/*/vite.config.ts", "tests/browser/**/*.ts", "tests/e2e/**/*.ts", "tests/fixtures/**/*.ts", From b37c05d9e21fd7f39ff3775e08d5c0d7e6bf322f Mon Sep 17 00:00:00 2001 From: dvcolomban Date: Wed, 9 Sep 2026 12:07:47 +0200 Subject: [PATCH 2/6] fix: preserve approval outcomes and reload milestones --- docs/benchmarks/2026-09-09-latency.csv | 241 ++++++++++++++++++ docs/benchmarks/2026-09-09-lifecycle.csv | 221 ++++++++++++++++ docs/benchmarks/2026-09-09-lifecycle.md | 52 ++++ packages/broker/src/runtime.ts | 9 +- packages/broker/test/runtime.test.ts | 27 +- packages/core/src/grant-request.ts | 16 +- packages/core/src/index.ts | 1 + packages/core/test/grant-request.test.ts | 27 ++ packages/devframe/README.md | 2 + packages/devframe/src/page-script.ts | 17 +- packages/devframe/src/panel-definition.ts | 4 +- packages/devframe/test/page-script.test.ts | 12 +- packages/mcp/src/index.ts | 27 +- packages/mcp/test/mvp-session.test.ts | 12 +- packages/mcp/test/semantic-controls.test.ts | 121 ++++++++- tests/e2e/devframe-native.test.ts | 6 +- tests/e2e/fixtures/devframe-service-worker.ts | 3 + tests/e2e/fixtures/lifecycle-diagnostics.ts | 119 +++++++++ 18 files changed, 873 insertions(+), 44 deletions(-) create mode 100644 docs/benchmarks/2026-09-09-latency.csv create mode 100644 docs/benchmarks/2026-09-09-lifecycle.csv create mode 100644 docs/benchmarks/2026-09-09-lifecycle.md create mode 100644 tests/e2e/fixtures/lifecycle-diagnostics.ts diff --git a/docs/benchmarks/2026-09-09-latency.csv b/docs/benchmarks/2026-09-09-latency.csv new file mode 100644 index 0000000..d5621d9 --- /dev/null +++ b/docs/benchmarks/2026-09-09-latency.csv @@ -0,0 +1,241 @@ +transport,sample,scenario,milliseconds,error +devframe,0,browser.fill,372.2828749999999, +devframe,1,browser.click,295.201583, +devframe,2,browser.snapshot,885.8209159999997, +devframe,3,browser.batch,1230.2917500000003, +devframe,4,browser.fill,372.884250000001, +devframe,5,browser.click,307.55574999999953, +devframe,6,browser.snapshot,749.7271669999991, +devframe,7,browser.batch,1150.9781660000008, +devframe,8,browser.fill,335.2848749999994, +devframe,9,browser.click,328.7793329999986, +devframe,10,browser.snapshot,726.2948749999996, +devframe,11,browser.batch,1396.0505840000005, +devframe,12,browser.fill,975.4526249999999, +devframe,13,browser.click,418.50783299999966, +devframe,14,browser.snapshot,925.5630830000009, +devframe,15,browser.batch,1475.1597909999982, +devframe,16,browser.fill,367.60662500000035, +devframe,17,browser.click,349.8762080000015, +devframe,18,browser.snapshot,725.6705000000002, +devframe,19,browser.batch,1821.4395409999997, +devframe,20,browser.fill,347.7748329999995, +devframe,21,browser.click,325.8550409999989, +devframe,22,browser.snapshot,705.8833340000019, +devframe,23,browser.batch,1311.808917000002, +devframe,24,browser.fill,330.0451249999969, +devframe,25,browser.click,320.3681250000009, +devframe,26,browser.snapshot,1102.395958000001, +devframe,27,browser.batch,1104.0686249999999, +devframe,28,browser.fill,314.4805000000015, +devframe,29,browser.click,306.6389589999999, +devframe,30,browser.snapshot,654.0953330000011, +devframe,31,browser.batch,1479.8669169999994, +devframe,32,browser.fill,348.3762499999975, +devframe,33,browser.click,1130.6139169999988, +devframe,34,browser.snapshot,754.5785829999986, +devframe,35,browser.batch,1196.7371250000033, +devframe,36,browser.fill,372.015999999996, +devframe,37,browser.click,293.83283400000073, +devframe,38,browser.snapshot,624.9125420000055, +devframe,39,browser.batch,1326.222332999998, +devframe,40,browser.fill,296.06683399999747, +devframe,41,browser.click,340.12366699999984, +devframe,42,browser.snapshot,651.0039590000015, +devframe,43,browser.batch,1163.4714160000003, +devframe,44,browser.fill,296.01987499999814, +devframe,45,browser.click,494.3464160000003, +devframe,46,browser.snapshot,1852.451000000001, +devframe,47,browser.batch,1142.930290999997, +devframe,48,browser.fill,315.67149999999674, +devframe,49,browser.click,329.15550000000076, +devframe,50,browser.snapshot,829.6743750000023, +devframe,51,browser.batch,1127.732333, +devframe,52,browser.fill,327.34108299999934, +devframe,53,browser.click,351.3675409999996, +devframe,54,browser.snapshot,740.8344169999982, +devframe,55,browser.batch,1148.3505000000005, +devframe,56,browser.fill,325.45524999999907, +devframe,57,browser.click,316.7242499999993, +devframe,58,browser.snapshot,695.9982079999972, +devframe,59,browser.batch,1644.6100829999996, +devframe,60,browser.fill,273.2493749999994, +devframe,61,browser.click,299.5064169999969, +devframe,62,browser.snapshot,661.7077920000011, +devframe,63,browser.batch,1207.6912090000042, +devframe,64,browser.fill,310.24441600000137, +devframe,65,browser.click,285.47766599999886, +devframe,66,browser.snapshot,695.1679999999978, +devframe,67,browser.batch,2065.8635830000057, +devframe,68,browser.fill,332.5876250000001, +devframe,69,browser.click,330.72099999999773, +devframe,70,browser.snapshot,656.7946250000023, +devframe,71,browser.batch,1173.5899160000045, +devframe,72,browser.fill,307.02570900000137, +devframe,73,browser.click,362.74829200000386, +devframe,74,browser.snapshot,956.2126250000001, +devframe,75,browser.batch,3865.3280830000003, +devframe,76,browser.fill,1499.5701670000053, +devframe,77,browser.click,471.1782089999906, +devframe,78,browser.snapshot,2188.4195000000036, +devframe,79,browser.batch,1553.6526250000024, +devframe,80,browser.fill,341.908666000003, +devframe,81,browser.click,326.61516700001084, +devframe,82,browser.snapshot,976.8965000000026, +devframe,83,browser.batch,1538.1514999999927, +devframe,84,browser.fill,321.6036669999885, +devframe,85,browser.click,322.21016699999745, +devframe,86,browser.snapshot,1248.497000000003, +devframe,87,browser.batch,1306.725375000009, +devframe,88,browser.fill,361.6015000000043, +devframe,89,browser.click,606.4772499999963, +devframe,90,browser.snapshot,937.1247919999878, +devframe,91,browser.batch,1526.1002080000035, +devframe,92,browser.fill,300.5969580000092, +devframe,93,browser.click,317.8986670000013, +devframe,94,browser.snapshot,695.1366660000058, +devframe,95,browser.batch,1378.874874999994, +devframe,96,browser.fill,276.3425840000127, +devframe,97,browser.click,371.6782080000121, +devframe,98,browser.snapshot,2735.0215829999943, +devframe,99,browser.batch,1427.9537500000006, +devframe,100,browser.fill,296.02037500000733, +devframe,101,browser.click,291.3111249999929, +devframe,102,browser.snapshot,760.8910840000026, +devframe,103,browser.batch,2199.9709170000133, +devframe,104,browser.fill,316.20937499999127, +devframe,105,browser.click,599.6370840000018, +devframe,106,browser.snapshot,1292.0612919999985, +devframe,107,browser.batch,2219.3544589999947, +devframe,108,browser.fill,381.60958400000527, +devframe,109,browser.click,318.2050840000011, +devframe,110,browser.snapshot,649.8490829999937, +devframe,111,browser.batch,1110.6722080000036, +devframe,112,browser.fill,340.2784999999858, +devframe,113,browser.click,296.1295839999948, +devframe,114,browser.snapshot,637.4309999999969, +devframe,115,browser.batch,1215.7875420000055, +devframe,116,browser.fill,309.9054999999935, +devframe,117,browser.click,302.4016249999986, +devframe,118,browser.snapshot,668.7026249999908, +devframe,119,browser.batch,1498.0010000000038, +devframe,120,browser.click:blocked,2002.6224159999983, +devframe,121,browser.click:blocked,2006.345291000005, +devframe,122,browser.click:blocked,2001.1201250000013, +devframe,123,browser.click:blocked,2001.661540999994, +devframe,124,browser.click:blocked,2001.296000000002, +devframe,125,browser.click:blocked,2002.1808750000055, +devframe,126,browser.click:blocked,2003.1580000000104, +devframe,127,browser.click:blocked,2056.594333000001, +devframe,128,browser.click:blocked,2002.1652920000051, +devframe,129,browser.click:blocked,2000.7545419999951, +devframe,130,browser.click:blocked,2187.196292000008, +devframe,131,browser.click:blocked,2016.8204169999954, +devframe,132,browser.click:blocked,2184.8196670000034, +devframe,133,browser.click:blocked,2002.7432920000138, +devframe,134,browser.click:blocked,2003.2419579999987, +devframe,135,browser.click:blocked,2004.6015420000185, +devframe,136,browser.click:blocked,2003.3656669999764, +devframe,137,browser.click:blocked,2025.3184579999943, +devframe,138,browser.click:blocked,2283.9179170000134, +devframe,139,browser.click:blocked,2013.988000000012, +devframe,140,browser.click:blocked,2026.5623750000086, +devframe,141,browser.click:blocked,2004.4344169999822, +devframe,142,browser.click:blocked,2101.339000000007, +devframe,143,browser.click:blocked,2000.875, +devframe,144,browser.click:blocked,2002.302834000002, +devframe,145,browser.click:blocked,2002.0181250000023, +devframe,146,browser.click:blocked,2017.5597919999855, +devframe,147,browser.click:blocked,2290.174082999991, +devframe,148,browser.click:blocked,2001.0391250000102, +devframe,149,browser.click:blocked,2032.6541670000006, +websocket,0,browser.snapshot,1744.658374999999,False +websocket,1,browser.fill,347.6744579999995,False +websocket,2,browser.click,284.39687499999854,False +websocket,3,browser.snapshot,1965.347667,False +websocket,4,browser.fill,529.0651249999992,False +websocket,5,browser.click,432.63545799999883,False +websocket,6,browser.snapshot,1888.4782920000016,False +websocket,7,browser.fill,188.5514579999981,False +websocket,8,browser.click,169.6525419999998,False +websocket,9,browser.snapshot,1115.8272080000024,False +websocket,10,browser.fill,185.18408400000044,False +websocket,11,browser.click,163.2671249999985,False +websocket,12,browser.snapshot,1061.5348749999976,False +websocket,13,browser.fill,260.890707999999,False +websocket,14,browser.click,449.15708299999824,False +websocket,15,browser.snapshot,2315.700832999999,False +websocket,16,browser.fill,220.46716599999854,False +websocket,17,browser.click,151.10516700000153,False +websocket,18,browser.snapshot,926.9267499999987,False +websocket,19,browser.fill,148.13362499999857,False +websocket,20,browser.click,103.9340000000011,False +websocket,21,browser.snapshot,682.0189580000006,False +websocket,22,browser.fill,110.34120900000198,False +websocket,23,browser.click,102.79241700000057,False +websocket,24,browser.snapshot,678.1170409999977,False +websocket,25,browser.fill,107.73254199999792,False +websocket,26,browser.click,100.87183300000106,False +websocket,27,browser.snapshot,648.6391250000015,False +websocket,28,browser.fill,110.91125000000102,False +websocket,29,browser.click,102.1441670000022,False +websocket,30,browser.snapshot,715.8509159999994,False +websocket,31,browser.fill,126.86491599999863,False +websocket,32,browser.click,625.4940409999981,False +websocket,33,browser.snapshot,847.3919999999998,False +websocket,34,browser.fill,111.20866599999863,False +websocket,35,browser.click,348.1989169999979,False +websocket,36,browser.snapshot,675.336457999998,False +websocket,37,browser.fill,110.09754199999952,False +websocket,38,browser.click,101.74375000000146,False +websocket,39,browser.snapshot,698.0110000000022,False +websocket,40,browser.fill,121.95858299999963,False +websocket,41,browser.click,157.7307499999988,False +websocket,42,browser.snapshot,935.7615000000005,False +websocket,43,browser.fill,115.79779099999723,False +websocket,44,browser.click,100.42279100000087,False +websocket,45,browser.snapshot,697.3596660000039,False +websocket,46,browser.fill,118.68445799999608,False +websocket,47,browser.click,108.03749999999854,False +websocket,48,browser.snapshot,718.5740839999999,False +websocket,49,browser.fill,110.84404200000426,False +websocket,50,browser.click,122.67570800000249,False +websocket,51,browser.snapshot,713.7019579999978,False +websocket,52,browser.fill,126.12220800000068,False +websocket,53,browser.click,117.3492910000059,False +websocket,54,browser.snapshot,1216.9126660000038,False +websocket,55,browser.fill,140.5834590000013,False +websocket,56,browser.click,276.10608299999876,False +websocket,57,browser.snapshot,1332.8142090000038,False +websocket,58,browser.fill,107.97799999999552,False +websocket,59,browser.click,99.01779199999874,False +websocket,60,browser.snapshot,663.9896670000016,False +websocket,61,browser.fill,106.87354200000118,False +websocket,62,browser.click,99.30225000000064,False +websocket,63,browser.snapshot,670.1052909999999,False +websocket,64,browser.fill,168.42158400000335,False +websocket,65,browser.click,127.05783399999927,False +websocket,66,browser.snapshot,744.9471659999981,False +websocket,67,browser.fill,115.79125000000204,False +websocket,68,browser.click,115.71587500000169,False +websocket,69,browser.snapshot,725.476125000001,False +websocket,70,browser.fill,116.20245799999975,False +websocket,71,browser.click,104.46258300000045,False +websocket,72,browser.snapshot,669.9595830000035,False +websocket,73,browser.fill,109.82525000000169,False +websocket,74,browser.click,107.52516599999944,False +websocket,75,browser.snapshot,689.3825419999994,False +websocket,76,browser.fill,116.42087499999616,False +websocket,77,browser.click,125.02524999999878,False +websocket,78,browser.snapshot,730.819792000002,False +websocket,79,browser.fill,112.83883300000161,False +websocket,80,browser.click,114.01954199999454,False +websocket,81,browser.snapshot,670.2629579999993,False +websocket,82,browser.fill,107.11358299999847,False +websocket,83,browser.click,104.14087499999732,False +websocket,84,browser.snapshot,1126.5086670000019,False +websocket,85,browser.fill,126.3837910000002,False +websocket,86,browser.click,109.9945829999997,False +websocket,87,browser.snapshot,742.9184590000004,False +websocket,88,browser.fill,123.49979200000234,False +websocket,89,browser.click,109.56429199999548,False diff --git a/docs/benchmarks/2026-09-09-lifecycle.csv b/docs/benchmarks/2026-09-09-lifecycle.csv new file mode 100644 index 0000000..e344cec --- /dev/null +++ b/docs/benchmarks/2026-09-09-lifecycle.csv @@ -0,0 +1,221 @@ +run,cycle,scenario,milliseconds,error +1,0,navigation,529.8928749999995, +1,0,reconnect,570.7437090000003, +1,0,cancellation,503.53520799999933, +1,0,revocation,2032.4986669999998,"AssertionError [ERR_ASSERTION]: Expected values to be strictly deep-equal: ++ actual - expected + ++ [ ++ { ++ availability: 'available', ++ capabilities: { ++ level: 'interact' ++ }, ++ targetRef: 't1', ++ title: 'CDB public deep DOM fixture', ++ type: 'page', ++ url: 'http://cdb-root.test:56999/?profile=normal&shadow=closed&frames=cross-origin' ++ } ++ ] +- [] +" +2,0,navigation,809.4494999999988, +2,0,reconnect,597.996916, +2,0,cancellation,470.90104099999735, +2,0,revocation,657.044415999997, +2,1,navigation,584.5572920000013, +2,1,reconnect,607.9674169999998, +2,1,cancellation,575.3359999999993, +2,1,revocation,498.8061669999988, +2,2,navigation,582.8690419999984, +2,2,reconnect,633.9129579999972, +2,2,cancellation,446.27683400000024, +2,2,revocation,466.963416999999, +2,3,navigation,495.0878329999978, +2,3,reconnect,630.3752499999973, +2,3,cancellation,496.0032089999986, +2,3,revocation,464.6810829999995, +2,4,navigation,557.5289169999996, +2,4,reconnect,550.255000000001, +2,4,cancellation,504.8969159999979, +2,4,revocation,473.49566699999923, +2,5,navigation,524.9513340000012, +2,5,reconnect,594.1804579999989, +2,5,cancellation,538.2508750000015, +2,5,revocation,567.2530000000006, +2,6,navigation,619.6551660000005, +2,6,reconnect,658.3662919999988, +2,6,cancellation,421.5407500000001, +2,6,revocation,482.6445420000018, +2,7,navigation,498.9461670000019, +2,7,reconnect,562.4247920000016, +2,7,cancellation,488.70929199999955, +2,7,revocation,459.21687499999825, +2,8,navigation,771.9752500000031, +2,8,reconnect,982.1749589999963, +2,8,cancellation,480.18208400000003, +2,8,revocation,458.58899999999994, +2,9,navigation,555.5794580000002, +2,9,reconnect,584.6011249999938, +2,9,cancellation,835.5297499999942, +2,9,revocation,450.096749999997, +2,10,navigation,607.8306250000023, +2,10,reconnect,552.6204160000052, +2,10,cancellation,448.69941700000345, +2,10,revocation,476.57474999999977, +2,11,navigation,447.080833, +2,11,reconnect,578.8883750000023, +2,11,cancellation,418.4232499999998, +2,11,revocation,10147.949208000005,AssertionError [ERR_ASSERTION]: Lifecycle state did not settle within 10 seconds. +3,0,navigation,610.7144160000007, +3,0,reconnect,766.5124169999999, +3,0,cancellation,729.1168749999997, +3,0,revocation,578.8984170000003, +3,1,navigation,610.1754579999997, +3,1,reconnect,639.6232080000009, +3,1,cancellation,452.1203750000004, +3,1,revocation,472.42420800000036, +3,2,navigation,537.8957499999997, +3,2,reconnect,620.057667000001, +3,2,cancellation,504.69462499999645, +3,2,revocation,664.3996670000015, +3,3,navigation,763.1090409999997, +3,3,reconnect,821.0622919999987, +3,3,cancellation,685.4374160000007, +3,3,revocation,639.1363749999982, +3,4,navigation,1503.728415999998, +3,4,reconnect,1824.1104579999992, +3,4,cancellation,851.6594160000022, +3,4,revocation,512.9981249999983, +3,5,navigation,663.9302920000009, +3,5,reconnect,687.5470000000023, +3,5,cancellation,966.8957090000004, +3,5,revocation,850.4676250000011, +3,6,navigation,1081.8857080000016, +3,6,reconnect,904.3157919999976, +3,6,cancellation,907.979540999997, +3,6,revocation,1179.9177500000005, +3,7,navigation,1007.9408330000006, +3,7,reconnect,1948.1131670000032, +3,7,cancellation,856.7285829999964, +3,7,revocation,3706.669750000001,"AssertionError [ERR_ASSERTION]: {""content"":[{""type"":""text"",""text"":""{\""code\"":\""MCP_ACTION_TIMEOUT\"",\""retryable\"":true,\""name\"":\""BrokerError\"",\""message\"":\""The element action exceeded its total deadline.\""}""}],""isError"":true}" +4,0,navigation,840.5522500000006, +4,0,reconnect,613.8751659999998, +4,0,cancellation,406.25287500000013, +4,0,revocation,580.9712500000005, +4,1,navigation,1035.4930839999997, +4,1,reconnect,556.6568330000009, +4,1,cancellation,454.0032500000016, +4,1,revocation,515.974000000002, +4,2,navigation,1141.3940829999992, +4,2,reconnect,547.7874589999992, +4,2,cancellation,441.2112909999996, +4,2,revocation,456.3913750000029, +4,3,navigation,481.79870799999844, +4,3,reconnect,533.2465419999971, +4,3,cancellation,448.8767079999998, +4,3,revocation,445.0839589999996, +4,4,navigation,467.38887500000055, +4,4,reconnect,564.445958999997, +4,4,cancellation,476.5116250000028, +4,4,revocation,442.97920800000065, +4,5,navigation,440.40604200000234, +4,5,reconnect,555.7663339999999, +4,5,cancellation,466.0645420000001, +4,5,revocation,372.3630839999969, +4,6,navigation,470.66541700000016, +4,6,reconnect,565.6133749999972, +4,6,cancellation,661.6829580000012, +4,6,revocation,905.8379590000004, +4,7,navigation,483.35275000000183, +4,7,reconnect,563.7919580000016, +4,7,cancellation,447.692417000002, +4,7,revocation,484.77487499999916, +4,8,navigation,461.7535409999982, +4,8,reconnect,601.1364579999972, +4,8,cancellation,475.97095799999806, +4,8,revocation,421.68858399999954, +4,9,navigation,479.75120900000184, +4,9,reconnect,562.9543749999939, +4,9,cancellation,524.4302910000042, +4,9,revocation,433.34520899999916, +4,10,navigation,536.872832999994, +4,10,reconnect,542.5917079999999, +4,10,cancellation,446.8356669999994, +4,10,revocation,454.43162499999744, +4,11,navigation,482.5273340000058, +4,11,reconnect,555.3129170000029, +4,11,cancellation,449.026208999996, +4,11,revocation,386.9897920000003, +4,12,navigation,459.3313750000016, +4,12,reconnect,613.0302919999958, +4,12,cancellation,478.3731669999979, +4,12,revocation,480.9335829999982, +4,13,navigation,640.1185830000031, +4,13,reconnect,579.1991660000058, +4,13,cancellation,444.59275000000343, +4,13,revocation,441.83141700000124, +4,14,navigation,466.1395829999965, +4,14,reconnect,577.3650840000046, +4,14,cancellation,463.84808400000475, +4,14,revocation,357.27283299999544, +4,15,navigation,461.580958999999, +4,15,reconnect,548.2259169999961, +4,15,cancellation,439.5142500000002, +4,15,revocation,417.14800000000105, +4,16,navigation,439.3299580000021, +4,16,reconnect,545.1488329999993, +4,16,cancellation,452.66941699999734, +4,16,revocation,387.4641249999986, +4,17,navigation,475.19341699999495, +4,17,reconnect,763.211000000003, +4,17,cancellation,472.66116599999805, +4,17,revocation,871.4312500000015, +4,18,navigation,717.788542000002, +4,18,reconnect,554.7275840000002, +4,18,cancellation,446.1434999999983, +4,18,revocation,444.8478749999995, +4,19,navigation,452.5534590000025, +4,19,reconnect,559.7667920000022, +4,19,cancellation,424.00483299999905, +4,19,revocation,381.98379099999875, +4,20,navigation,455.3364169999986, +4,20,reconnect,562.4774580000012, +4,20,cancellation,445.31087500000285, +4,20,revocation,378.50850000000355, +4,21,navigation,468.294167, +4,21,reconnect,561.1938749999972, +4,21,cancellation,453.0898329999982, +4,21,revocation,375.6490830000039, +4,22,navigation,517.2823750000025, +4,22,reconnect,592.3987080000006, +4,22,cancellation,434.72745900000155, +4,22,revocation,423.90404199999466, +4,23,navigation,450.7939170000027, +4,23,reconnect,599.7050410000011, +4,23,cancellation,1324.3795830000017, +4,23,revocation,518.8442919999943, +4,24,navigation,596.5919589999976, +4,24,reconnect,574.5035419999986, +4,24,cancellation,776.609292000001, +4,24,revocation,1636.4512499999983, +4,25,navigation,1313.766791999995, +4,25,reconnect,788.9881250000035, +4,25,cancellation,437.35641600000963, +4,25,revocation,505.1249170000083, +4,26,navigation,685.5883749999921, +4,26,reconnect,604.414042000004, +4,26,cancellation,426.9273339999927, +4,26,revocation,1128.7142500000045, +4,27,navigation,528.9595420000114, +4,27,reconnect,631.538292000012, +4,27,cancellation,647.5146249999962, +4,27,revocation,494.4516670000012, +4,28,navigation,455.2708330000023, +4,28,reconnect,619.6287089999969, +4,28,cancellation,488.17804199999955, +4,28,revocation,467.6162919999915, +4,29,navigation,490.4511249999923, +4,29,reconnect,604.9181249999965, +4,29,cancellation,442.97474999999395, +4,29,revocation,453.1534580000007, diff --git a/docs/benchmarks/2026-09-09-lifecycle.md b/docs/benchmarks/2026-09-09-lifecycle.md new file mode 100644 index 0000000..1b250ac --- /dev/null +++ b/docs/benchmarks/2026-09-09-lifecycle.md @@ -0,0 +1,52 @@ +# Lifecycle fixes and repeated validation + +Recorded on 9 September 2026 with Chromium 151.0.7922.34, Node 24.20.0, macOS arm64 and an Apple M3 Pro with 11 logical processors. Public fixtures retain 10,000 nodes, 200 DOM levels, 20 closed shadow roots and three cross-origin frame levels. No production page data is included. + +## Lifecycle matrix + +Run the optional matrix through the existing extension E2E: + +```sh +CDB_LIFECYCLE_DIAGNOSTICS_OUTPUT=/tmp/cdb-lifecycle.json pnpm exec vitest run --project extension-e2e tests/e2e/devframe-native.test.ts +``` + +Each of 30 cycles reloads and acts, reconnects and acts, cancels a covered action and acts again, then revokes during a covered action, obtains another approval and acts again. Cancellation and revocation also check the ordinary shared Devframe RPC connection. The surrounding E2E retains independent overlapping grants, live group membership, navigation scope, the standalone panel and successful interaction in closed shadow roots inside nested cross-origin frames. + +| Final run scenario | Cycles | Failures | Complete scenario p95 | +| --- | ---: | ---: | ---: | +| Reload and action | 30 | 0 | 1,141 ms | +| Reconnect and action | 30 | 0 | 763 ms | +| Cancel, shared RPC read and action | 30 | 0 | 777 ms | +| Revoke, shared RPC read, reapprove and action | 30 | 0 | 1,129 ms | + +These are complete lifecycle scenarios, not warm single-action measurements. Machine load averages were 26.3/20.6/18.3 before and 22.2/19.9/18.2 afterward. [All lifecycle samples](./2026-09-09-lifecycle.csv) include the preliminary failures: + +- Run 1 revoked a scope left empty by the earlier overlap test. The diagnostic setup now clears those earlier scopes. +- Run 2 completed 11 cycles before its rapid reapproval reached the existing two-second request rate limit. The test service now disables this throttle, and the diagnostic surfaces an early access-request error immediately. +- Run 3 timed out during discovery after reapproval in cycle 8. No input was dispatched. The 13th CDP command, an accessibility query, had not completed by the two-second deadline. This run overlapped downstream builds and shell startup. +- Run 4 completed all 120 scenarios without overlapping builds or other test suites. The original mouse-dispatch stall and Chrome crash were not reproduced. The cause of the discovery timeout remains unresolved; a clean run does not explain it. + +Opt-in `cdb.mcp.action` diagnostics now include pending and failed commands, their elapsed time at action completion and dispatch status. Previously only completed commands appeared, hiding the operation that was pending at a timeout. Normal tool responses are unchanged. A stalled-dispatch regression verifies that a diagnostic remains a snapshot of the pending command even after late completion, and that input is not replayed. + +## Performance gates + +Both benchmarks ran sequentially after the lifecycle matrix, using their existing warmups and 30 samples. [Raw measurements](./2026-09-09-latency.csv) retain these failed gates. + +| Transport and scenario | p95 | Required gate | Result | +| --- | ---: | ---: | --- | +| Devframe fill by locator | 975 ms | 500 ms | Failed | +| Devframe click by locator | 606 ms | 500 ms | Failed | +| Devframe compact snapshot | 2,188 ms | 1,000 ms | Failed | +| Devframe batch with observation | 2,219 ms | — | Measured | +| Devframe individual fill, click and snapshot | 3,383 ms | — | Measured | +| Devframe covered click | 2,284 ms | — | No input dispatched | +| WebSocket action by reference | 433 ms | 500 ms | Passed | +| WebSocket compact snapshot | 1,965 ms | 1,000 ms | Failed | + +Devframe load averages were 15.8/18.6/17.8 before and 16.2/18.1/17.7 afterward. WebSocket load rose from 13.5/16.6/17.2 to 29.4/21.5/19.0. Both transports had slower snapshots than the earlier passing measurements. Different action selectors prevent a direct transport-only comparison. Contention is a plausible contributor, but is not isolated as the cause. No speculative performance optimization was applied, and the earlier passing runs do not satisfy the current gates. + +All measured actions completed successfully. The covered-action duration includes delivery and event-loop overhead around the two-second input deadline. It is reported separately from successful actions. No private application loading, approval UI time or model time is included in these public benchmark results. + +## Remaining validation + +Performance acceptance is open. The main-profile QA Helper must reload the rebuilt extension before final staging and local-shell validation can establish the new readiness replay behavior. Upstream toast removal is fixed in [Devframe PR #375](https://github.com/devframes/devframe/pull/375). Downstream pins remain unchanged pending the compatible Vite upgrade; changing live notification details without resurfacing dismissed toasts remains deferred with that adoption. diff --git a/packages/broker/src/runtime.ts b/packages/broker/src/runtime.ts index ae1977f..274ec95 100644 --- a/packages/broker/src/runtime.ts +++ b/packages/broker/src/runtime.ts @@ -280,12 +280,14 @@ export async function createBroker(configuration: BrokerDefinition = {}): Promis }); }); - const unsubscribeRequests = grantCoordinator.subscribe(({ requestId, request, error }) => { + const unsubscribeRequests = grantCoordinator.subscribe(({ requestId, request, error, reason }) => { for (const session of sessions.values()) session.refreshTargets(); const pending = pendingRequests.get(requestId); if (pending !== undefined && (request === undefined || error !== undefined)) { pendingRequests.delete(requestId); - pending.reject(new BrokerError(error?.code ?? 'ACCESS_REQUEST_TIMEOUT', error?.message ?? 'The browser access request ended.', error?.retryable ?? false)); + const code = reason === 'expired' ? 'ACCESS_REQUEST_TIMEOUT' : reason === 'rejected' ? 'ACCESS_REQUEST_REJECTED' : 'ACCESS_REQUEST_CANCELLED'; + const message = reason === 'expired' ? 'The browser access request expired.' : reason === 'rejected' ? 'The browser access request was rejected.' : 'The browser access request was cancelled.'; + pending.reject(new BrokerError(error?.code ?? code, error?.message ?? message, error?.retryable ?? false)); } changed(); }); @@ -636,7 +638,8 @@ export async function createBroker(configuration: BrokerDefinition = {}): Promis }, reconcileScope, async revokeScope(requestId: string) { - await grantCoordinator.cancel(requestId); + const request = grantCoordinator.getRequest(requestId); + await grantCoordinator.cancel(requestId, request?.state === 'granted' ? 'revoked' : 'rejected'); await refreshSessions(); changed(); }, diff --git a/packages/broker/test/runtime.test.ts b/packages/broker/test/runtime.test.ts index bdff42f..b551c03 100644 --- a/packages/broker/test/runtime.test.ts +++ b/packages/broker/test/runtime.test.ts @@ -13,9 +13,9 @@ afterEach(async () => { await Promise.all(brokers.splice(0).map(async broker => broker.dispose())); }); -async function fixture() { +async function fixture(accessRequestTimeoutMilliseconds = 60_000) { const identityStore = createMemoryBrokerIdentityStore(); - const broker = await createBroker({ identityStore, timing: { requestRateLimitMilliseconds: 0 } }); + const broker = await createBroker({ identityStore, timing: { requestRateLimitMilliseconds: 0, accessRequestTimeoutMilliseconds } }); brokers.push(broker); const peer = { id: 'provider-peer' }; const registration = { id: 'test-provider', instanceId: crypto.randomUUID(), name: 'Public test provider', version: '1.0.0', maximumLevel: 'debug' as const }; @@ -67,6 +67,29 @@ async function approve(setup: Awaited>, principalId: } describe('composed browser broker', () => { + it('reports approval expiry separately from rejection and clears the request', async () => { + expect.assertions(3); + const setup = await fixture(500); + const outcome = setup.broker.invoke({ id: 'expired-agent' }, 'browser.request_access', { level: 'interact' }) + .catch((error: unknown) => error); + await expect.poll(() => setup.broker.snapshot().requests.length).toBe(1); + expect(await outcome).toMatchObject({ code: 'ACCESS_REQUEST_TIMEOUT', retryable: false }); + expect(setup.broker.snapshot().requests).toEqual([]); + }); + + it('reports explicit rejection without granting access or calling it a timeout', async () => { + expect.assertions(4); + const setup = await fixture(); + const outcome = setup.broker.invoke({ id: 'rejected-agent' }, 'browser.request_access', { level: 'interact' }) + .catch((error: unknown) => error); + await expect.poll(() => setup.broker.snapshot().requests.length).toBe(1); + const request = setup.broker.snapshot().requests[0]!; + await setup.broker.revokeScope(request.id); + expect(await outcome).toMatchObject({ code: 'ACCESS_REQUEST_REJECTED', retryable: false }); + expect(setup.broker.snapshot().requests).toEqual([]); + expect(await setup.broker.invoke({ id: 'rejected-agent' }, 'browser.list_targets', {})).toEqual([]); + }); + it('forgets every retained credential for a provider installation', async () => { expect.assertions(4); const setup = await fixture(); diff --git a/packages/core/src/grant-request.ts b/packages/core/src/grant-request.ts index d57ece1..fd41440 100644 --- a/packages/core/src/grant-request.ts +++ b/packages/core/src/grant-request.ts @@ -46,7 +46,10 @@ export interface GrantRequestClaim { readonly requestId: string; } +export type GrantRequestCancellationReason = 'cancelled' | 'expired' | 'rejected' | 'revoked'; + export interface GrantRequestChange { + readonly reason?: GrantRequestCancellationReason; readonly error?: GrantRequestError; readonly request?: GrantRequest; readonly requestId: string; @@ -63,7 +66,7 @@ export const defaultGrantRequestTimingPolicy: Readonly export interface GrantRequestCoordinator { /** Copies of coordinator-owned requests and their last committed bindings, without claim secrets. */ inspect: () => readonly { readonly request: GrantRequest; readonly bindings: readonly AuthorityBinding[]; readonly provider?: GrantRequestProvider }[]; - cancel: (requestId: string) => Promise; + cancel: (requestId: string, reason?: GrantRequestCancellationReason) => Promise; claim: (requestId: string, provider: GrantRequestProvider) => GrantRequestClaim; complete: (claim: GrantRequestClaim, targets: readonly GrantedTargetReference[]) => Promise; dispose: () => Promise; @@ -114,10 +117,11 @@ export function createGrantRequestCoordinator( validateTimeoutMilliseconds(timing.requestTimeoutMilliseconds, 'requestTimeoutMilliseconds'); let disposed = false; - function notify(requestId: string, error?: GrantRequestError): void { + function notify(requestId: string, error?: GrantRequestError, reason?: GrantRequestCancellationReason): void { const stored = requests.get(requestId); const request = stored?.cancelled === true ? undefined : stored?.request; const change = { + ...(reason === undefined ? {} : { reason }), ...(error === undefined ? {} : { error }), ...(request === undefined ? {} : { request: structuredClone(request) }), requestId, @@ -156,7 +160,7 @@ export function createGrantRequestCoordinator( scheduleExpiry(stored); return; } - void cancel(stored.request.id).catch(() => notify(stored.request.id, new GrantRequestError('GRANT_AUTHORITY_UNAVAILABLE', 'Expired authority could not be removed from the store.', true))); + void cancel(stored.request.id, 'expired').catch(() => notify(stored.request.id, new GrantRequestError('GRANT_AUTHORITY_UNAVAILABLE', 'Expired authority could not be removed from the store.', true))); }, Math.min(2_147_483_647, Math.max(0, deadline - Date.now()))); } @@ -278,13 +282,13 @@ export function createGrantRequestCoordinator( } } - async function cancel(requestId: string): Promise { + async function cancel(requestId: string, reason: GrantRequestCancellationReason = 'cancelled'): Promise { const stored = requests.get(requestId); if (stored === undefined) return; if (!stored.cancelled) { stored.cancelled = true; if (stored.timeout !== undefined) clearTimeout(stored.timeout); - notify(requestId); + notify(requestId, undefined, reason); } await stored.operation?.catch(() => undefined); if (stored.bindingIds.size > 0) await removeBindings(stored).catch(authorityFailure); @@ -325,7 +329,7 @@ export function createGrantRequestCoordinator( }, async dispose() { disposed = true; - await Promise.all(Array.from(requests.keys(), cancel)); + await Promise.all(Array.from(requests.keys(), async requestId => cancel(requestId))); listeners.clear(); }, getRequest(requestId) { diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index c7872fe..26df16d 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -94,6 +94,7 @@ export { defaultGrantRequestTimingPolicy, type GrantedTargetReference, type GrantRequest, + type GrantRequestCancellationReason, type GrantRequestChange, type GrantRequestClaim, type GrantRequestCoordinator, diff --git a/packages/core/test/grant-request.test.ts b/packages/core/test/grant-request.test.ts index 604918d..15353b9 100644 --- a/packages/core/test/grant-request.test.ts +++ b/packages/core/test/grant-request.test.ts @@ -422,3 +422,30 @@ it('projects committed request metadata without claim secrets and revokes bindin expect(coordinator.inspect()[0]?.bindings).toEqual([]); await coordinator.dispose(); }); + +it('publishes the first cancellation reason once and fences a racing approval', async () => { + expect.assertions(3); + const { coordinator } = setup(); + const request = await coordinator.request({ capabilities: { level: 'interact' }, logicalSessionId: 'session', principalId: 'client' }); + const claim = coordinator.claim(request.id, provider); + const changed = vi.fn(); + coordinator.subscribe(changed); + await Promise.all([coordinator.cancel(request.id, 'rejected'), coordinator.cancel(request.id, 'expired')]); + expect(changed).toHaveBeenCalledExactlyOnceWith({ requestId: request.id, reason: 'rejected' }); + await expect(coordinator.complete(claim, [{ targetGeneration: 1, targetId: target.id }])).rejects.toMatchObject({ code: 'GRANT_CLAIM_INVALID' }); + expect(coordinator.getRequest(request.id)).toBeUndefined(); + await coordinator.dispose(); +}); + +it('publishes expiry separately from ordinary cancellation', async () => { + expect.assertions(2); + vi.useFakeTimers(); + const { coordinator } = setup(); + const request = await coordinator.request({ capabilities: { level: 'interact' }, expiresAt: new Date(Date.now() + 100).toISOString(), logicalSessionId: 'session', principalId: 'client' }); + const changed = vi.fn(); + coordinator.subscribe(changed); + await vi.advanceTimersByTimeAsync(100); + expect(changed).toHaveBeenCalledExactlyOnceWith({ requestId: request.id, reason: 'expired' }); + expect(coordinator.getRequest(request.id)).toBeUndefined(); + await coordinator.dispose(); +}); diff --git a/packages/devframe/README.md b/packages/devframe/README.md index 786b3f8..74cf057 100644 --- a/packages/devframe/README.md +++ b/packages/devframe/README.md @@ -49,4 +49,6 @@ For an existing application container, import `mountBrowserControlPanel` from `@ The page script emits `cdb:review-request` with `{ requestId }` to request the embedding application's final approval UI. This is an untrusted presentation intent. It never grants authority or proves a human decision. The extension must authenticate final approval from its trusted popup or other host-approved channel. +Hosts with a direct approval channel can set `approvalAction: 'accept'` on `createCdbPanel`. The page script then labels its action **Accept** and emits `cdb:accept-request`; the embedding application authenticates and handles the final approval. The default remains **Review request** and `cdb:review-request`. + See the [runnable example](../../examples/devframe/README.md). diff --git a/packages/devframe/src/page-script.ts b/packages/devframe/src/page-script.ts index 176890e..9c27868 100644 --- a/packages/devframe/src/page-script.ts +++ b/packages/devframe/src/page-script.ts @@ -6,6 +6,17 @@ import { createBrowserControlPanelClient } from './panel.js'; /** Page events request the host's final approval UI. They never approve browser access. */ export const browserControlReviewEvent = 'cdb:review-request'; +export const browserControlAcceptEvent = 'cdb:accept-request'; + +export interface BrowserControlPageOptions { + /** The embedding application owns the final approval channel for either action. */ + readonly approvalAction?: 'review' | 'accept'; +} + +/** Client-script entry selected by a host that supplies a direct approval channel. */ +export async function setupBrowserControlAcceptPage(context: BrowserControlPageContext): Promise<() => void> { + return setupBrowserControlPage(context, { approvalAction: 'accept' }); +} export interface BrowserControlPageContext { readonly rpc: DevframeRpcClient; @@ -19,17 +30,17 @@ export interface BrowserControlPageContext { } /** Uses the hub's existing page connection; the embedding extension handles the review intent. */ -export default async function setupBrowserControlPage(context: BrowserControlPageContext): Promise<() => void> { +export default async function setupBrowserControlPage(context: BrowserControlPageContext, options: BrowserControlPageOptions = {}): Promise<() => void> { const client = createBrowserControlPanelClient(context.rpc); const review = (requestId: string): void => { - window.dispatchEvent(new CustomEvent(browserControlReviewEvent, { detail: { requestId } })); + window.dispatchEvent(new CustomEvent(options.approvalAction === 'accept' ? browserControlAcceptEvent : browserControlReviewEvent, { detail: { requestId } })); }; const controller = createBrowserControlNotificationController({ onReview: request => review(request.id), onRevoke: async requestId => client.revokeScope(requestId) }); const notifications = new Map void>(); const prefix = `cdb:browser-control:${crypto.randomUUID()}`; const stopNotifications = controller.subscribe((state) => { const items = [ - ...state.requests.map(request => ({ id: `request:${request.id}`, title: 'Browser control requested', description: `${request.principalLabel} requests ${request.level} access with ${request.navigation} navigation.`, label: 'Review request', action: async () => controller.review(request.id) })), + ...state.requests.map(request => ({ id: `request:${request.id}`, title: 'Browser control requested', description: `${request.principalLabel} requests ${request.level} access with ${request.navigation} navigation.`, label: options.approvalAction === 'accept' ? 'Accept' : 'Review request', action: async () => controller.review(request.id) })), ...Array.from(Map.groupBy(state.grants, grant => grant.requestId), ([requestId, grants]) => ({ id: `grant:${requestId}`, title: 'Browser control active', description: `${grants[0]!.principalLabel}: ${grants[0]!.level} access to ${grants.length} approved ${grants.length === 1 ? 'tab' : 'tabs'}.`, label: 'Stop control', action: async () => controller.revoke(requestId) })), ]; for (const [id, remove] of notifications) { diff --git a/packages/devframe/src/panel-definition.ts b/packages/devframe/src/panel-definition.ts index 8ad3488..f82d8e9 100644 --- a/packages/devframe/src/panel-definition.ts +++ b/packages/devframe/src/panel-definition.ts @@ -30,6 +30,8 @@ export type CdbPanelDefinition = Pick BrowserControlPanelClient; readonly name?: string; + /** Match the notification action to the embedding application's approval channel. */ + readonly approvalAction?: 'review' | 'accept'; /** A mounted host may use its own JSON renderer and component properties. */ readonly renderer?: { readonly type: string; readonly components?: BrowserControlPanelComponents }; readonly dock?: { readonly category?: string; readonly defaultOrder?: number }; @@ -58,7 +60,7 @@ export function createCdbPanel(options: CdbPanelOptions): CdbPanel { icon: 'ph:browser-duotone', capabilities: { build: false }, clientAssets: jsonRenderSpaDir, - dock: { defaultOrder: 1_000, ...options.dock, clientScript: { importFrom: join(directory, 'view/page-script.js') } }, + dock: { defaultOrder: 1_000, ...options.dock, clientScript: { importFrom: join(directory, 'view/page-script.js'), ...(options.approvalAction === 'accept' ? { importName: 'setupBrowserControlAcceptPage' } : {}) } }, async setup(context) { if (disposed) throw new Error('The CDB panel was disposed.'); const client = options.client(); diff --git a/packages/devframe/test/page-script.test.ts b/packages/devframe/test/page-script.test.ts index 043ecf8..66f57a1 100644 --- a/packages/devframe/test/page-script.test.ts +++ b/packages/devframe/test/page-script.test.ts @@ -5,11 +5,11 @@ import type { BrowserControlPageContext } from '../src/page-script.js'; import { expect, it, vi } from 'vitest'; -import setupBrowserControlPage from '../src/page-script.js'; +import setupBrowserControlPage, { setupBrowserControlAcceptPage } from '../src/page-script.js'; import * as panel from '../src/panel.js'; -it('uses native hub notifications and removes actions when a request ends', async () => { - expect.assertions(8); +it.each(['review', 'accept'] as const)('uses native hub notifications with %s and removes ended requests', async (approvalAction) => { + expect.assertions(9); const state: BrokerState = { revision: 1, providers: [], principals: [], targets: [], grants: [], scopes: [], leases: [], requests: [{ id: 'request', principalId: 'principal', principalLabel: 'Agent', level: 'interact', navigation: 'same-origin', state: 'pending', createdAt: 0, expiresAt: null }] }; let update: (state: BrokerState) => void = () => {}; const stopWatching = vi.fn(); @@ -29,14 +29,16 @@ it('uses native hub notifications and removes actions when a request ends', asyn revokeGrant: async () => true, disconnectProvider: async () => true, }); - const dispose = await setupBrowserControlPage({ rpc: {} as BrowserControlPageContext['rpc'], current: { domElements: {} }, commands: { register }, messages: { info } }); + const setupPage = approvalAction === 'accept' ? setupBrowserControlAcceptPage : setupBrowserControlPage; + const dispose = await setupPage({ rpc: {} as BrowserControlPageContext['rpc'], current: { domElements: {} }, commands: { register }, messages: { info } }); try { expect(info).toHaveBeenCalledWith('Browser control requested', expect.objectContaining({ notify: true, autoDismiss: false, description: 'Agent requests interact access with same-origin navigation.' })); expect(document.querySelector('aside')).toBeNull(); expect(document.documentElement.hasAttribute('data-cdb-notifications-ready')).toBe(true); const command = register.mock.calls[0]![0]; + expect(command.title).toBe(approvalAction === 'accept' ? 'Accept' : 'Review request'); await command.action(); - expect(dispatch).toHaveBeenCalledWith(expect.objectContaining({ type: 'cdb:review-request', detail: { requestId: 'request' } })); + expect(dispatch).toHaveBeenCalledWith(expect.objectContaining({ type: `cdb:${approvalAction}-request`, detail: { requestId: 'request' } })); update({ ...state, requests: [] }); await vi.waitUntil(() => dismiss.mock.calls.length === 1); expect(unregister).toHaveBeenCalledOnce(); diff --git a/packages/mcp/src/index.ts b/packages/mcp/src/index.ts index b33a104..1b18727 100644 --- a/packages/mcp/src/index.ts +++ b/packages/mcp/src/index.ts @@ -261,7 +261,7 @@ interface InputActionScope { requestCharacters: number; responseCharacters: number; retries: number; - readonly commands: { method: string; milliseconds: number }[]; + readonly commands: { method: string; started: number; milliseconds?: number; outcome: 'pending' | 'completed' | 'failed' }[]; } const inputActionScope = new AsyncLocalStorage(); @@ -315,7 +315,7 @@ async function invokeElementAction(name: string, timeoutMilliseconds: number, pa signal.removeEventListener('abort', abort); if (inputActionDiagnostics.hasSubscribers) { scope.phaseMilliseconds[scope.phase] += performance.now() - scope.phaseStarted; - inputActionDiagnostics.publish({ name, milliseconds: performance.now() - started, phaseMilliseconds: { ...scope.phaseMilliseconds }, commandCount: scope.commandCount, requestCharacters: scope.requestCharacters, responseCharacters: scope.responseCharacters, retries: scope.retries, commands: [...scope.commands], dispatched: scope.dispatched, timedOut: cancellation.signal.aborted }); + inputActionDiagnostics.publish({ name, milliseconds: performance.now() - started, phaseMilliseconds: { ...scope.phaseMilliseconds }, commandCount: scope.commandCount, requestCharacters: scope.requestCharacters, responseCharacters: scope.responseCharacters, retries: scope.retries, commands: scope.commands.map(({ started: commandStarted, milliseconds, ...command }) => ({ ...command, milliseconds: milliseconds ?? performance.now() - commandStarted })), dispatched: scope.dispatched, timedOut: cancellation.signal.aborted }); } } } @@ -1176,7 +1176,7 @@ async function executeLifecycleAction( }, signal); }; const loaderId = stringValue(property(navigationValue, 'loaderId')); - if (input.sessionId === undefined && loaderId !== undefined) { + if (input.sessionId === undefined && (loaderId !== undefined || commandMethods.includes('Page.reload'))) { const target = await renewedTarget; return { command: commandResult, milestone: await waitAfterAuthorityRenewal(target) }; } @@ -3164,11 +3164,22 @@ function createCdbToolDefinitionsForSession( if (inputActionDiagnostics.hasSubscribers) scope.requestCharacters += JSON.stringify(command).length; } const started = performance.now(); - const result = await rawClient.executeCommand(command); - if (scope !== undefined && inputActionDiagnostics.hasSubscribers) scope.commands.push({ method: command.method, milliseconds: performance.now() - started }); - if (scope !== undefined && inputActionDiagnostics.hasSubscribers) scope.responseCharacters += JSON.stringify(result).length; - scope?.signal.throwIfAborted(); - return result; + const measurement: InputActionScope['commands'][number] | undefined = scope !== undefined && inputActionDiagnostics.hasSubscribers + ? { method: command.method, started, outcome: 'pending' } + : undefined; + if (measurement !== undefined) scope!.commands.push(measurement); + try { + const result = await rawClient.executeCommand(command); + if (measurement !== undefined) measurement.outcome = 'completed'; + if (scope !== undefined && inputActionDiagnostics.hasSubscribers) scope.responseCharacters += JSON.stringify(result).length; + scope?.signal.throwIfAborted(); + return result; + } catch (error) { + if (measurement !== undefined && measurement.outcome === 'pending') measurement.outcome = 'failed'; + throw error; + } finally { + if (measurement !== undefined) measurement.milliseconds = performance.now() - started; + } }, listTargets: async () => { const scope = inputActionScope.getStore(); diff --git a/packages/mcp/test/mvp-session.test.ts b/packages/mcp/test/mvp-session.test.ts index c05f03f..6bf52ae 100644 --- a/packages/mcp/test/mvp-session.test.ts +++ b/packages/mcp/test/mvp-session.test.ts @@ -3,6 +3,7 @@ import type { CdpCommand, Lease, PublishedTarget } from '@dvcol/cdb'; import type { McpChromeDebuggerBridgeClient } from '../src/index.js'; import { Buffer } from 'node:buffer'; +import { channel } from 'node:diagnostics_channel'; import { expect, it, vi } from 'vitest'; @@ -534,8 +535,14 @@ it('bounds a stalled lease acquisition and releases authority arriving after the }); it('reports uncertain input at the deadline and never replays a stalled dispatch', async () => { - expect.assertions(3); + expect.assertions(5); vi.useFakeTimers(); + const measurements: unknown[] = []; + const collect = (measurement: unknown): void => { + measurements.push(measurement); + }; + const diagnostics = channel('cdb.mcp.action'); + diagnostics.subscribe(collect); let dispatches = 0; const harness = toolHarness(async (command) => { if (command.method === 'Accessibility.getFullAXTree') return { value: { nodes: [{ backendDOMNodeId: 1, nodeId: 'control', name: { value: 'Control' }, role: { value: 'button' } }] } }; @@ -555,10 +562,13 @@ it('reports uncertain input at the deadline and never replays a stalled dispatch const action = harness.invoke('browser.click', { ref: 'e1' }); await vi.advanceTimersByTimeAsync(2_000); expect((await action).text).toContain('MCP_ACTION_OUTCOME_UNKNOWN'); + expect(measurements).toMatchObject([{ dispatched: true }]); expect(cancel).toHaveBeenCalledOnce(); await vi.advanceTimersByTimeAsync(5_000); expect(dispatches).toBe(1); + expect((measurements[0] as { commands: unknown[] }).commands).toContainEqual(expect.objectContaining({ method: 'Input.dispatchMouseEvent', outcome: 'pending' })); } finally { + diagnostics.unsubscribe(collect); harness.session.dispose(); vi.useRealTimers(); } diff --git a/packages/mcp/test/semantic-controls.test.ts b/packages/mcp/test/semantic-controls.test.ts index 76ab3b9..ca3896c 100644 --- a/packages/mcp/test/semantic-controls.test.ts +++ b/packages/mcp/test/semantic-controls.test.ts @@ -116,8 +116,10 @@ it('continues a load wait on renewed authority and returns the new generation', expect(releasedRequests).toHaveLength(2); }); -it('ignores a stale same-document milestone while a full root navigation renews authority', async () => { - expect.assertions(3); +it.each(['load', 'domcontentloaded', 'commit'] as const)('ignores stale history and child-frame events while reload waits for %s', async (waitUntil) => { + expect.assertions(4); + const toolName = 'browser.reload'; + let dispatchCount = 0; const renewedTarget = { ...target, generation: 2 }; let authorityRenewed = false; const client = { @@ -126,7 +128,8 @@ it('ignores a stale same-document milestone while a full root navigation renews }, async cancelCommand() {}, async executeCommand(command: CdpCommand) { - return { operationId: command.operationId, value: { frameId: 'frame-1', loaderId: 'loader-2' } }; + dispatchCount += 1; + return { operationId: command.operationId, value: {} }; }, async listTargets() { return [authorityRenewed ? renewedTarget : target]; @@ -147,19 +150,19 @@ it('ignores a stale same-document milestone while a full root navigation renews [Symbol.asyncIterator]() { return { next: async () => request.targetGeneration === 1 - && request.match.method === 'Page.navigatedWithinDocument' + && ['Page.navigatedWithinDocument', 'Page.frameNavigated'].includes(request.match.method) ? { done: false as const, value: { method: request.match.method, - parameters: { frameId: 'frame-1', url: 'https://example.test/previous-route' }, + parameters: { frameId: 'child-frame', frame: { id: 'child-frame', parentId: 'frame-1' }, url: 'https://example.test/previous-route' }, sequence: 1, subscriptionId: crypto.randomUUID(), targetGeneration: 1, targetId: target.id, }, } - : request.targetGeneration === 2 && request.match.method === 'Page.loadEventFired' + : request.targetGeneration === 2 && request.match.method === (waitUntil === 'load' ? 'Page.loadEventFired' : 'Page.domContentEventFired') ? { done: false as const, value: { @@ -190,12 +193,12 @@ it('ignores a stale same-document milestone while a full root navigation renews } as unknown as McpChromeDebuggerBridgeClient; const session = createCdbToolSession({ client }); const targetRef = session.projectTarget(target)?.targetRef; - const navigate = session.definitions.find(tool => tool.name === 'browser.navigate'); + const navigate = session.definitions.find(tool => tool.name === toolName); if (navigate === undefined || targetRef === undefined) throw new Error('browser.navigate is missing'); const result = await navigate.invoke({ targetRef, - url: 'https://next.example.test/', + waitUntil, }); const payload = JSON.parse((result.content[0] as { readonly text: string }).text) as { readonly milestone: { readonly method: string }; @@ -204,10 +207,11 @@ it('ignores a stale same-document milestone while a full root navigation renews expect(result.isError).toBeUndefined(); expect(payload.target.targetRef).toBe('t1'); - expect(payload.milestone.method).toBe('Page.loadEventFired'); + expect(waitUntil === 'commit' ? payload.milestone : payload.milestone.method).toBe(waitUntil === 'commit' ? 'authority-renewed' : waitUntil === 'load' ? 'Page.loadEventFired' : 'Page.domContentEventFired'); + expect(dispatchCount).toBe(1); }); -it('fails a root full-navigation result when renewed authority is not published', async () => { +it.each(['browser.navigate', 'browser.reload'])('fails %s when renewed authority is not published', async (toolName) => { expect.assertions(2); const client = { async acquireLease(request: { readonly requestedMethods: readonly string[] }) { @@ -215,7 +219,7 @@ it('fails a root full-navigation result when renewed authority is not published' }, async cancelCommand() {}, async executeCommand(command: CdpCommand) { - return { operationId: command.operationId, value: { frameId: 'frame-1', loaderId: 'loader-1' } }; + return { operationId: command.operationId, value: toolName === 'browser.reload' ? {} : { frameId: 'frame-1', loaderId: 'loader-1' } }; }, async listTargets() { return [target]; @@ -262,13 +266,13 @@ it('fails a root full-navigation result when renewed authority is not published' } as unknown as McpChromeDebuggerBridgeClient; const session = createCdbToolSession({ client }); const targetRef = session.projectTarget(target)?.targetRef; - const navigate = session.definitions.find(tool => tool.name === 'browser.navigate'); + const navigate = session.definitions.find(tool => tool.name === toolName); if (navigate === undefined || targetRef === undefined) throw new Error('browser.navigate is missing'); const result = await navigate.invoke({ targetRef, timeoutMilliseconds: 1, - url: 'https://next.example.test/', + ...(toolName === 'browser.navigate' ? { url: 'https://next.example.test/' } : {}), }); expect(result.isError).toBe(true); @@ -277,3 +281,94 @@ it('fails a root full-navigation result when renewed authority is not published' retryable: true, }); }); + +it('ignores a stale same-document milestone while a full root navigation renews authority', async () => { + expect.assertions(3); + const renewedTarget = { ...target, generation: 2 }; + let authorityRenewed = false; + const client = { + async acquireLease(request: { readonly requestedMethods: readonly string[]; readonly targetGeneration: number }) { + return { ...lease(request.requestedMethods), targetGeneration: request.targetGeneration }; + }, + async cancelCommand() {}, + async executeCommand(command: CdpCommand) { + return { operationId: command.operationId, value: { frameId: 'frame-1', loaderId: 'loader-2' } }; + }, + async listTargets() { + return [authorityRenewed ? renewedTarget : target]; + }, + async releaseLease() {}, + async subscribe(request: { readonly match: { readonly method: string }; readonly targetGeneration: number }) { + let resolvePendingEvent: ((result: IteratorResult) => void) | undefined; + return { + close() { + resolvePendingEvent?.({ done: true, value: undefined }); + }, + droppedCount: 0, + id: crypto.randomUUID(), + lastDeliveredSequence: 0, + overflowed: false, + targetGeneration: request.targetGeneration, + targetId: target.id, + [Symbol.asyncIterator]() { + return { + next: async () => request.targetGeneration === 1 + && request.match.method === 'Page.navigatedWithinDocument' + ? { + done: false as const, + value: { + method: request.match.method, + parameters: { frameId: 'frame-1', url: 'https://example.test/previous-route' }, + sequence: 1, + subscriptionId: crypto.randomUUID(), + targetGeneration: 1, + targetId: target.id, + }, + } + : request.targetGeneration === 2 && request.match.method === 'Page.loadEventFired' + ? { + done: false as const, + value: { + method: request.match.method, + parameters: { timestamp: 2 }, + sequence: 1, + subscriptionId: crypto.randomUUID(), + targetGeneration: 2, + targetId: target.id, + }, + } + : new Promise>((resolve) => { + resolvePendingEvent = resolve; + }), + }; + }, + }; + }, + watchTargets() { + return { + async* [Symbol.asyncIterator]() { + await new Promise(resolve => setTimeout(resolve, 5)); + authorityRenewed = true; + yield { kind: 'updated' as const, sequence: 2, target: renewedTarget }; + }, + }; + }, + } as unknown as McpChromeDebuggerBridgeClient; + const session = createCdbToolSession({ client }); + const targetRef = session.projectTarget(target)?.targetRef; + const navigate = session.definitions.find(tool => tool.name === 'browser.navigate'); + if (navigate === undefined || targetRef === undefined) throw new Error('browser.navigate is missing'); + + const result = await navigate.invoke({ + targetRef, + url: 'https://next.example.test/', + }); + const payload = JSON.parse((result.content[0] as { readonly text: string }).text) as { + readonly milestone: { readonly method: string }; + readonly target: { readonly targetRef: string }; + }; + + expect(result.isError).toBeUndefined(); + expect(payload.target.targetRef).toBe('t1'); + expect(payload.milestone.method).toBe('Page.loadEventFired'); +}); diff --git a/tests/e2e/devframe-native.test.ts b/tests/e2e/devframe-native.test.ts index 1eb21dd..b79db4e 100644 --- a/tests/e2e/devframe-native.test.ts +++ b/tests/e2e/devframe-native.test.ts @@ -17,6 +17,7 @@ import { build } from 'vite'; import { expect, it } from 'vitest'; import { deepDomPage } from './fixtures/deep-dom-page.js'; +import { diagnoseLifecycle } from './fixtures/lifecycle-diagnostics.js'; import { benchmarkAgentWorkflow } from './fixtures/workflow-benchmark.js'; interface FixtureWorker { @@ -50,7 +51,7 @@ it('runs native MCP actions through Chrome extension and shared Devframe RPC acr } }); const devframe = await createDevServer(defineDevframe({ ...panel.definition, - services: [createCdbService()], + services: [createCdbService({ broker: { timing: { requestRateLimitMilliseconds: 0 } } })], async setup(context) { service = getCdbService(context); await panel.definition.setup(context); @@ -192,9 +193,10 @@ it('runs native MCP actions through Chrome extension and shared Devframe RPC acr await management.getByRole('button', { name: 'Revoke target', exact: true }).click(); await expect.poll(() => broker.snapshot().grants).toEqual([]); await expect.poll(async () => management.getByText('No shared tabs. Approve an agent request in the browser to grant access.', { exact: true }).count()).toBe(1); + await diagnoseLifecycle({ agent, broker, worker, page, chromiumVersion: browser.browser()?.version() ?? 'unavailable' }); await worker.evaluate(async () => (globalThis as unknown as FixtureWorker).stopDevframeProvider()); expect(broker.snapshot().grants).toEqual([]); } finally { for (const cleanup of cleanups.reverse()) await cleanup(); } -}, process.env.CDB_DEVFRAME_BENCHMARK_OUTPUT === undefined ? 90_000 : 240_000); +}, process.env.CDB_LIFECYCLE_DIAGNOSTICS_OUTPUT === undefined ? (process.env.CDB_DEVFRAME_BENCHMARK_OUTPUT === undefined ? 90_000 : 240_000) : 600_000); diff --git a/tests/e2e/fixtures/devframe-service-worker.ts b/tests/e2e/fixtures/devframe-service-worker.ts index 9f0c3ec..05bbd64 100644 --- a/tests/e2e/fixtures/devframe-service-worker.ts +++ b/tests/e2e/fixtures/devframe-service-worker.ts @@ -42,6 +42,9 @@ Object.assign(globalThis, { if (joined) await chrome.tabs.group({ tabIds: [tab.id], groupId: approvedGroupId }); else await chrome.tabs.ungroup(tab.id); }, + async readDevframeState() { + return client!.snapshot(); + }, disconnectDevframeProvider() { client?.disconnected(); connection?.close?.(); diff --git a/tests/e2e/fixtures/lifecycle-diagnostics.ts b/tests/e2e/fixtures/lifecycle-diagnostics.ts new file mode 100644 index 0000000..9c130ce --- /dev/null +++ b/tests/e2e/fixtures/lifecycle-diagnostics.ts @@ -0,0 +1,119 @@ +import type { BrokerRuntime } from '@dvcol/cdb-broker'; +import type { Client } from '@modelcontextprotocol/client'; +import type { Page, Worker } from 'playwright'; + +import assert from 'node:assert/strict'; +import { channel } from 'node:diagnostics_channel'; +import { writeFile } from 'node:fs/promises'; +import { loadavg } from 'node:os'; +import { setTimeout as delay } from 'node:timers/promises'; +import { styleText } from 'node:util'; + +interface LifecycleWorker { + approveDevframeRequest: (requestId: string) => Promise; + disconnectDevframeProvider: () => void; + readDevframeState: () => Promise; +} + +/** Optional repeated lifecycle diagnostics over the same real extension and MCP connection. */ +export async function diagnoseLifecycle(options: { agent: Client; broker: BrokerRuntime; worker: Worker; page: Page; chromiumVersion: string }): Promise { + const output = process.env.CDB_LIFECYCLE_DIAGNOSTICS_OUTPUT; + if (output === undefined) return; + const { agent, broker, worker, page } = options; + const cycles = 30; + const initialLoad = loadavg(); + const measurements: { cycle: number; scenario: string; milliseconds: number; error?: string }[] = []; + const actionMetrics: unknown[] = []; + const diagnostics = channel('cdb.mcp.action'); + const collect = (measurement: unknown): void => { + actionMetrics.push(measurement); + }; + diagnostics.subscribe(collect); + async function until(ready: () => boolean): Promise { + const deadline = Date.now() + 10_000; + while (!ready()) { + assert(Date.now() < deadline, 'Lifecycle state did not settle within 10 seconds.'); + await delay(20); + } + } + async function call(name: string, parameters: Record): Promise { + const result = await agent.callTool({ name, arguments: parameters }); + assert(!result.isError, JSON.stringify(result)); + const content = result.content as { type: string; text?: string }[]; + return JSON.parse(content.find(item => item.type === 'text')?.text ?? 'null'); + } + async function approve(): Promise { + const requested = call('browser.request_access', { level: 'interact' }); + requested.catch(() => {}); + await Promise.race([ + until(() => broker.snapshot().requests.length === 1), + requested.then(() => { + throw new Error('Access completed without an approval request.'); + }), + ]); + await worker.evaluate(async requestId => (globalThis as unknown as LifecycleWorker).approveDevframeRequest(requestId), broker.snapshot().requests[0]!.id); + await requested; + const targets = await call('browser.list_targets', {}) as { targetRef: string }[]; + assert.equal(targets.length, 1); + return targets[0]!.targetRef; + } + async function record(cycle: number, scenario: string, run: () => Promise): Promise { + const started = performance.now(); + try { + await run(); + measurements.push({ cycle, scenario, milliseconds: performance.now() - started }); + } catch (error) { + measurements.push({ cycle, scenario, milliseconds: performance.now() - started, error: String(error) }); + throw error; + } + } + const locator = { role: 'button', name: { match: 'exact', value: 'Save deep value' } }; + try { + for (const scope of broker.snapshot().scopes) await broker.revokeScope(scope.id); + await page.goto(page.url().replace('cdb-other.test', 'cdb-root.test')); + let targetRef = await approve(); + for (let cycle = 0; cycle < cycles; cycle += 1) { + await record(cycle, 'navigation', async () => { + await call('browser.reload', { targetRef, timeoutMilliseconds: 10_000 }); + await call('browser.click', { targetRef, locator }); + }); + await record(cycle, 'reconnect', async () => { + const generation = broker.snapshot().grants[0]!.targetGeneration; + await worker.evaluate(() => (globalThis as unknown as LifecycleWorker).disconnectDevframeProvider()); + await until(() => broker.snapshot().grants.some(grant => grant.state === 'active' && grant.targetGeneration > generation)); + await call('browser.click', { targetRef, locator }); + }); + await record(cycle, 'cancellation', async () => { + await page.locator('#toggle-overlay').click(); + const abort = new AbortController(); + const pending = agent.callTool({ name: 'browser.click', arguments: { targetRef, locator } }, { signal: abort.signal }).then(value => ({ value }), (error: unknown) => ({ error })); + await delay(100); + abort.abort(); + const outcome = await pending; + assert('error' in outcome || outcome.value.isError, 'Cancellation unexpectedly completed the covered action.'); + await until(() => broker.snapshot().leases.length === 0); + await worker.evaluate(async () => (globalThis as unknown as LifecycleWorker).readDevframeState()); + await page.locator('#parent-overlay').evaluate(element => element.remove()); + await call('browser.click', { targetRef, locator }); + }); + await record(cycle, 'revocation', async () => { + await page.locator('#toggle-overlay').click(); + const pending = agent.callTool({ name: 'browser.click', arguments: { targetRef, locator } }).then(value => ({ value }), (error: unknown) => ({ error })); + await delay(100); + await broker.revokeScope(broker.snapshot().scopes[0]!.id); + const outcome = await pending; + assert('error' in outcome || outcome.value.isError, 'Revocation unexpectedly completed the covered action.'); + assert.deepEqual(await call('browser.list_targets', {}), []); + await worker.evaluate(async () => (globalThis as unknown as LifecycleWorker).readDevframeState()); + await page.locator('#parent-overlay').evaluate(element => element.remove()); + targetRef = await approve(); + await call('browser.click', { targetRef, locator }); + }); + if ((cycle + 1) % 5 === 0) console.info(styleText('cyan', '🔎 [lifecycle-diagnostics]'), `${cycle + 1}/${cycles} cycles completed`); + } + } finally { + diagnostics.unsubscribe(collect); + await writeFile(output, JSON.stringify({ cycles, environment: { chromiumVersion: options.chromiumVersion, node: process.version, initialLoad, finalLoad: loadavg() }, measurements, actionMetrics }, null, 2)); + for (const scope of broker.snapshot().scopes) await broker.revokeScope(scope.id); + } +} From 3493134d249604b745b356eacf970b1d2918ef1d Mon Sep 17 00:00:00 2001 From: dvcolomban Date: Wed, 9 Sep 2026 12:10:43 +0200 Subject: [PATCH 3/6] docs: keep live host validation outside public benchmarks --- docs/benchmarks/2026-09-09-lifecycle.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/benchmarks/2026-09-09-lifecycle.md b/docs/benchmarks/2026-09-09-lifecycle.md index 1b250ac..0bd220b 100644 --- a/docs/benchmarks/2026-09-09-lifecycle.md +++ b/docs/benchmarks/2026-09-09-lifecycle.md @@ -49,4 +49,4 @@ All measured actions completed successfully. The covered-action duration include ## Remaining validation -Performance acceptance is open. The main-profile QA Helper must reload the rebuilt extension before final staging and local-shell validation can establish the new readiness replay behavior. Upstream toast removal is fixed in [Devframe PR #375](https://github.com/devframes/devframe/pull/375). Downstream pins remain unchanged pending the compatible Vite upgrade; changing live notification details without resurfacing dismissed toasts remains deferred with that adoption. +Performance acceptance is open. These public fixtures do not establish live validation in embedding applications. Upstream toast removal is fixed in [Devframe PR #375](https://github.com/devframes/devframe/pull/375). Downstream pins remain unchanged pending the compatible Vite upgrade; changing live notification details without resurfacing dismissed toasts remains deferred with that adoption. From 70953290046eff5122dbe550a327b9d683484fbb Mon Sep 17 00:00:00 2001 From: dvcolomban Date: Wed, 9 Sep 2026 14:58:42 +0200 Subject: [PATCH 4/6] fix: update notifications and bound frame discovery --- docs/benchmarks/2026-09-09-aligned.csv | 2380 +++++++++++++++++ docs/benchmarks/2026-09-09-aligned.md | 114 + .../2026-09-09-followup-lifecycle.csv | 25 + docs/benchmarks/2026-09-09-lifecycle.md | 2 +- package.json | 2 + packages/devframe/README.md | 4 + packages/devframe/src/page-script.ts | 40 +- packages/devframe/test/page-script.test.ts | 118 +- packages/mcp/README.md | 2 + packages/mcp/src/index.ts | 36 +- packages/mcp/test/locator-session.test.ts | 44 + patches/@devframes__hub-ui@0.9.10.patch | 32 + patches/README.md | 9 + pnpm-lock.yaml | 39 +- pnpm-workspace.yaml | 3 + tests/e2e/devframe-native.test.ts | 2 +- tests/e2e/devframe-notifications.test.ts | 64 + tests/e2e/fixtures/workflow-benchmark.ts | 136 +- tests/e2e/native-agent-benchmark.test.ts | 8 + 19 files changed, 2991 insertions(+), 69 deletions(-) create mode 100644 docs/benchmarks/2026-09-09-aligned.csv create mode 100644 docs/benchmarks/2026-09-09-aligned.md create mode 100644 docs/benchmarks/2026-09-09-followup-lifecycle.csv create mode 100644 patches/@devframes__hub-ui@0.9.10.patch create mode 100644 patches/README.md create mode 100644 tests/e2e/devframe-notifications.test.ts diff --git a/docs/benchmarks/2026-09-09-aligned.csv b/docs/benchmarks/2026-09-09-aligned.csv new file mode 100644 index 0000000..6e0ca0e --- /dev/null +++ b/docs/benchmarks/2026-09-09-aligned.csv @@ -0,0 +1,2380 @@ +run,transport,selector,operation,milliseconds,arguments_characters,response_characters,error +cdb-aligned-websocket-before-run2,websocket,ref,browser.fill,100.850,55,56,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.click,96.317,30,57,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.snapshot,652.538,18,456,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.batch,799.477,131,605,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.fill,117.591,55,56,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.click,132.298,30,57,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.snapshot,619.824,18,456,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.batch,993.632,131,605,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.fill,103.954,55,56,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.click,97.728,30,57,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.snapshot,605.533,18,456,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.batch,742.676,131,605,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.fill,95.391,55,56,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.click,88.826,30,57,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.snapshot,570.927,18,456,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.batch,767.012,131,605,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.fill,102.022,55,56,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.click,87.729,30,57,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.snapshot,605.491,18,457,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.batch,824.714,132,610,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.fill,190.978,56,56,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.click,95.694,31,57,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.snapshot,744.312,18,461,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.batch,834.009,133,610,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.fill,112.083,56,56,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.click,92.218,31,57,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.snapshot,621.944,18,461,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.batch,1073.002,133,610,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.fill,122.802,56,56,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.click,186.925,31,57,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.snapshot,727.573,18,461,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.batch,862.100,133,610,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.fill,108.047,56,56,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.click,99.181,31,57,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.snapshot,746.659,18,461,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.batch,953.051,133,610,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.fill,155.394,56,56,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.click,108.964,31,57,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.snapshot,647.160,18,461,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.batch,908.220,133,610,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.fill,105.692,56,56,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.click,101.530,31,57,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.snapshot,624.866,18,461,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.batch,796.727,133,610,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.fill,102.733,56,56,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.click,104.418,31,57,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.snapshot,654.086,18,461,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.batch,838.757,133,610,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.fill,154.159,56,56,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.click,98.820,31,57,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.snapshot,598.025,18,461,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.batch,817.824,133,610,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.fill,159.905,56,56,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.click,113.951,31,57,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.snapshot,731.802,18,461,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.batch,1255.345,133,610,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.fill,101.783,56,56,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.click,93.759,31,57,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.snapshot,640.061,18,461,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.batch,807.659,133,610,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.fill,112.377,56,56,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.click,89.560,31,57,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.snapshot,599.714,18,461,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.batch,1085.676,133,610,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.fill,115.044,56,56,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.click,110.727,31,57,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.snapshot,601.245,18,461,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.batch,747.312,133,610,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.fill,98.809,56,56,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.click,91.286,31,57,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.snapshot,636.386,18,461,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.batch,780.098,133,610,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.fill,107.297,56,56,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.click,98.998,31,57,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.snapshot,752.409,18,461,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.batch,1009.203,133,610,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.fill,110.325,56,56,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.click,94.237,31,57,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.snapshot,650.834,18,461,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.batch,809.465,133,610,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.fill,101.151,56,56,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.click,91.906,31,57,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.snapshot,600.580,18,461,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.batch,899.548,133,610,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.fill,110.537,56,56,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.click,94.465,31,57,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.snapshot,630.301,18,461,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.batch,1096.836,133,610,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.fill,100.919,56,56,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.click,105.728,31,57,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.snapshot,621.696,18,461,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.batch,791.732,133,610,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.fill,115.694,56,56,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.click,95.919,31,57,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.snapshot,680.596,18,461,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.batch,829.249,133,610,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.fill,115.115,56,56,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.click,90.904,31,57,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.snapshot,648.420,18,461,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.batch,1838.261,133,610,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.fill,203.936,56,56,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.click,163.304,31,57,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.snapshot,921.656,18,461,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.batch,914.516,133,610,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.fill,105.251,56,56,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.click,105.509,31,57,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.snapshot,679.719,18,461,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.batch,795.705,133,610,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.fill,100.493,56,56,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.click,94.186,31,57,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.snapshot,653.524,18,461,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.batch,804.935,133,610,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.fill,106.244,56,56,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.click,109.664,31,57,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.snapshot,663.295,18,461,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.batch,849.003,133,610,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.fill,97.857,56,56,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.click,95.893,31,57,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.snapshot,620.881,18,461,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.batch,876.588,133,610,false +cdb-aligned-websocket-before-run2,websocket,ref,browser.click:blocked,2001.939,31,172,true +cdb-aligned-websocket-before-run2,websocket,ref,browser.click:blocked,2000.123,31,172,true +cdb-aligned-websocket-before-run2,websocket,ref,browser.click:blocked,2000.814,31,172,true +cdb-aligned-websocket-before-run2,websocket,ref,browser.click:blocked,2002.306,31,172,true +cdb-aligned-websocket-before-run2,websocket,ref,browser.click:blocked,2001.079,31,172,true +cdb-aligned-websocket-before-run2,websocket,ref,browser.click:blocked,1999.650,31,172,true +cdb-aligned-websocket-before-run2,websocket,ref,browser.click:blocked,2002.160,31,172,true +cdb-aligned-websocket-before-run2,websocket,ref,browser.click:blocked,2002.765,31,172,true +cdb-aligned-websocket-before-run2,websocket,ref,browser.click:blocked,2002.350,31,172,true +cdb-aligned-websocket-before-run2,websocket,ref,browser.click:blocked,2001.590,31,172,true +cdb-aligned-websocket-before-run2,websocket,ref,browser.click:blocked,2002.408,31,172,true +cdb-aligned-websocket-before-run2,websocket,ref,browser.click:blocked,2001.390,31,172,true +cdb-aligned-websocket-before-run2,websocket,ref,browser.click:blocked,2001.619,31,172,true +cdb-aligned-websocket-before-run2,websocket,ref,browser.click:blocked,2006.222,31,172,true +cdb-aligned-websocket-before-run2,websocket,ref,browser.click:blocked,2000.315,31,172,true +cdb-aligned-websocket-before-run2,websocket,ref,browser.click:blocked,2001.311,31,172,true +cdb-aligned-websocket-before-run2,websocket,ref,browser.click:blocked,2000.537,31,172,true +cdb-aligned-websocket-before-run2,websocket,ref,browser.click:blocked,2003.014,31,172,true +cdb-aligned-websocket-before-run2,websocket,ref,browser.click:blocked,2002.922,31,172,true +cdb-aligned-websocket-before-run2,websocket,ref,browser.click:blocked,2002.310,31,172,true +cdb-aligned-websocket-before-run2,websocket,ref,browser.click:blocked,2002.079,31,172,true +cdb-aligned-websocket-before-run2,websocket,ref,browser.click:blocked,2002.015,31,172,true +cdb-aligned-websocket-before-run2,websocket,ref,browser.click:blocked,2000.808,31,172,true +cdb-aligned-websocket-before-run2,websocket,ref,browser.click:blocked,2003.238,31,172,true +cdb-aligned-websocket-before-run2,websocket,ref,browser.click:blocked,2001.955,31,172,true +cdb-aligned-websocket-before-run2,websocket,ref,browser.click:blocked,2001.658,31,172,true +cdb-aligned-websocket-before-run2,websocket,ref,browser.click:blocked,2000.696,31,172,true +cdb-aligned-websocket-before-run2,websocket,ref,browser.click:blocked,2001.753,31,172,true +cdb-aligned-websocket-before-run2,websocket,ref,browser.click:blocked,2002.687,31,172,true +cdb-aligned-websocket-before-run2,websocket,ref,browser.click:blocked,2003.001,31,172,true +cdb-aligned-websocket-before-run2,websocket,locator,browser.fill,644.083,118,56,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.click,286.249,97,57,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.snapshot,700.771,18,461,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.batch,988.665,261,610,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.fill,208.793,118,56,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.click,227.215,97,57,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.snapshot,607.240,18,461,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.batch,1275.969,261,610,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.fill,215.072,118,56,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.click,188.187,97,57,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.snapshot,854.498,18,461,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.batch,908.889,261,610,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.fill,203.706,118,56,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.click,188.000,97,57,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.snapshot,748.006,18,461,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.batch,1025.232,261,610,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.fill,205.680,118,56,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.click,198.003,97,57,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.snapshot,646.509,18,461,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.batch,1078.249,261,610,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.fill,212.226,118,56,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.click,179.524,97,57,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.snapshot,592.072,18,461,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.batch,860.039,261,610,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.fill,195.687,118,56,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.click,174.439,97,57,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.snapshot,608.519,18,461,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.batch,860.894,261,610,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.fill,189.663,118,56,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.click,175.308,97,57,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.snapshot,584.770,18,461,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.batch,863.129,261,610,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.fill,195.050,118,56,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.click,181.961,97,57,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.snapshot,576.311,18,461,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.batch,919.936,261,610,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.fill,203.526,118,56,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.click,187.130,97,57,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.snapshot,597.039,18,461,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.batch,858.232,261,610,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.fill,190.644,118,56,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.click,173.140,97,57,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.snapshot,620.674,18,461,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.batch,863.546,261,610,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.fill,190.270,118,56,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.click,177.798,97,57,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.snapshot,597.355,18,461,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.batch,858.176,261,610,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.fill,194.374,118,56,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.click,188.062,97,57,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.snapshot,634.014,18,461,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.batch,891.770,261,610,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.fill,203.661,118,56,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.click,181.900,97,57,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.snapshot,605.101,18,461,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.batch,893.355,261,610,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.fill,197.527,118,56,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.click,182.822,97,57,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.snapshot,612.556,18,461,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.batch,862.920,261,610,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.fill,195.462,118,56,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.click,177.294,97,57,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.snapshot,581.507,18,461,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.batch,910.839,261,610,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.fill,202.803,118,56,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.click,176.560,97,57,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.snapshot,573.069,18,461,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.batch,877.780,261,610,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.fill,192.100,118,56,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.click,179.809,97,57,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.snapshot,585.387,18,461,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.batch,883.730,261,610,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.fill,192.374,118,56,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.click,176.698,97,57,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.snapshot,587.039,18,461,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.batch,891.264,261,610,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.fill,194.365,118,56,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.click,194.201,97,57,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.snapshot,608.139,18,461,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.batch,854.655,261,610,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.fill,193.740,118,56,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.click,176.155,97,57,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.snapshot,634.267,18,461,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.batch,895.048,261,610,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.fill,201.982,118,56,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.click,185.460,97,57,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.snapshot,643.023,18,461,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.batch,900.368,261,610,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.fill,198.589,118,56,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.click,229.749,97,57,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.snapshot,901.770,18,461,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.batch,1254.013,261,610,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.fill,247.359,118,56,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.click,270.576,97,57,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.snapshot,1483.972,18,461,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.batch,1716.991,261,610,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.fill,266.970,118,56,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.click,287.017,97,57,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.snapshot,809.155,18,461,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.batch,1085.140,261,610,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.fill,315.629,118,56,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.click,245.230,97,57,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.snapshot,680.485,18,461,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.batch,946.977,261,610,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.fill,339.351,118,56,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.click,186.384,97,57,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.snapshot,608.960,18,461,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.batch,961.514,261,610,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.fill,202.998,118,56,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.click,176.249,97,57,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.snapshot,682.514,18,461,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.batch,926.268,261,610,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.fill,205.884,118,56,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.click,181.272,97,57,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.snapshot,581.463,18,461,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.batch,852.156,261,610,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.fill,194.727,118,56,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.click,193.143,97,57,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.snapshot,599.226,18,461,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.batch,870.157,261,610,false +cdb-aligned-websocket-before-run2,websocket,locator,browser.click:blocked,2000.409,97,172,true +cdb-aligned-websocket-before-run2,websocket,locator,browser.click:blocked,2002.386,97,172,true +cdb-aligned-websocket-before-run2,websocket,locator,browser.click:blocked,2003.197,97,172,true +cdb-aligned-websocket-before-run2,websocket,locator,browser.click:blocked,2001.514,97,172,true +cdb-aligned-websocket-before-run2,websocket,locator,browser.click:blocked,2000.527,97,172,true +cdb-aligned-websocket-before-run2,websocket,locator,browser.click:blocked,2001.392,97,172,true +cdb-aligned-websocket-before-run2,websocket,locator,browser.click:blocked,2001.921,97,172,true +cdb-aligned-websocket-before-run2,websocket,locator,browser.click:blocked,2000.817,97,172,true +cdb-aligned-websocket-before-run2,websocket,locator,browser.click:blocked,2000.693,97,172,true +cdb-aligned-websocket-before-run2,websocket,locator,browser.click:blocked,2001.429,97,172,true +cdb-aligned-websocket-before-run2,websocket,locator,browser.click:blocked,2001.018,97,172,true +cdb-aligned-websocket-before-run2,websocket,locator,browser.click:blocked,2002.225,97,172,true +cdb-aligned-websocket-before-run2,websocket,locator,browser.click:blocked,2000.536,97,172,true +cdb-aligned-websocket-before-run2,websocket,locator,browser.click:blocked,2001.945,97,172,true +cdb-aligned-websocket-before-run2,websocket,locator,browser.click:blocked,1999.834,97,172,true +cdb-aligned-websocket-before-run2,websocket,locator,browser.click:blocked,2001.935,97,172,true +cdb-aligned-websocket-before-run2,websocket,locator,browser.click:blocked,2000.458,97,172,true +cdb-aligned-websocket-before-run2,websocket,locator,browser.click:blocked,2003.039,97,172,true +cdb-aligned-websocket-before-run2,websocket,locator,browser.click:blocked,2025.623,97,172,true +cdb-aligned-websocket-before-run2,websocket,locator,browser.click:blocked,2005.278,97,172,true +cdb-aligned-websocket-before-run2,websocket,locator,browser.click:blocked,2000.817,97,172,true +cdb-aligned-websocket-before-run2,websocket,locator,browser.click:blocked,2001.227,97,172,true +cdb-aligned-websocket-before-run2,websocket,locator,browser.click:blocked,2002.003,97,172,true +cdb-aligned-websocket-before-run2,websocket,locator,browser.click:blocked,2000.952,97,172,true +cdb-aligned-websocket-before-run2,websocket,locator,browser.click:blocked,2001.079,97,172,true +cdb-aligned-websocket-before-run2,websocket,locator,browser.click:blocked,2000.728,97,172,true +cdb-aligned-websocket-before-run2,websocket,locator,browser.click:blocked,2001.198,97,172,true +cdb-aligned-websocket-before-run2,websocket,locator,browser.click:blocked,2002.719,97,172,true +cdb-aligned-websocket-before-run2,websocket,locator,browser.click:blocked,2000.377,97,172,true +cdb-aligned-websocket-before-run2,websocket,locator,browser.click:blocked,2001.766,97,172,true +cdb-aligned-devframe-before,devframe,ref,browser.fill,104.066,55,56,false +cdb-aligned-devframe-before,devframe,ref,browser.click,93.529,30,57,false +cdb-aligned-devframe-before,devframe,ref,browser.snapshot,730.327,18,456,false +cdb-aligned-devframe-before,devframe,ref,browser.batch,753.342,131,605,false +cdb-aligned-devframe-before,devframe,ref,browser.fill,100.512,55,56,false +cdb-aligned-devframe-before,devframe,ref,browser.click,84.664,30,57,false +cdb-aligned-devframe-before,devframe,ref,browser.snapshot,575.800,18,456,false +cdb-aligned-devframe-before,devframe,ref,browser.batch,711.061,131,605,false +cdb-aligned-devframe-before,devframe,ref,browser.fill,94.192,55,56,false +cdb-aligned-devframe-before,devframe,ref,browser.click,88.922,30,57,false +cdb-aligned-devframe-before,devframe,ref,browser.snapshot,614.592,18,456,false +cdb-aligned-devframe-before,devframe,ref,browser.batch,837.354,131,605,false +cdb-aligned-devframe-before,devframe,ref,browser.fill,100.221,55,56,false +cdb-aligned-devframe-before,devframe,ref,browser.click,115.483,30,57,false +cdb-aligned-devframe-before,devframe,ref,browser.snapshot,714.280,18,456,false +cdb-aligned-devframe-before,devframe,ref,browser.batch,751.249,131,605,false +cdb-aligned-devframe-before,devframe,ref,browser.fill,99.544,55,56,false +cdb-aligned-devframe-before,devframe,ref,browser.click,87.867,30,57,false +cdb-aligned-devframe-before,devframe,ref,browser.snapshot,596.735,18,457,false +cdb-aligned-devframe-before,devframe,ref,browser.batch,741.387,132,610,false +cdb-aligned-devframe-before,devframe,ref,browser.fill,94.342,56,56,false +cdb-aligned-devframe-before,devframe,ref,browser.click,89.745,31,57,false +cdb-aligned-devframe-before,devframe,ref,browser.snapshot,660.726,18,461,false +cdb-aligned-devframe-before,devframe,ref,browser.batch,812.642,133,610,false +cdb-aligned-devframe-before,devframe,ref,browser.fill,104.339,56,56,false +cdb-aligned-devframe-before,devframe,ref,browser.click,89.699,31,57,false +cdb-aligned-devframe-before,devframe,ref,browser.snapshot,622.521,18,461,false +cdb-aligned-devframe-before,devframe,ref,browser.batch,906.388,133,610,false +cdb-aligned-devframe-before,devframe,ref,browser.fill,96.110,56,56,false +cdb-aligned-devframe-before,devframe,ref,browser.click,83.051,31,57,false +cdb-aligned-devframe-before,devframe,ref,browser.snapshot,579.372,18,461,false +cdb-aligned-devframe-before,devframe,ref,browser.batch,749.656,133,610,false +cdb-aligned-devframe-before,devframe,ref,browser.fill,88.501,56,56,false +cdb-aligned-devframe-before,devframe,ref,browser.click,82.776,31,57,false +cdb-aligned-devframe-before,devframe,ref,browser.snapshot,590.464,18,461,false +cdb-aligned-devframe-before,devframe,ref,browser.batch,769.605,133,610,false +cdb-aligned-devframe-before,devframe,ref,browser.fill,95.667,56,56,false +cdb-aligned-devframe-before,devframe,ref,browser.click,83.181,31,57,false +cdb-aligned-devframe-before,devframe,ref,browser.snapshot,651.451,18,461,false +cdb-aligned-devframe-before,devframe,ref,browser.batch,810.476,133,610,false +cdb-aligned-devframe-before,devframe,ref,browser.fill,105.964,56,56,false +cdb-aligned-devframe-before,devframe,ref,browser.click,112.907,31,57,false +cdb-aligned-devframe-before,devframe,ref,browser.snapshot,651.291,18,461,false +cdb-aligned-devframe-before,devframe,ref,browser.batch,930.393,133,610,false +cdb-aligned-devframe-before,devframe,ref,browser.fill,108.655,56,56,false +cdb-aligned-devframe-before,devframe,ref,browser.click,111.568,31,57,false +cdb-aligned-devframe-before,devframe,ref,browser.snapshot,1012.675,18,461,false +cdb-aligned-devframe-before,devframe,ref,browser.batch,791.056,133,610,false +cdb-aligned-devframe-before,devframe,ref,browser.fill,130.655,56,56,false +cdb-aligned-devframe-before,devframe,ref,browser.click,90.408,31,57,false +cdb-aligned-devframe-before,devframe,ref,browser.snapshot,615.943,18,461,false +cdb-aligned-devframe-before,devframe,ref,browser.batch,786.404,133,610,false +cdb-aligned-devframe-before,devframe,ref,browser.fill,98.144,56,56,false +cdb-aligned-devframe-before,devframe,ref,browser.click,83.996,31,57,false +cdb-aligned-devframe-before,devframe,ref,browser.snapshot,613.391,18,461,false +cdb-aligned-devframe-before,devframe,ref,browser.batch,761.260,133,610,false +cdb-aligned-devframe-before,devframe,ref,browser.fill,96.506,56,56,false +cdb-aligned-devframe-before,devframe,ref,browser.click,87.619,31,57,false +cdb-aligned-devframe-before,devframe,ref,browser.snapshot,634.891,18,461,false +cdb-aligned-devframe-before,devframe,ref,browser.batch,801.653,133,610,false +cdb-aligned-devframe-before,devframe,ref,browser.fill,91.649,56,56,false +cdb-aligned-devframe-before,devframe,ref,browser.click,87.699,31,57,false +cdb-aligned-devframe-before,devframe,ref,browser.snapshot,594.566,18,461,false +cdb-aligned-devframe-before,devframe,ref,browser.batch,734.554,133,610,false +cdb-aligned-devframe-before,devframe,ref,browser.fill,89.940,56,56,false +cdb-aligned-devframe-before,devframe,ref,browser.click,106.134,31,57,false +cdb-aligned-devframe-before,devframe,ref,browser.snapshot,608.549,18,461,false +cdb-aligned-devframe-before,devframe,ref,browser.batch,756.881,133,610,false +cdb-aligned-devframe-before,devframe,ref,browser.fill,93.339,56,56,false +cdb-aligned-devframe-before,devframe,ref,browser.click,85.273,31,57,false +cdb-aligned-devframe-before,devframe,ref,browser.snapshot,600.843,18,461,false +cdb-aligned-devframe-before,devframe,ref,browser.batch,795.403,133,610,false +cdb-aligned-devframe-before,devframe,ref,browser.fill,94.342,56,56,false +cdb-aligned-devframe-before,devframe,ref,browser.click,92.417,31,57,false +cdb-aligned-devframe-before,devframe,ref,browser.snapshot,619.168,18,461,false +cdb-aligned-devframe-before,devframe,ref,browser.batch,772.628,133,610,false +cdb-aligned-devframe-before,devframe,ref,browser.fill,96.869,56,56,false +cdb-aligned-devframe-before,devframe,ref,browser.click,83.843,31,57,false +cdb-aligned-devframe-before,devframe,ref,browser.snapshot,613.331,18,461,false +cdb-aligned-devframe-before,devframe,ref,browser.batch,746.664,133,610,false +cdb-aligned-devframe-before,devframe,ref,browser.fill,88.428,56,56,false +cdb-aligned-devframe-before,devframe,ref,browser.click,82.767,31,57,false +cdb-aligned-devframe-before,devframe,ref,browser.snapshot,573.358,18,461,false +cdb-aligned-devframe-before,devframe,ref,browser.batch,726.509,133,610,false +cdb-aligned-devframe-before,devframe,ref,browser.fill,93.237,56,56,false +cdb-aligned-devframe-before,devframe,ref,browser.click,88.080,31,57,false +cdb-aligned-devframe-before,devframe,ref,browser.snapshot,565.703,18,461,false +cdb-aligned-devframe-before,devframe,ref,browser.batch,745.814,133,610,false +cdb-aligned-devframe-before,devframe,ref,browser.fill,93.573,56,56,false +cdb-aligned-devframe-before,devframe,ref,browser.click,89.406,31,57,false +cdb-aligned-devframe-before,devframe,ref,browser.snapshot,572.814,18,461,false +cdb-aligned-devframe-before,devframe,ref,browser.batch,720.655,133,610,false +cdb-aligned-devframe-before,devframe,ref,browser.fill,98.156,56,56,false +cdb-aligned-devframe-before,devframe,ref,browser.click,82.908,31,57,false +cdb-aligned-devframe-before,devframe,ref,browser.snapshot,589.519,18,461,false +cdb-aligned-devframe-before,devframe,ref,browser.batch,715.673,133,610,false +cdb-aligned-devframe-before,devframe,ref,browser.fill,89.456,56,56,false +cdb-aligned-devframe-before,devframe,ref,browser.click,82.304,31,57,false +cdb-aligned-devframe-before,devframe,ref,browser.snapshot,567.874,18,461,false +cdb-aligned-devframe-before,devframe,ref,browser.batch,732.491,133,610,false +cdb-aligned-devframe-before,devframe,ref,browser.fill,89.008,56,56,false +cdb-aligned-devframe-before,devframe,ref,browser.click,84.414,31,57,false +cdb-aligned-devframe-before,devframe,ref,browser.snapshot,580.061,18,461,false +cdb-aligned-devframe-before,devframe,ref,browser.batch,726.630,133,610,false +cdb-aligned-devframe-before,devframe,ref,browser.fill,89.356,56,56,false +cdb-aligned-devframe-before,devframe,ref,browser.click,86.130,31,57,false +cdb-aligned-devframe-before,devframe,ref,browser.snapshot,565.131,18,461,false +cdb-aligned-devframe-before,devframe,ref,browser.batch,746.996,133,610,false +cdb-aligned-devframe-before,devframe,ref,browser.fill,93.723,56,56,false +cdb-aligned-devframe-before,devframe,ref,browser.click,88.652,31,57,false +cdb-aligned-devframe-before,devframe,ref,browser.snapshot,594.171,18,461,false +cdb-aligned-devframe-before,devframe,ref,browser.batch,740.821,133,610,false +cdb-aligned-devframe-before,devframe,ref,browser.fill,94.555,56,56,false +cdb-aligned-devframe-before,devframe,ref,browser.click,84.390,31,57,false +cdb-aligned-devframe-before,devframe,ref,browser.snapshot,578.605,18,461,false +cdb-aligned-devframe-before,devframe,ref,browser.batch,726.696,133,610,false +cdb-aligned-devframe-before,devframe,ref,browser.fill,88.792,56,56,false +cdb-aligned-devframe-before,devframe,ref,browser.click,82.149,31,57,false +cdb-aligned-devframe-before,devframe,ref,browser.snapshot,581.087,18,461,false +cdb-aligned-devframe-before,devframe,ref,browser.batch,717.663,133,610,false +cdb-aligned-devframe-before,devframe,ref,browser.click:blocked,2005.463,31,197,true +cdb-aligned-devframe-before,devframe,ref,browser.click:blocked,2004.453,31,197,true +cdb-aligned-devframe-before,devframe,ref,browser.click:blocked,2003.356,31,197,true +cdb-aligned-devframe-before,devframe,ref,browser.click:blocked,2002.313,31,197,true +cdb-aligned-devframe-before,devframe,ref,browser.click:blocked,2005.888,31,197,true +cdb-aligned-devframe-before,devframe,ref,browser.click:blocked,2003.654,31,197,true +cdb-aligned-devframe-before,devframe,ref,browser.click:blocked,2008.438,31,197,true +cdb-aligned-devframe-before,devframe,ref,browser.click:blocked,2004.979,31,197,true +cdb-aligned-devframe-before,devframe,ref,browser.click:blocked,2004.797,31,197,true +cdb-aligned-devframe-before,devframe,ref,browser.click:blocked,2010.147,31,197,true +cdb-aligned-devframe-before,devframe,ref,browser.click:blocked,2004.570,31,197,true +cdb-aligned-devframe-before,devframe,ref,browser.click:blocked,2003.098,31,197,true +cdb-aligned-devframe-before,devframe,ref,browser.click:blocked,2006.892,31,197,true +cdb-aligned-devframe-before,devframe,ref,browser.click:blocked,2003.687,31,197,true +cdb-aligned-devframe-before,devframe,ref,browser.click:blocked,2004.773,31,197,true +cdb-aligned-devframe-before,devframe,ref,browser.click:blocked,2001.920,31,197,true +cdb-aligned-devframe-before,devframe,ref,browser.click:blocked,2005.558,31,197,true +cdb-aligned-devframe-before,devframe,ref,browser.click:blocked,2005.765,31,197,true +cdb-aligned-devframe-before,devframe,ref,browser.click:blocked,2001.628,31,197,true +cdb-aligned-devframe-before,devframe,ref,browser.click:blocked,2005.079,31,197,true +cdb-aligned-devframe-before,devframe,ref,browser.click:blocked,2009.399,31,197,true +cdb-aligned-devframe-before,devframe,ref,browser.click:blocked,2005.163,31,197,true +cdb-aligned-devframe-before,devframe,ref,browser.click:blocked,2002.479,31,197,true +cdb-aligned-devframe-before,devframe,ref,browser.click:blocked,2006.422,31,197,true +cdb-aligned-devframe-before,devframe,ref,browser.click:blocked,2002.785,31,197,true +cdb-aligned-devframe-before,devframe,ref,browser.click:blocked,2004.916,31,197,true +cdb-aligned-devframe-before,devframe,ref,browser.click:blocked,2004.614,31,197,true +cdb-aligned-devframe-before,devframe,ref,browser.click:blocked,2004.111,31,197,true +cdb-aligned-devframe-before,devframe,ref,browser.click:blocked,2001.204,31,197,true +cdb-aligned-devframe-before,devframe,ref,browser.click:blocked,2001.942,31,197,true +cdb-aligned-devframe-before,devframe,locator,browser.fill,307.098,118,56,false +cdb-aligned-devframe-before,devframe,locator,browser.click,446.050,97,57,false +cdb-aligned-devframe-before,devframe,locator,browser.snapshot,668.532,18,461,false +cdb-aligned-devframe-before,devframe,locator,browser.batch,1542.640,261,610,false +cdb-aligned-devframe-before,devframe,locator,browser.fill,294.148,118,56,false +cdb-aligned-devframe-before,devframe,locator,browser.click,265.277,97,57,false +cdb-aligned-devframe-before,devframe,locator,browser.snapshot,704.455,18,461,false +cdb-aligned-devframe-before,devframe,locator,browser.batch,1127.458,261,610,false +cdb-aligned-devframe-before,devframe,locator,browser.fill,274.874,118,56,false +cdb-aligned-devframe-before,devframe,locator,browser.click,333.387,97,57,false +cdb-aligned-devframe-before,devframe,locator,browser.snapshot,679.520,18,461,false +cdb-aligned-devframe-before,devframe,locator,browser.batch,1858.327,261,610,false +cdb-aligned-devframe-before,devframe,locator,browser.fill,322.065,118,56,false +cdb-aligned-devframe-before,devframe,locator,browser.click,348.484,97,57,false +cdb-aligned-devframe-before,devframe,locator,browser.snapshot,767.463,18,461,false +cdb-aligned-devframe-before,devframe,locator,browser.batch,1224.699,261,610,false +cdb-aligned-devframe-before,devframe,locator,browser.fill,345.412,118,56,false +cdb-aligned-devframe-before,devframe,locator,browser.click,456.265,97,57,false +cdb-aligned-devframe-before,devframe,locator,browser.snapshot,859.477,18,461,false +cdb-aligned-devframe-before,devframe,locator,browser.batch,1052.461,261,610,false +cdb-aligned-devframe-before,devframe,locator,browser.fill,324.846,118,56,false +cdb-aligned-devframe-before,devframe,locator,browser.click,339.914,97,57,false +cdb-aligned-devframe-before,devframe,locator,browser.snapshot,603.646,18,461,false +cdb-aligned-devframe-before,devframe,locator,browser.batch,1119.891,261,610,false +cdb-aligned-devframe-before,devframe,locator,browser.fill,364.026,118,56,false +cdb-aligned-devframe-before,devframe,locator,browser.click,291.372,97,57,false +cdb-aligned-devframe-before,devframe,locator,browser.snapshot,695.998,18,461,false +cdb-aligned-devframe-before,devframe,locator,browser.batch,1121.545,261,610,false +cdb-aligned-devframe-before,devframe,locator,browser.fill,344.368,118,56,false +cdb-aligned-devframe-before,devframe,locator,browser.click,275.336,97,57,false +cdb-aligned-devframe-before,devframe,locator,browser.snapshot,590.111,18,461,false +cdb-aligned-devframe-before,devframe,locator,browser.batch,1085.547,261,610,false +cdb-aligned-devframe-before,devframe,locator,browser.fill,331.611,118,56,false +cdb-aligned-devframe-before,devframe,locator,browser.click,349.221,97,57,false +cdb-aligned-devframe-before,devframe,locator,browser.snapshot,567.709,18,461,false +cdb-aligned-devframe-before,devframe,locator,browser.batch,1261.126,261,610,false +cdb-aligned-devframe-before,devframe,locator,browser.fill,437.917,118,56,false +cdb-aligned-devframe-before,devframe,locator,browser.click,441.670,97,57,false +cdb-aligned-devframe-before,devframe,locator,browser.snapshot,614.084,18,461,false +cdb-aligned-devframe-before,devframe,locator,browser.batch,1278.103,261,610,false +cdb-aligned-devframe-before,devframe,locator,browser.fill,357.383,118,56,false +cdb-aligned-devframe-before,devframe,locator,browser.click,370.424,97,57,false +cdb-aligned-devframe-before,devframe,locator,browser.snapshot,571.920,18,461,false +cdb-aligned-devframe-before,devframe,locator,browser.batch,1229.091,261,610,false +cdb-aligned-devframe-before,devframe,locator,browser.fill,323.656,118,56,false +cdb-aligned-devframe-before,devframe,locator,browser.click,355.424,97,57,false +cdb-aligned-devframe-before,devframe,locator,browser.snapshot,611.128,18,461,false +cdb-aligned-devframe-before,devframe,locator,browser.batch,1291.342,261,610,false +cdb-aligned-devframe-before,devframe,locator,browser.fill,406.162,118,56,false +cdb-aligned-devframe-before,devframe,locator,browser.click,344.034,97,57,false +cdb-aligned-devframe-before,devframe,locator,browser.snapshot,581.763,18,461,false +cdb-aligned-devframe-before,devframe,locator,browser.batch,1293.550,261,610,false +cdb-aligned-devframe-before,devframe,locator,browser.fill,424.984,118,56,false +cdb-aligned-devframe-before,devframe,locator,browser.click,456.078,97,57,false +cdb-aligned-devframe-before,devframe,locator,browser.snapshot,623.690,18,461,false +cdb-aligned-devframe-before,devframe,locator,browser.batch,1126.250,261,610,false +cdb-aligned-devframe-before,devframe,locator,browser.fill,415.438,118,56,false +cdb-aligned-devframe-before,devframe,locator,browser.click,376.528,97,57,false +cdb-aligned-devframe-before,devframe,locator,browser.snapshot,632.152,18,461,false +cdb-aligned-devframe-before,devframe,locator,browser.batch,1265.944,261,610,false +cdb-aligned-devframe-before,devframe,locator,browser.fill,433.839,118,56,false +cdb-aligned-devframe-before,devframe,locator,browser.click,374.878,97,57,false +cdb-aligned-devframe-before,devframe,locator,browser.snapshot,633.119,18,461,false +cdb-aligned-devframe-before,devframe,locator,browser.batch,1149.099,261,610,false +cdb-aligned-devframe-before,devframe,locator,browser.fill,326.585,118,56,false +cdb-aligned-devframe-before,devframe,locator,browser.click,407.169,97,57,false +cdb-aligned-devframe-before,devframe,locator,browser.snapshot,646.519,18,461,false +cdb-aligned-devframe-before,devframe,locator,browser.batch,1285.977,261,610,false +cdb-aligned-devframe-before,devframe,locator,browser.fill,429.891,118,56,false +cdb-aligned-devframe-before,devframe,locator,browser.click,438.233,97,57,false +cdb-aligned-devframe-before,devframe,locator,browser.snapshot,631.215,18,461,false +cdb-aligned-devframe-before,devframe,locator,browser.batch,1215.314,261,610,false +cdb-aligned-devframe-before,devframe,locator,browser.fill,420.812,118,56,false +cdb-aligned-devframe-before,devframe,locator,browser.click,432.174,97,57,false +cdb-aligned-devframe-before,devframe,locator,browser.snapshot,664.959,18,461,false +cdb-aligned-devframe-before,devframe,locator,browser.batch,1257.799,261,610,false +cdb-aligned-devframe-before,devframe,locator,browser.fill,326.526,118,56,false +cdb-aligned-devframe-before,devframe,locator,browser.click,432.816,97,57,false +cdb-aligned-devframe-before,devframe,locator,browser.snapshot,624.788,18,461,false +cdb-aligned-devframe-before,devframe,locator,browser.batch,1251.837,261,610,false +cdb-aligned-devframe-before,devframe,locator,browser.fill,371.122,118,56,false +cdb-aligned-devframe-before,devframe,locator,browser.click,336.903,97,57,false +cdb-aligned-devframe-before,devframe,locator,browser.snapshot,592.021,18,461,false +cdb-aligned-devframe-before,devframe,locator,browser.batch,1330.396,261,610,false +cdb-aligned-devframe-before,devframe,locator,browser.fill,332.089,118,56,false +cdb-aligned-devframe-before,devframe,locator,browser.click,273.460,97,57,false +cdb-aligned-devframe-before,devframe,locator,browser.snapshot,663.480,18,461,false +cdb-aligned-devframe-before,devframe,locator,browser.batch,1251.497,261,610,false +cdb-aligned-devframe-before,devframe,locator,browser.fill,322.947,118,56,false +cdb-aligned-devframe-before,devframe,locator,browser.click,340.396,97,57,false +cdb-aligned-devframe-before,devframe,locator,browser.snapshot,625.137,18,461,false +cdb-aligned-devframe-before,devframe,locator,browser.batch,1168.285,261,610,false +cdb-aligned-devframe-before,devframe,locator,browser.fill,537.405,118,56,false +cdb-aligned-devframe-before,devframe,locator,browser.click,290.459,97,57,false +cdb-aligned-devframe-before,devframe,locator,browser.snapshot,643.662,18,461,false +cdb-aligned-devframe-before,devframe,locator,browser.batch,1122.630,261,610,false +cdb-aligned-devframe-before,devframe,locator,browser.fill,331.875,118,56,false +cdb-aligned-devframe-before,devframe,locator,browser.click,297.636,97,57,false +cdb-aligned-devframe-before,devframe,locator,browser.snapshot,601.933,18,461,false +cdb-aligned-devframe-before,devframe,locator,browser.batch,1289.874,261,610,false +cdb-aligned-devframe-before,devframe,locator,browser.fill,450.520,118,56,false +cdb-aligned-devframe-before,devframe,locator,browser.click,296.028,97,57,false +cdb-aligned-devframe-before,devframe,locator,browser.snapshot,729.033,18,461,false +cdb-aligned-devframe-before,devframe,locator,browser.batch,1079.317,261,610,false +cdb-aligned-devframe-before,devframe,locator,browser.fill,349.810,118,56,false +cdb-aligned-devframe-before,devframe,locator,browser.click,424.241,97,57,false +cdb-aligned-devframe-before,devframe,locator,browser.snapshot,672.811,18,461,false +cdb-aligned-devframe-before,devframe,locator,browser.batch,1380.985,261,610,false +cdb-aligned-devframe-before,devframe,locator,browser.fill,298.762,118,56,false +cdb-aligned-devframe-before,devframe,locator,browser.click,381.518,97,57,false +cdb-aligned-devframe-before,devframe,locator,browser.snapshot,673.477,18,461,false +cdb-aligned-devframe-before,devframe,locator,browser.batch,1276.475,261,610,false +cdb-aligned-devframe-before,devframe,locator,browser.fill,349.092,118,56,false +cdb-aligned-devframe-before,devframe,locator,browser.click,315.707,97,57,false +cdb-aligned-devframe-before,devframe,locator,browser.snapshot,656.487,18,461,false +cdb-aligned-devframe-before,devframe,locator,browser.batch,1136.783,261,610,false +cdb-aligned-devframe-before,devframe,locator,browser.fill,295.324,118,56,false +cdb-aligned-devframe-before,devframe,locator,browser.click,285.077,97,57,false +cdb-aligned-devframe-before,devframe,locator,browser.snapshot,633.594,18,461,false +cdb-aligned-devframe-before,devframe,locator,browser.batch,1292.400,261,610,false +cdb-aligned-devframe-before,devframe,locator,browser.click:blocked,2026.332,97,197,true +cdb-aligned-devframe-before,devframe,locator,browser.click:blocked,2078.657,97,197,true +cdb-aligned-devframe-before,devframe,locator,browser.click:blocked,2166.388,97,197,true +cdb-aligned-devframe-before,devframe,locator,browser.click:blocked,2001.493,97,197,true +cdb-aligned-devframe-before,devframe,locator,browser.click:blocked,2060.925,97,197,true +cdb-aligned-devframe-before,devframe,locator,browser.click:blocked,2051.839,97,197,true +cdb-aligned-devframe-before,devframe,locator,browser.click:blocked,2001.442,97,197,true +cdb-aligned-devframe-before,devframe,locator,browser.click:blocked,2000.921,97,197,true +cdb-aligned-devframe-before,devframe,locator,browser.click:blocked,2000.384,97,197,true +cdb-aligned-devframe-before,devframe,locator,browser.click:blocked,2001.923,97,197,true +cdb-aligned-devframe-before,devframe,locator,browser.click:blocked,2003.992,97,197,true +cdb-aligned-devframe-before,devframe,locator,browser.click:blocked,2002.736,97,197,true +cdb-aligned-devframe-before,devframe,locator,browser.click:blocked,2000.461,97,197,true +cdb-aligned-devframe-before,devframe,locator,browser.click:blocked,2003.326,97,197,true +cdb-aligned-devframe-before,devframe,locator,browser.click:blocked,2003.266,97,197,true +cdb-aligned-devframe-before,devframe,locator,browser.click:blocked,2000.196,97,197,true +cdb-aligned-devframe-before,devframe,locator,browser.click:blocked,2004.421,97,197,true +cdb-aligned-devframe-before,devframe,locator,browser.click:blocked,2002.615,97,197,true +cdb-aligned-devframe-before,devframe,locator,browser.click:blocked,2032.387,97,197,true +cdb-aligned-devframe-before,devframe,locator,browser.click:blocked,2013.513,97,197,true +cdb-aligned-devframe-before,devframe,locator,browser.click:blocked,2005.347,97,197,true +cdb-aligned-devframe-before,devframe,locator,browser.click:blocked,2047.933,97,197,true +cdb-aligned-devframe-before,devframe,locator,browser.click:blocked,2137.193,97,197,true +cdb-aligned-devframe-before,devframe,locator,browser.click:blocked,2003.621,97,197,true +cdb-aligned-devframe-before,devframe,locator,browser.click:blocked,2046.911,97,197,true +cdb-aligned-devframe-before,devframe,locator,browser.click:blocked,2002.456,97,197,true +cdb-aligned-devframe-before,devframe,locator,browser.click:blocked,2003.009,97,197,true +cdb-aligned-devframe-before,devframe,locator,browser.click:blocked,2000.290,97,197,true +cdb-aligned-devframe-before,devframe,locator,browser.click:blocked,2001.732,97,197,true +cdb-aligned-devframe-before,devframe,locator,browser.click:blocked,2004.124,97,197,true +cdb-aligned-websocket-final,websocket,ref,browser.fill,125.710,55,56,false +cdb-aligned-websocket-final,websocket,ref,browser.click,102.587,30,57,false +cdb-aligned-websocket-final,websocket,ref,browser.snapshot,643.090,18,456,false +cdb-aligned-websocket-final,websocket,ref,browser.batch,761.473,131,605,false +cdb-aligned-websocket-final,websocket,ref,browser.fill,101.112,55,56,false +cdb-aligned-websocket-final,websocket,ref,browser.click,96.903,30,57,false +cdb-aligned-websocket-final,websocket,ref,browser.snapshot,636.080,18,456,false +cdb-aligned-websocket-final,websocket,ref,browser.batch,934.368,131,605,false +cdb-aligned-websocket-final,websocket,ref,browser.fill,97.252,55,56,false +cdb-aligned-websocket-final,websocket,ref,browser.click,99.629,30,57,false +cdb-aligned-websocket-final,websocket,ref,browser.snapshot,636.894,18,456,false +cdb-aligned-websocket-final,websocket,ref,browser.batch,782.730,131,605,false +cdb-aligned-websocket-final,websocket,ref,browser.fill,96.522,55,56,false +cdb-aligned-websocket-final,websocket,ref,browser.click,98.351,30,57,false +cdb-aligned-websocket-final,websocket,ref,browser.snapshot,657.567,18,456,false +cdb-aligned-websocket-final,websocket,ref,browser.batch,894.459,131,605,false +cdb-aligned-websocket-final,websocket,ref,browser.fill,103.947,55,56,false +cdb-aligned-websocket-final,websocket,ref,browser.click,121.643,30,57,false +cdb-aligned-websocket-final,websocket,ref,browser.snapshot,636.255,18,457,false +cdb-aligned-websocket-final,websocket,ref,browser.batch,908.680,132,610,false +cdb-aligned-websocket-final,websocket,ref,browser.fill,104.533,56,56,false +cdb-aligned-websocket-final,websocket,ref,browser.click,93.928,31,57,false +cdb-aligned-websocket-final,websocket,ref,browser.snapshot,648.901,18,461,false +cdb-aligned-websocket-final,websocket,ref,browser.batch,789.784,133,610,false +cdb-aligned-websocket-final,websocket,ref,browser.fill,107.163,56,56,false +cdb-aligned-websocket-final,websocket,ref,browser.click,134.566,31,57,false +cdb-aligned-websocket-final,websocket,ref,browser.snapshot,652.533,18,461,false +cdb-aligned-websocket-final,websocket,ref,browser.batch,894.372,133,610,false +cdb-aligned-websocket-final,websocket,ref,browser.fill,110.137,56,56,false +cdb-aligned-websocket-final,websocket,ref,browser.click,93.737,31,57,false +cdb-aligned-websocket-final,websocket,ref,browser.snapshot,610.916,18,461,false +cdb-aligned-websocket-final,websocket,ref,browser.batch,787.507,133,610,false +cdb-aligned-websocket-final,websocket,ref,browser.fill,118.146,56,56,false +cdb-aligned-websocket-final,websocket,ref,browser.click,140.368,31,57,false +cdb-aligned-websocket-final,websocket,ref,browser.snapshot,649.870,18,461,false +cdb-aligned-websocket-final,websocket,ref,browser.batch,851.749,133,610,false +cdb-aligned-websocket-final,websocket,ref,browser.fill,107.676,56,56,false +cdb-aligned-websocket-final,websocket,ref,browser.click,91.018,31,57,false +cdb-aligned-websocket-final,websocket,ref,browser.snapshot,641.999,18,461,false +cdb-aligned-websocket-final,websocket,ref,browser.batch,783.138,133,610,false +cdb-aligned-websocket-final,websocket,ref,browser.fill,98.303,56,56,false +cdb-aligned-websocket-final,websocket,ref,browser.click,90.478,31,57,false +cdb-aligned-websocket-final,websocket,ref,browser.snapshot,605.817,18,461,false +cdb-aligned-websocket-final,websocket,ref,browser.batch,790.715,133,610,false +cdb-aligned-websocket-final,websocket,ref,browser.fill,99.515,56,56,false +cdb-aligned-websocket-final,websocket,ref,browser.click,93.240,31,57,false +cdb-aligned-websocket-final,websocket,ref,browser.snapshot,596.222,18,461,false +cdb-aligned-websocket-final,websocket,ref,browser.batch,795.898,133,610,false +cdb-aligned-websocket-final,websocket,ref,browser.fill,99.308,56,56,false +cdb-aligned-websocket-final,websocket,ref,browser.click,93.232,31,57,false +cdb-aligned-websocket-final,websocket,ref,browser.snapshot,650.366,18,461,false +cdb-aligned-websocket-final,websocket,ref,browser.batch,821.946,133,610,false +cdb-aligned-websocket-final,websocket,ref,browser.fill,101.138,56,56,false +cdb-aligned-websocket-final,websocket,ref,browser.click,91.005,31,57,false +cdb-aligned-websocket-final,websocket,ref,browser.snapshot,665.184,18,461,false +cdb-aligned-websocket-final,websocket,ref,browser.batch,752.382,133,610,false +cdb-aligned-websocket-final,websocket,ref,browser.fill,100.397,56,56,false +cdb-aligned-websocket-final,websocket,ref,browser.click,92.085,31,57,false +cdb-aligned-websocket-final,websocket,ref,browser.snapshot,589.777,18,461,false +cdb-aligned-websocket-final,websocket,ref,browser.batch,815.886,133,610,false +cdb-aligned-websocket-final,websocket,ref,browser.fill,116.241,56,56,false +cdb-aligned-websocket-final,websocket,ref,browser.click,91.539,31,57,false +cdb-aligned-websocket-final,websocket,ref,browser.snapshot,614.700,18,461,false +cdb-aligned-websocket-final,websocket,ref,browser.batch,847.624,133,610,false +cdb-aligned-websocket-final,websocket,ref,browser.fill,97.635,56,56,false +cdb-aligned-websocket-final,websocket,ref,browser.click,134.918,31,57,false +cdb-aligned-websocket-final,websocket,ref,browser.snapshot,691.625,18,461,false +cdb-aligned-websocket-final,websocket,ref,browser.batch,899.642,133,610,false +cdb-aligned-websocket-final,websocket,ref,browser.fill,108.843,56,56,false +cdb-aligned-websocket-final,websocket,ref,browser.click,132.810,31,57,false +cdb-aligned-websocket-final,websocket,ref,browser.snapshot,653.788,18,461,false +cdb-aligned-websocket-final,websocket,ref,browser.batch,863.976,133,610,false +cdb-aligned-websocket-final,websocket,ref,browser.fill,108.961,56,56,false +cdb-aligned-websocket-final,websocket,ref,browser.click,95.425,31,57,false +cdb-aligned-websocket-final,websocket,ref,browser.snapshot,711.863,18,461,false +cdb-aligned-websocket-final,websocket,ref,browser.batch,950.744,133,610,false +cdb-aligned-websocket-final,websocket,ref,browser.fill,97.588,56,56,false +cdb-aligned-websocket-final,websocket,ref,browser.click,93.612,31,57,false +cdb-aligned-websocket-final,websocket,ref,browser.snapshot,625.285,18,461,false +cdb-aligned-websocket-final,websocket,ref,browser.batch,770.658,133,610,false +cdb-aligned-websocket-final,websocket,ref,browser.fill,96.505,56,56,false +cdb-aligned-websocket-final,websocket,ref,browser.click,136.740,31,57,false +cdb-aligned-websocket-final,websocket,ref,browser.snapshot,650.293,18,461,false +cdb-aligned-websocket-final,websocket,ref,browser.batch,781.756,133,610,false +cdb-aligned-websocket-final,websocket,ref,browser.fill,132.347,56,56,false +cdb-aligned-websocket-final,websocket,ref,browser.click,93.288,31,57,false +cdb-aligned-websocket-final,websocket,ref,browser.snapshot,634.770,18,461,false +cdb-aligned-websocket-final,websocket,ref,browser.batch,931.025,133,610,false +cdb-aligned-websocket-final,websocket,ref,browser.fill,104.678,56,56,false +cdb-aligned-websocket-final,websocket,ref,browser.click,90.536,31,57,false +cdb-aligned-websocket-final,websocket,ref,browser.snapshot,618.921,18,461,false +cdb-aligned-websocket-final,websocket,ref,browser.batch,796.601,133,610,false +cdb-aligned-websocket-final,websocket,ref,browser.fill,99.963,56,56,false +cdb-aligned-websocket-final,websocket,ref,browser.click,89.815,31,57,false +cdb-aligned-websocket-final,websocket,ref,browser.snapshot,620.308,18,461,false +cdb-aligned-websocket-final,websocket,ref,browser.batch,791.375,133,610,false +cdb-aligned-websocket-final,websocket,ref,browser.fill,112.617,56,56,false +cdb-aligned-websocket-final,websocket,ref,browser.click,93.466,31,57,false +cdb-aligned-websocket-final,websocket,ref,browser.snapshot,658.628,18,461,false +cdb-aligned-websocket-final,websocket,ref,browser.batch,1116.960,133,610,false +cdb-aligned-websocket-final,websocket,ref,browser.fill,103.187,56,56,false +cdb-aligned-websocket-final,websocket,ref,browser.click,96.616,31,57,false +cdb-aligned-websocket-final,websocket,ref,browser.snapshot,648.176,18,461,false +cdb-aligned-websocket-final,websocket,ref,browser.batch,822.425,133,610,false +cdb-aligned-websocket-final,websocket,ref,browser.fill,99.033,56,56,false +cdb-aligned-websocket-final,websocket,ref,browser.click,89.423,31,57,false +cdb-aligned-websocket-final,websocket,ref,browser.snapshot,619.810,18,461,false +cdb-aligned-websocket-final,websocket,ref,browser.batch,804.675,133,610,false +cdb-aligned-websocket-final,websocket,ref,browser.fill,113.941,56,56,false +cdb-aligned-websocket-final,websocket,ref,browser.click,97.044,31,57,false +cdb-aligned-websocket-final,websocket,ref,browser.snapshot,679.276,18,461,false +cdb-aligned-websocket-final,websocket,ref,browser.batch,874.531,133,610,false +cdb-aligned-websocket-final,websocket,ref,browser.fill,150.116,56,56,false +cdb-aligned-websocket-final,websocket,ref,browser.click,96.505,31,57,false +cdb-aligned-websocket-final,websocket,ref,browser.snapshot,651.503,18,461,false +cdb-aligned-websocket-final,websocket,ref,browser.batch,812.727,133,610,false +cdb-aligned-websocket-final,websocket,ref,browser.fill,101.557,56,56,false +cdb-aligned-websocket-final,websocket,ref,browser.click,94.317,31,57,false +cdb-aligned-websocket-final,websocket,ref,browser.snapshot,623.638,18,461,false +cdb-aligned-websocket-final,websocket,ref,browser.batch,813.837,133,610,false +cdb-aligned-websocket-final,websocket,ref,browser.click:blocked,2000.811,31,172,true +cdb-aligned-websocket-final,websocket,ref,browser.click:blocked,2000.571,31,172,true +cdb-aligned-websocket-final,websocket,ref,browser.click:blocked,2001.031,31,172,true +cdb-aligned-websocket-final,websocket,ref,browser.click:blocked,2001.712,31,172,true +cdb-aligned-websocket-final,websocket,ref,browser.click:blocked,2001.002,31,172,true +cdb-aligned-websocket-final,websocket,ref,browser.click:blocked,2001.847,31,172,true +cdb-aligned-websocket-final,websocket,ref,browser.click:blocked,2001.669,31,172,true +cdb-aligned-websocket-final,websocket,ref,browser.click:blocked,2001.996,31,172,true +cdb-aligned-websocket-final,websocket,ref,browser.click:blocked,2001.776,31,172,true +cdb-aligned-websocket-final,websocket,ref,browser.click:blocked,2002.933,31,172,true +cdb-aligned-websocket-final,websocket,ref,browser.click:blocked,2002.023,31,172,true +cdb-aligned-websocket-final,websocket,ref,browser.click:blocked,2001.039,31,172,true +cdb-aligned-websocket-final,websocket,ref,browser.click:blocked,2000.864,31,172,true +cdb-aligned-websocket-final,websocket,ref,browser.click:blocked,2001.650,31,172,true +cdb-aligned-websocket-final,websocket,ref,browser.click:blocked,2000.767,31,172,true +cdb-aligned-websocket-final,websocket,ref,browser.click:blocked,2002.014,31,172,true +cdb-aligned-websocket-final,websocket,ref,browser.click:blocked,2001.880,31,172,true +cdb-aligned-websocket-final,websocket,ref,browser.click:blocked,2000.563,31,172,true +cdb-aligned-websocket-final,websocket,ref,browser.click:blocked,2000.807,31,172,true +cdb-aligned-websocket-final,websocket,ref,browser.click:blocked,2000.914,31,172,true +cdb-aligned-websocket-final,websocket,ref,browser.click:blocked,2001.849,31,172,true +cdb-aligned-websocket-final,websocket,ref,browser.click:blocked,2001.820,31,172,true +cdb-aligned-websocket-final,websocket,ref,browser.click:blocked,2001.684,31,172,true +cdb-aligned-websocket-final,websocket,ref,browser.click:blocked,2002.417,31,172,true +cdb-aligned-websocket-final,websocket,ref,browser.click:blocked,2001.985,31,172,true +cdb-aligned-websocket-final,websocket,ref,browser.click:blocked,2001.420,31,172,true +cdb-aligned-websocket-final,websocket,ref,browser.click:blocked,2002.013,31,172,true +cdb-aligned-websocket-final,websocket,ref,browser.click:blocked,2001.867,31,172,true +cdb-aligned-websocket-final,websocket,ref,browser.click:blocked,2001.530,31,172,true +cdb-aligned-websocket-final,websocket,ref,browser.click:blocked,2001.457,31,172,true +cdb-aligned-websocket-final,websocket,locator,browser.fill,256.794,118,56,false +cdb-aligned-websocket-final,websocket,locator,browser.click,602.280,97,57,false +cdb-aligned-websocket-final,websocket,locator,browser.snapshot,717.292,18,461,false +cdb-aligned-websocket-final,websocket,locator,browser.batch,1074.523,261,610,false +cdb-aligned-websocket-final,websocket,locator,browser.fill,203.307,118,56,false +cdb-aligned-websocket-final,websocket,locator,browser.click,201.543,97,57,false +cdb-aligned-websocket-final,websocket,locator,browser.snapshot,634.541,18,461,false +cdb-aligned-websocket-final,websocket,locator,browser.batch,893.878,261,610,false +cdb-aligned-websocket-final,websocket,locator,browser.fill,200.952,118,56,false +cdb-aligned-websocket-final,websocket,locator,browser.click,182.223,97,57,false +cdb-aligned-websocket-final,websocket,locator,browser.snapshot,575.174,18,461,false +cdb-aligned-websocket-final,websocket,locator,browser.batch,847.651,261,610,false +cdb-aligned-websocket-final,websocket,locator,browser.fill,188.445,118,56,false +cdb-aligned-websocket-final,websocket,locator,browser.click,170.307,97,57,false +cdb-aligned-websocket-final,websocket,locator,browser.snapshot,591.031,18,461,false +cdb-aligned-websocket-final,websocket,locator,browser.batch,966.351,261,610,false +cdb-aligned-websocket-final,websocket,locator,browser.fill,199.649,118,56,false +cdb-aligned-websocket-final,websocket,locator,browser.click,185.688,97,57,false +cdb-aligned-websocket-final,websocket,locator,browser.snapshot,620.812,18,461,false +cdb-aligned-websocket-final,websocket,locator,browser.batch,921.538,261,610,false +cdb-aligned-websocket-final,websocket,locator,browser.fill,187.456,118,56,false +cdb-aligned-websocket-final,websocket,locator,browser.click,172.897,97,57,false +cdb-aligned-websocket-final,websocket,locator,browser.snapshot,781.027,18,461,false +cdb-aligned-websocket-final,websocket,locator,browser.batch,1270.952,261,610,false +cdb-aligned-websocket-final,websocket,locator,browser.fill,210.310,118,56,false +cdb-aligned-websocket-final,websocket,locator,browser.click,211.076,97,57,false +cdb-aligned-websocket-final,websocket,locator,browser.snapshot,618.221,18,461,false +cdb-aligned-websocket-final,websocket,locator,browser.batch,899.880,261,610,false +cdb-aligned-websocket-final,websocket,locator,browser.fill,192.650,118,56,false +cdb-aligned-websocket-final,websocket,locator,browser.click,175.391,97,57,false +cdb-aligned-websocket-final,websocket,locator,browser.snapshot,595.797,18,461,false +cdb-aligned-websocket-final,websocket,locator,browser.batch,950.270,261,610,false +cdb-aligned-websocket-final,websocket,locator,browser.fill,284.535,118,56,false +cdb-aligned-websocket-final,websocket,locator,browser.click,222.219,97,57,false +cdb-aligned-websocket-final,websocket,locator,browser.snapshot,615.605,18,461,false +cdb-aligned-websocket-final,websocket,locator,browser.batch,934.534,261,610,false +cdb-aligned-websocket-final,websocket,locator,browser.fill,211.998,118,56,false +cdb-aligned-websocket-final,websocket,locator,browser.click,214.718,97,57,false +cdb-aligned-websocket-final,websocket,locator,browser.snapshot,666.219,18,461,false +cdb-aligned-websocket-final,websocket,locator,browser.batch,979.126,261,610,false +cdb-aligned-websocket-final,websocket,locator,browser.fill,184.274,118,56,false +cdb-aligned-websocket-final,websocket,locator,browser.click,189.235,97,57,false +cdb-aligned-websocket-final,websocket,locator,browser.snapshot,709.514,18,461,false +cdb-aligned-websocket-final,websocket,locator,browser.batch,895.532,261,610,false +cdb-aligned-websocket-final,websocket,locator,browser.fill,209.854,118,56,false +cdb-aligned-websocket-final,websocket,locator,browser.click,190.554,97,57,false +cdb-aligned-websocket-final,websocket,locator,browser.snapshot,612.038,18,461,false +cdb-aligned-websocket-final,websocket,locator,browser.batch,885.612,261,610,false +cdb-aligned-websocket-final,websocket,locator,browser.fill,193.010,118,56,false +cdb-aligned-websocket-final,websocket,locator,browser.click,179.371,97,57,false +cdb-aligned-websocket-final,websocket,locator,browser.snapshot,614.251,18,461,false +cdb-aligned-websocket-final,websocket,locator,browser.batch,885.574,261,610,false +cdb-aligned-websocket-final,websocket,locator,browser.fill,207.411,118,56,false +cdb-aligned-websocket-final,websocket,locator,browser.click,240.675,97,57,false +cdb-aligned-websocket-final,websocket,locator,browser.snapshot,649.766,18,461,false +cdb-aligned-websocket-final,websocket,locator,browser.batch,908.012,261,610,false +cdb-aligned-websocket-final,websocket,locator,browser.fill,195.954,118,56,false +cdb-aligned-websocket-final,websocket,locator,browser.click,204.538,97,57,false +cdb-aligned-websocket-final,websocket,locator,browser.snapshot,613.003,18,461,false +cdb-aligned-websocket-final,websocket,locator,browser.batch,878.021,261,610,false +cdb-aligned-websocket-final,websocket,locator,browser.fill,227.191,118,56,false +cdb-aligned-websocket-final,websocket,locator,browser.click,185.537,97,57,false +cdb-aligned-websocket-final,websocket,locator,browser.snapshot,633.970,18,461,false +cdb-aligned-websocket-final,websocket,locator,browser.batch,907.635,261,610,false +cdb-aligned-websocket-final,websocket,locator,browser.fill,205.674,118,56,false +cdb-aligned-websocket-final,websocket,locator,browser.click,194.097,97,57,false +cdb-aligned-websocket-final,websocket,locator,browser.snapshot,592.348,18,461,false +cdb-aligned-websocket-final,websocket,locator,browser.batch,867.610,261,610,false +cdb-aligned-websocket-final,websocket,locator,browser.fill,218.754,118,56,false +cdb-aligned-websocket-final,websocket,locator,browser.click,178.115,97,57,false +cdb-aligned-websocket-final,websocket,locator,browser.snapshot,626.267,18,461,false +cdb-aligned-websocket-final,websocket,locator,browser.batch,868.579,261,610,false +cdb-aligned-websocket-final,websocket,locator,browser.fill,189.357,118,56,false +cdb-aligned-websocket-final,websocket,locator,browser.click,182.839,97,57,false +cdb-aligned-websocket-final,websocket,locator,browser.snapshot,638.431,18,461,false +cdb-aligned-websocket-final,websocket,locator,browser.batch,923.786,261,610,false +cdb-aligned-websocket-final,websocket,locator,browser.fill,199.916,118,56,false +cdb-aligned-websocket-final,websocket,locator,browser.click,181.638,97,57,false +cdb-aligned-websocket-final,websocket,locator,browser.snapshot,614.112,18,461,false +cdb-aligned-websocket-final,websocket,locator,browser.batch,1025.136,261,610,false +cdb-aligned-websocket-final,websocket,locator,browser.fill,197.851,118,56,false +cdb-aligned-websocket-final,websocket,locator,browser.click,180.848,97,57,false +cdb-aligned-websocket-final,websocket,locator,browser.snapshot,623.590,18,461,false +cdb-aligned-websocket-final,websocket,locator,browser.batch,898.218,261,610,false +cdb-aligned-websocket-final,websocket,locator,browser.fill,189.515,118,56,false +cdb-aligned-websocket-final,websocket,locator,browser.click,193.180,97,57,false +cdb-aligned-websocket-final,websocket,locator,browser.snapshot,670.752,18,461,false +cdb-aligned-websocket-final,websocket,locator,browser.batch,1087.348,261,610,false +cdb-aligned-websocket-final,websocket,locator,browser.fill,192.539,118,56,false +cdb-aligned-websocket-final,websocket,locator,browser.click,179.636,97,57,false +cdb-aligned-websocket-final,websocket,locator,browser.snapshot,583.847,18,461,false +cdb-aligned-websocket-final,websocket,locator,browser.batch,893.445,261,610,false +cdb-aligned-websocket-final,websocket,locator,browser.fill,197.718,118,56,false +cdb-aligned-websocket-final,websocket,locator,browser.click,178.522,97,57,false +cdb-aligned-websocket-final,websocket,locator,browser.snapshot,602.867,18,461,false +cdb-aligned-websocket-final,websocket,locator,browser.batch,990.211,261,610,false +cdb-aligned-websocket-final,websocket,locator,browser.fill,201.075,118,56,false +cdb-aligned-websocket-final,websocket,locator,browser.click,178.606,97,57,false +cdb-aligned-websocket-final,websocket,locator,browser.snapshot,612.621,18,461,false +cdb-aligned-websocket-final,websocket,locator,browser.batch,919.138,261,610,false +cdb-aligned-websocket-final,websocket,locator,browser.fill,199.865,118,56,false +cdb-aligned-websocket-final,websocket,locator,browser.click,182.938,97,57,false +cdb-aligned-websocket-final,websocket,locator,browser.snapshot,632.192,18,461,false +cdb-aligned-websocket-final,websocket,locator,browser.batch,922.215,261,610,false +cdb-aligned-websocket-final,websocket,locator,browser.fill,196.468,118,56,false +cdb-aligned-websocket-final,websocket,locator,browser.click,186.570,97,57,false +cdb-aligned-websocket-final,websocket,locator,browser.snapshot,577.686,18,461,false +cdb-aligned-websocket-final,websocket,locator,browser.batch,873.323,261,610,false +cdb-aligned-websocket-final,websocket,locator,browser.fill,190.303,118,56,false +cdb-aligned-websocket-final,websocket,locator,browser.click,176.376,97,57,false +cdb-aligned-websocket-final,websocket,locator,browser.snapshot,594.695,18,461,false +cdb-aligned-websocket-final,websocket,locator,browser.batch,917.169,261,610,false +cdb-aligned-websocket-final,websocket,locator,browser.fill,195.796,118,56,false +cdb-aligned-websocket-final,websocket,locator,browser.click,171.049,97,57,false +cdb-aligned-websocket-final,websocket,locator,browser.snapshot,624.176,18,461,false +cdb-aligned-websocket-final,websocket,locator,browser.batch,928.746,261,610,false +cdb-aligned-websocket-final,websocket,locator,browser.fill,186.579,118,56,false +cdb-aligned-websocket-final,websocket,locator,browser.click,179.168,97,57,false +cdb-aligned-websocket-final,websocket,locator,browser.snapshot,594.746,18,461,false +cdb-aligned-websocket-final,websocket,locator,browser.batch,920.616,261,610,false +cdb-aligned-websocket-final,websocket,locator,browser.click:blocked,2001.022,97,172,true +cdb-aligned-websocket-final,websocket,locator,browser.click:blocked,2010.702,97,172,true +cdb-aligned-websocket-final,websocket,locator,browser.click:blocked,2015.405,97,172,true +cdb-aligned-websocket-final,websocket,locator,browser.click:blocked,2034.893,97,172,true +cdb-aligned-websocket-final,websocket,locator,browser.click:blocked,2002.248,97,172,true +cdb-aligned-websocket-final,websocket,locator,browser.click:blocked,2008.607,97,172,true +cdb-aligned-websocket-final,websocket,locator,browser.click:blocked,2001.469,97,172,true +cdb-aligned-websocket-final,websocket,locator,browser.click:blocked,2000.179,97,172,true +cdb-aligned-websocket-final,websocket,locator,browser.click:blocked,2001.383,97,172,true +cdb-aligned-websocket-final,websocket,locator,browser.click:blocked,2001.007,97,172,true +cdb-aligned-websocket-final,websocket,locator,browser.click:blocked,2000.695,97,172,true +cdb-aligned-websocket-final,websocket,locator,browser.click:blocked,2001.795,97,172,true +cdb-aligned-websocket-final,websocket,locator,browser.click:blocked,2000.216,97,172,true +cdb-aligned-websocket-final,websocket,locator,browser.click:blocked,2001.291,97,172,true +cdb-aligned-websocket-final,websocket,locator,browser.click:blocked,2000.902,97,172,true +cdb-aligned-websocket-final,websocket,locator,browser.click:blocked,2001.941,97,172,true +cdb-aligned-websocket-final,websocket,locator,browser.click:blocked,2001.171,97,172,true +cdb-aligned-websocket-final,websocket,locator,browser.click:blocked,2000.859,97,172,true +cdb-aligned-websocket-final,websocket,locator,browser.click:blocked,2000.161,97,172,true +cdb-aligned-websocket-final,websocket,locator,browser.click:blocked,2002.201,97,172,true +cdb-aligned-websocket-final,websocket,locator,browser.click:blocked,2001.783,97,172,true +cdb-aligned-websocket-final,websocket,locator,browser.click:blocked,2019.602,97,172,true +cdb-aligned-websocket-final,websocket,locator,browser.click:blocked,2007.508,97,172,true +cdb-aligned-websocket-final,websocket,locator,browser.click:blocked,2000.628,97,172,true +cdb-aligned-websocket-final,websocket,locator,browser.click:blocked,2001.322,97,172,true +cdb-aligned-websocket-final,websocket,locator,browser.click:blocked,2002.131,97,172,true +cdb-aligned-websocket-final,websocket,locator,browser.click:blocked,2001.335,97,172,true +cdb-aligned-websocket-final,websocket,locator,browser.click:blocked,2000.018,97,172,true +cdb-aligned-websocket-final,websocket,locator,browser.click:blocked,2000.776,97,172,true +cdb-aligned-websocket-final,websocket,locator,browser.click:blocked,2001.525,97,172,true +cdb-aligned-devframe-final,devframe,ref,browser.fill,88.018,55,56,false +cdb-aligned-devframe-final,devframe,ref,browser.click,82.298,30,57,false +cdb-aligned-devframe-final,devframe,ref,browser.snapshot,603.242,18,456,false +cdb-aligned-devframe-final,devframe,ref,browser.batch,731.865,131,605,false +cdb-aligned-devframe-final,devframe,ref,browser.fill,96.004,55,56,false +cdb-aligned-devframe-final,devframe,ref,browser.click,83.228,30,57,false +cdb-aligned-devframe-final,devframe,ref,browser.snapshot,572.510,18,456,false +cdb-aligned-devframe-final,devframe,ref,browser.batch,745.158,131,605,false +cdb-aligned-devframe-final,devframe,ref,browser.fill,96.372,55,56,false +cdb-aligned-devframe-final,devframe,ref,browser.click,85.345,30,57,false +cdb-aligned-devframe-final,devframe,ref,browser.snapshot,601.268,18,456,false +cdb-aligned-devframe-final,devframe,ref,browser.batch,718.126,131,605,false +cdb-aligned-devframe-final,devframe,ref,browser.fill,86.163,55,56,false +cdb-aligned-devframe-final,devframe,ref,browser.click,84.152,30,57,false +cdb-aligned-devframe-final,devframe,ref,browser.snapshot,590.813,18,456,false +cdb-aligned-devframe-final,devframe,ref,browser.batch,717.286,131,605,false +cdb-aligned-devframe-final,devframe,ref,browser.fill,89.482,55,56,false +cdb-aligned-devframe-final,devframe,ref,browser.click,84.672,30,57,false +cdb-aligned-devframe-final,devframe,ref,browser.snapshot,605.309,18,457,false +cdb-aligned-devframe-final,devframe,ref,browser.batch,758.002,132,610,false +cdb-aligned-devframe-final,devframe,ref,browser.fill,94.520,56,56,false +cdb-aligned-devframe-final,devframe,ref,browser.click,89.802,31,57,false +cdb-aligned-devframe-final,devframe,ref,browser.snapshot,653.743,18,461,false +cdb-aligned-devframe-final,devframe,ref,browser.batch,712.995,133,610,false +cdb-aligned-devframe-final,devframe,ref,browser.fill,94.071,56,56,false +cdb-aligned-devframe-final,devframe,ref,browser.click,87.562,31,57,false +cdb-aligned-devframe-final,devframe,ref,browser.snapshot,722.050,18,461,false +cdb-aligned-devframe-final,devframe,ref,browser.batch,744.758,133,610,false +cdb-aligned-devframe-final,devframe,ref,browser.fill,94.699,56,56,false +cdb-aligned-devframe-final,devframe,ref,browser.click,82.504,31,57,false +cdb-aligned-devframe-final,devframe,ref,browser.snapshot,634.316,18,461,false +cdb-aligned-devframe-final,devframe,ref,browser.batch,724.557,133,610,false +cdb-aligned-devframe-final,devframe,ref,browser.fill,135.901,56,56,false +cdb-aligned-devframe-final,devframe,ref,browser.click,104.579,31,57,false +cdb-aligned-devframe-final,devframe,ref,browser.snapshot,648.629,18,461,false +cdb-aligned-devframe-final,devframe,ref,browser.batch,842.125,133,610,false +cdb-aligned-devframe-final,devframe,ref,browser.fill,94.424,56,56,false +cdb-aligned-devframe-final,devframe,ref,browser.click,86.297,31,57,false +cdb-aligned-devframe-final,devframe,ref,browser.snapshot,696.299,18,461,false +cdb-aligned-devframe-final,devframe,ref,browser.batch,742.042,133,610,false +cdb-aligned-devframe-final,devframe,ref,browser.fill,95.025,56,56,false +cdb-aligned-devframe-final,devframe,ref,browser.click,84.823,31,57,false +cdb-aligned-devframe-final,devframe,ref,browser.snapshot,605.350,18,461,false +cdb-aligned-devframe-final,devframe,ref,browser.batch,726.580,133,610,false +cdb-aligned-devframe-final,devframe,ref,browser.fill,86.780,56,56,false +cdb-aligned-devframe-final,devframe,ref,browser.click,83.910,31,57,false +cdb-aligned-devframe-final,devframe,ref,browser.snapshot,609.939,18,461,false +cdb-aligned-devframe-final,devframe,ref,browser.batch,840.831,133,610,false +cdb-aligned-devframe-final,devframe,ref,browser.fill,90.771,56,56,false +cdb-aligned-devframe-final,devframe,ref,browser.click,100.282,31,57,false +cdb-aligned-devframe-final,devframe,ref,browser.snapshot,614.312,18,461,false +cdb-aligned-devframe-final,devframe,ref,browser.batch,727.900,133,610,false +cdb-aligned-devframe-final,devframe,ref,browser.fill,96.800,56,56,false +cdb-aligned-devframe-final,devframe,ref,browser.click,83.378,31,57,false +cdb-aligned-devframe-final,devframe,ref,browser.snapshot,614.054,18,461,false +cdb-aligned-devframe-final,devframe,ref,browser.batch,750.327,133,610,false +cdb-aligned-devframe-final,devframe,ref,browser.fill,95.509,56,56,false +cdb-aligned-devframe-final,devframe,ref,browser.click,83.715,31,57,false +cdb-aligned-devframe-final,devframe,ref,browser.snapshot,645.982,18,461,false +cdb-aligned-devframe-final,devframe,ref,browser.batch,805.162,133,610,false +cdb-aligned-devframe-final,devframe,ref,browser.fill,100.283,56,56,false +cdb-aligned-devframe-final,devframe,ref,browser.click,93.447,31,57,false +cdb-aligned-devframe-final,devframe,ref,browser.snapshot,637.871,18,461,false +cdb-aligned-devframe-final,devframe,ref,browser.batch,766.388,133,610,false +cdb-aligned-devframe-final,devframe,ref,browser.fill,94.958,56,56,false +cdb-aligned-devframe-final,devframe,ref,browser.click,99.695,31,57,false +cdb-aligned-devframe-final,devframe,ref,browser.snapshot,669.262,18,461,false +cdb-aligned-devframe-final,devframe,ref,browser.batch,761.252,133,610,false +cdb-aligned-devframe-final,devframe,ref,browser.fill,94.869,56,56,false +cdb-aligned-devframe-final,devframe,ref,browser.click,83.617,31,57,false +cdb-aligned-devframe-final,devframe,ref,browser.snapshot,583.434,18,461,false +cdb-aligned-devframe-final,devframe,ref,browser.batch,792.972,133,610,false +cdb-aligned-devframe-final,devframe,ref,browser.fill,92.929,56,56,false +cdb-aligned-devframe-final,devframe,ref,browser.click,82.606,31,57,false +cdb-aligned-devframe-final,devframe,ref,browser.snapshot,595.436,18,461,false +cdb-aligned-devframe-final,devframe,ref,browser.batch,716.975,133,610,false +cdb-aligned-devframe-final,devframe,ref,browser.fill,87.617,56,56,false +cdb-aligned-devframe-final,devframe,ref,browser.click,79.033,31,57,false +cdb-aligned-devframe-final,devframe,ref,browser.snapshot,599.608,18,461,false +cdb-aligned-devframe-final,devframe,ref,browser.batch,719.685,133,610,false +cdb-aligned-devframe-final,devframe,ref,browser.fill,94.224,56,56,false +cdb-aligned-devframe-final,devframe,ref,browser.click,87.863,31,57,false +cdb-aligned-devframe-final,devframe,ref,browser.snapshot,585.709,18,461,false +cdb-aligned-devframe-final,devframe,ref,browser.batch,778.706,133,610,false +cdb-aligned-devframe-final,devframe,ref,browser.fill,100.268,56,56,false +cdb-aligned-devframe-final,devframe,ref,browser.click,99.297,31,57,false +cdb-aligned-devframe-final,devframe,ref,browser.snapshot,584.344,18,461,false +cdb-aligned-devframe-final,devframe,ref,browser.batch,786.607,133,610,false +cdb-aligned-devframe-final,devframe,ref,browser.fill,91.816,56,56,false +cdb-aligned-devframe-final,devframe,ref,browser.click,123.683,31,57,false +cdb-aligned-devframe-final,devframe,ref,browser.snapshot,721.600,18,461,false +cdb-aligned-devframe-final,devframe,ref,browser.batch,753.618,133,610,false +cdb-aligned-devframe-final,devframe,ref,browser.fill,95.610,56,56,false +cdb-aligned-devframe-final,devframe,ref,browser.click,86.497,31,57,false +cdb-aligned-devframe-final,devframe,ref,browser.snapshot,600.570,18,461,false +cdb-aligned-devframe-final,devframe,ref,browser.batch,729.854,133,610,false +cdb-aligned-devframe-final,devframe,ref,browser.fill,91.808,56,56,false +cdb-aligned-devframe-final,devframe,ref,browser.click,84.225,31,57,false +cdb-aligned-devframe-final,devframe,ref,browser.snapshot,637.709,18,461,false +cdb-aligned-devframe-final,devframe,ref,browser.batch,809.601,133,610,false +cdb-aligned-devframe-final,devframe,ref,browser.fill,99.468,56,56,false +cdb-aligned-devframe-final,devframe,ref,browser.click,86.270,31,57,false +cdb-aligned-devframe-final,devframe,ref,browser.snapshot,574.601,18,461,false +cdb-aligned-devframe-final,devframe,ref,browser.batch,788.271,133,610,false +cdb-aligned-devframe-final,devframe,ref,browser.fill,94.330,56,56,false +cdb-aligned-devframe-final,devframe,ref,browser.click,87.336,31,57,false +cdb-aligned-devframe-final,devframe,ref,browser.snapshot,569.574,18,461,false +cdb-aligned-devframe-final,devframe,ref,browser.batch,728.545,133,610,false +cdb-aligned-devframe-final,devframe,ref,browser.fill,91.929,56,56,false +cdb-aligned-devframe-final,devframe,ref,browser.click,83.765,31,57,false +cdb-aligned-devframe-final,devframe,ref,browser.snapshot,580.447,18,461,false +cdb-aligned-devframe-final,devframe,ref,browser.batch,708.669,133,610,false +cdb-aligned-devframe-final,devframe,ref,browser.fill,89.274,56,56,false +cdb-aligned-devframe-final,devframe,ref,browser.click,83.036,31,57,false +cdb-aligned-devframe-final,devframe,ref,browser.snapshot,602.801,18,461,false +cdb-aligned-devframe-final,devframe,ref,browser.batch,738.504,133,610,false +cdb-aligned-devframe-final,devframe,ref,browser.fill,91.906,56,56,false +cdb-aligned-devframe-final,devframe,ref,browser.click,91.025,31,57,false +cdb-aligned-devframe-final,devframe,ref,browser.snapshot,607.479,18,461,false +cdb-aligned-devframe-final,devframe,ref,browser.batch,809.914,133,610,false +cdb-aligned-devframe-final,devframe,ref,browser.click:blocked,2004.977,31,197,true +cdb-aligned-devframe-final,devframe,ref,browser.click:blocked,2003.364,31,197,true +cdb-aligned-devframe-final,devframe,ref,browser.click:blocked,2002.574,31,197,true +cdb-aligned-devframe-final,devframe,ref,browser.click:blocked,2001.980,31,197,true +cdb-aligned-devframe-final,devframe,ref,browser.click:blocked,2001.241,31,197,true +cdb-aligned-devframe-final,devframe,ref,browser.click:blocked,2004.907,31,197,true +cdb-aligned-devframe-final,devframe,ref,browser.click:blocked,2035.897,31,197,true +cdb-aligned-devframe-final,devframe,ref,browser.click:blocked,2003.762,31,197,true +cdb-aligned-devframe-final,devframe,ref,browser.click:blocked,2003.521,31,197,true +cdb-aligned-devframe-final,devframe,ref,browser.click:blocked,2002.160,31,197,true +cdb-aligned-devframe-final,devframe,ref,browser.click:blocked,2004.448,31,197,true +cdb-aligned-devframe-final,devframe,ref,browser.click:blocked,2001.832,31,197,true +cdb-aligned-devframe-final,devframe,ref,browser.click:blocked,2006.704,31,197,true +cdb-aligned-devframe-final,devframe,ref,browser.click:blocked,2005.048,31,197,true +cdb-aligned-devframe-final,devframe,ref,browser.click:blocked,2004.385,31,197,true +cdb-aligned-devframe-final,devframe,ref,browser.click:blocked,2005.645,31,197,true +cdb-aligned-devframe-final,devframe,ref,browser.click:blocked,2003.371,31,197,true +cdb-aligned-devframe-final,devframe,ref,browser.click:blocked,2002.687,31,197,true +cdb-aligned-devframe-final,devframe,ref,browser.click:blocked,2002.376,31,197,true +cdb-aligned-devframe-final,devframe,ref,browser.click:blocked,2002.315,31,197,true +cdb-aligned-devframe-final,devframe,ref,browser.click:blocked,2004.631,31,197,true +cdb-aligned-devframe-final,devframe,ref,browser.click:blocked,2002.205,31,197,true +cdb-aligned-devframe-final,devframe,ref,browser.click:blocked,2007.229,31,197,true +cdb-aligned-devframe-final,devframe,ref,browser.click:blocked,2001.779,31,197,true +cdb-aligned-devframe-final,devframe,ref,browser.click:blocked,2005.731,31,197,true +cdb-aligned-devframe-final,devframe,ref,browser.click:blocked,2001.764,31,197,true +cdb-aligned-devframe-final,devframe,ref,browser.click:blocked,2002.831,31,197,true +cdb-aligned-devframe-final,devframe,ref,browser.click:blocked,2003.137,31,197,true +cdb-aligned-devframe-final,devframe,ref,browser.click:blocked,2006.857,31,197,true +cdb-aligned-devframe-final,devframe,ref,browser.click:blocked,2004.026,31,197,true +cdb-aligned-devframe-final,devframe,locator,browser.fill,326.028,118,56,false +cdb-aligned-devframe-final,devframe,locator,browser.click,269.845,97,57,false +cdb-aligned-devframe-final,devframe,locator,browser.snapshot,737.313,18,461,false +cdb-aligned-devframe-final,devframe,locator,browser.batch,1134.772,261,610,false +cdb-aligned-devframe-final,devframe,locator,browser.fill,459.162,118,56,false +cdb-aligned-devframe-final,devframe,locator,browser.click,522.173,97,57,false +cdb-aligned-devframe-final,devframe,locator,browser.snapshot,704.864,18,461,false +cdb-aligned-devframe-final,devframe,locator,browser.batch,1270.937,261,610,false +cdb-aligned-devframe-final,devframe,locator,browser.fill,304.845,118,56,false +cdb-aligned-devframe-final,devframe,locator,browser.click,839.432,97,57,false +cdb-aligned-devframe-final,devframe,locator,browser.snapshot,663.875,18,461,false +cdb-aligned-devframe-final,devframe,locator,browser.batch,1075.940,261,610,false +cdb-aligned-devframe-final,devframe,locator,browser.fill,453.844,118,56,false +cdb-aligned-devframe-final,devframe,locator,browser.click,357.202,97,57,false +cdb-aligned-devframe-final,devframe,locator,browser.snapshot,596.851,18,461,false +cdb-aligned-devframe-final,devframe,locator,browser.batch,1362.910,261,610,false +cdb-aligned-devframe-final,devframe,locator,browser.fill,321.378,118,56,false +cdb-aligned-devframe-final,devframe,locator,browser.click,315.966,97,57,false +cdb-aligned-devframe-final,devframe,locator,browser.snapshot,598.542,18,461,false +cdb-aligned-devframe-final,devframe,locator,browser.batch,1246.113,261,610,false +cdb-aligned-devframe-final,devframe,locator,browser.fill,286.183,118,56,false +cdb-aligned-devframe-final,devframe,locator,browser.click,318.662,97,57,false +cdb-aligned-devframe-final,devframe,locator,browser.snapshot,647.900,18,461,false +cdb-aligned-devframe-final,devframe,locator,browser.batch,1334.540,261,610,false +cdb-aligned-devframe-final,devframe,locator,browser.fill,420.563,118,56,false +cdb-aligned-devframe-final,devframe,locator,browser.click,367.449,97,57,false +cdb-aligned-devframe-final,devframe,locator,browser.snapshot,848.921,18,461,false +cdb-aligned-devframe-final,devframe,locator,browser.batch,1292.561,261,610,false +cdb-aligned-devframe-final,devframe,locator,browser.fill,338.127,118,56,false +cdb-aligned-devframe-final,devframe,locator,browser.click,320.017,97,57,false +cdb-aligned-devframe-final,devframe,locator,browser.snapshot,621.152,18,461,false +cdb-aligned-devframe-final,devframe,locator,browser.batch,1187.023,261,610,false +cdb-aligned-devframe-final,devframe,locator,browser.fill,310.342,118,56,false +cdb-aligned-devframe-final,devframe,locator,browser.click,248.405,97,57,false +cdb-aligned-devframe-final,devframe,locator,browser.snapshot,777.826,18,461,false +cdb-aligned-devframe-final,devframe,locator,browser.batch,1113.329,261,610,false +cdb-aligned-devframe-final,devframe,locator,browser.fill,286.460,118,56,false +cdb-aligned-devframe-final,devframe,locator,browser.click,391.620,97,57,false +cdb-aligned-devframe-final,devframe,locator,browser.snapshot,916.579,18,461,false +cdb-aligned-devframe-final,devframe,locator,browser.batch,1033.854,261,610,false +cdb-aligned-devframe-final,devframe,locator,browser.fill,325.078,118,56,false +cdb-aligned-devframe-final,devframe,locator,browser.click,325.236,97,57,false +cdb-aligned-devframe-final,devframe,locator,browser.snapshot,746.520,18,461,false +cdb-aligned-devframe-final,devframe,locator,browser.batch,1509.607,261,610,false +cdb-aligned-devframe-final,devframe,locator,browser.fill,320.006,118,56,false +cdb-aligned-devframe-final,devframe,locator,browser.click,266.932,97,57,false +cdb-aligned-devframe-final,devframe,locator,browser.snapshot,633.822,18,461,false +cdb-aligned-devframe-final,devframe,locator,browser.batch,1121.278,261,610,false +cdb-aligned-devframe-final,devframe,locator,browser.fill,301.773,118,56,false +cdb-aligned-devframe-final,devframe,locator,browser.click,287.714,97,57,false +cdb-aligned-devframe-final,devframe,locator,browser.snapshot,663.195,18,461,false +cdb-aligned-devframe-final,devframe,locator,browser.batch,1008.390,261,610,false +cdb-aligned-devframe-final,devframe,locator,browser.fill,266.984,118,56,false +cdb-aligned-devframe-final,devframe,locator,browser.click,260.587,97,57,false +cdb-aligned-devframe-final,devframe,locator,browser.snapshot,717.765,18,461,false +cdb-aligned-devframe-final,devframe,locator,browser.batch,983.019,261,610,false +cdb-aligned-devframe-final,devframe,locator,browser.fill,247.999,118,56,false +cdb-aligned-devframe-final,devframe,locator,browser.click,310.843,97,57,false +cdb-aligned-devframe-final,devframe,locator,browser.snapshot,603.090,18,461,false +cdb-aligned-devframe-final,devframe,locator,browser.batch,1096.830,261,610,false +cdb-aligned-devframe-final,devframe,locator,browser.fill,255.051,118,56,false +cdb-aligned-devframe-final,devframe,locator,browser.click,274.290,97,57,false +cdb-aligned-devframe-final,devframe,locator,browser.snapshot,628.900,18,461,false +cdb-aligned-devframe-final,devframe,locator,browser.batch,1045.957,261,610,false +cdb-aligned-devframe-final,devframe,locator,browser.fill,258.932,118,56,false +cdb-aligned-devframe-final,devframe,locator,browser.click,247.100,97,57,false +cdb-aligned-devframe-final,devframe,locator,browser.snapshot,593.495,18,461,false +cdb-aligned-devframe-final,devframe,locator,browser.batch,1195.198,261,610,false +cdb-aligned-devframe-final,devframe,locator,browser.fill,363.917,118,56,false +cdb-aligned-devframe-final,devframe,locator,browser.click,364.284,97,57,false +cdb-aligned-devframe-final,devframe,locator,browser.snapshot,667.268,18,461,false +cdb-aligned-devframe-final,devframe,locator,browser.batch,1173.217,261,610,false +cdb-aligned-devframe-final,devframe,locator,browser.fill,390.237,118,56,false +cdb-aligned-devframe-final,devframe,locator,browser.click,317.914,97,57,false +cdb-aligned-devframe-final,devframe,locator,browser.snapshot,598.087,18,461,false +cdb-aligned-devframe-final,devframe,locator,browser.batch,1246.020,261,610,false +cdb-aligned-devframe-final,devframe,locator,browser.fill,359.646,118,56,false +cdb-aligned-devframe-final,devframe,locator,browser.click,341.275,97,57,false +cdb-aligned-devframe-final,devframe,locator,browser.snapshot,630.758,18,461,false +cdb-aligned-devframe-final,devframe,locator,browser.batch,1116.495,261,610,false +cdb-aligned-devframe-final,devframe,locator,browser.fill,319.452,118,56,false +cdb-aligned-devframe-final,devframe,locator,browser.click,388.269,97,57,false +cdb-aligned-devframe-final,devframe,locator,browser.snapshot,614.624,18,461,false +cdb-aligned-devframe-final,devframe,locator,browser.batch,1153.822,261,610,false +cdb-aligned-devframe-final,devframe,locator,browser.fill,275.739,118,56,false +cdb-aligned-devframe-final,devframe,locator,browser.click,284.748,97,57,false +cdb-aligned-devframe-final,devframe,locator,browser.snapshot,601.509,18,461,false +cdb-aligned-devframe-final,devframe,locator,browser.batch,1105.548,261,610,false +cdb-aligned-devframe-final,devframe,locator,browser.fill,339.995,118,56,false +cdb-aligned-devframe-final,devframe,locator,browser.click,293.390,97,57,false +cdb-aligned-devframe-final,devframe,locator,browser.snapshot,583.573,18,461,false +cdb-aligned-devframe-final,devframe,locator,browser.batch,1407.198,261,610,false +cdb-aligned-devframe-final,devframe,locator,browser.fill,324.645,118,56,false +cdb-aligned-devframe-final,devframe,locator,browser.click,332.169,97,57,false +cdb-aligned-devframe-final,devframe,locator,browser.snapshot,601.874,18,461,false +cdb-aligned-devframe-final,devframe,locator,browser.batch,1076.067,261,610,false +cdb-aligned-devframe-final,devframe,locator,browser.fill,298.937,118,56,false +cdb-aligned-devframe-final,devframe,locator,browser.click,279.638,97,57,false +cdb-aligned-devframe-final,devframe,locator,browser.snapshot,634.963,18,461,false +cdb-aligned-devframe-final,devframe,locator,browser.batch,1123.801,261,610,false +cdb-aligned-devframe-final,devframe,locator,browser.fill,335.841,118,56,false +cdb-aligned-devframe-final,devframe,locator,browser.click,288.333,97,57,false +cdb-aligned-devframe-final,devframe,locator,browser.snapshot,716.417,18,461,false +cdb-aligned-devframe-final,devframe,locator,browser.batch,1298.758,261,610,false +cdb-aligned-devframe-final,devframe,locator,browser.fill,340.047,118,56,false +cdb-aligned-devframe-final,devframe,locator,browser.click,284.323,97,57,false +cdb-aligned-devframe-final,devframe,locator,browser.snapshot,685.227,18,461,false +cdb-aligned-devframe-final,devframe,locator,browser.batch,1174.204,261,610,false +cdb-aligned-devframe-final,devframe,locator,browser.fill,313.723,118,56,false +cdb-aligned-devframe-final,devframe,locator,browser.click,413.386,97,57,false +cdb-aligned-devframe-final,devframe,locator,browser.snapshot,621.537,18,461,false +cdb-aligned-devframe-final,devframe,locator,browser.batch,1193.260,261,610,false +cdb-aligned-devframe-final,devframe,locator,browser.fill,341.308,118,56,false +cdb-aligned-devframe-final,devframe,locator,browser.click,341.048,97,57,false +cdb-aligned-devframe-final,devframe,locator,browser.snapshot,646.385,18,461,false +cdb-aligned-devframe-final,devframe,locator,browser.batch,1200.274,261,610,false +cdb-aligned-devframe-final,devframe,locator,browser.fill,435.978,118,56,false +cdb-aligned-devframe-final,devframe,locator,browser.click,305.365,97,57,false +cdb-aligned-devframe-final,devframe,locator,browser.snapshot,675.460,18,461,false +cdb-aligned-devframe-final,devframe,locator,browser.batch,1146.640,261,610,false +cdb-aligned-devframe-final,devframe,locator,browser.click:blocked,2001.521,97,197,true +cdb-aligned-devframe-final,devframe,locator,browser.click:blocked,2000.465,97,197,true +cdb-aligned-devframe-final,devframe,locator,browser.click:blocked,2003.136,97,197,true +cdb-aligned-devframe-final,devframe,locator,browser.click:blocked,2001.352,97,197,true +cdb-aligned-devframe-final,devframe,locator,browser.click:blocked,2013.479,97,197,true +cdb-aligned-devframe-final,devframe,locator,browser.click:blocked,2002.140,97,197,true +cdb-aligned-devframe-final,devframe,locator,browser.click:blocked,2001.411,97,197,true +cdb-aligned-devframe-final,devframe,locator,browser.click:blocked,2001.020,97,197,true +cdb-aligned-devframe-final,devframe,locator,browser.click:blocked,2001.642,97,197,true +cdb-aligned-devframe-final,devframe,locator,browser.click:blocked,2000.741,97,197,true +cdb-aligned-devframe-final,devframe,locator,browser.click:blocked,2002.094,97,197,true +cdb-aligned-devframe-final,devframe,locator,browser.click:blocked,2002.356,97,197,true +cdb-aligned-devframe-final,devframe,locator,browser.click:blocked,2007.245,97,197,true +cdb-aligned-devframe-final,devframe,locator,browser.click:blocked,2001.516,97,197,true +cdb-aligned-devframe-final,devframe,locator,browser.click:blocked,2001.842,97,197,true +cdb-aligned-devframe-final,devframe,locator,browser.click:blocked,2001.987,97,197,true +cdb-aligned-devframe-final,devframe,locator,browser.click:blocked,2001.793,97,197,true +cdb-aligned-devframe-final,devframe,locator,browser.click:blocked,2002.590,97,197,true +cdb-aligned-devframe-final,devframe,locator,browser.click:blocked,2160.022,97,197,true +cdb-aligned-devframe-final,devframe,locator,browser.click:blocked,2002.763,97,197,true +cdb-aligned-devframe-final,devframe,locator,browser.click:blocked,2001.481,97,197,true +cdb-aligned-devframe-final,devframe,locator,browser.click:blocked,2038.299,97,197,true +cdb-aligned-devframe-final,devframe,locator,browser.click:blocked,2000.835,97,197,true +cdb-aligned-devframe-final,devframe,locator,browser.click:blocked,2001.782,97,197,true +cdb-aligned-devframe-final,devframe,locator,browser.click:blocked,2001.691,97,197,true +cdb-aligned-devframe-final,devframe,locator,browser.click:blocked,2063.094,97,197,true +cdb-aligned-devframe-final,devframe,locator,browser.click:blocked,2001.165,97,197,true +cdb-aligned-devframe-final,devframe,locator,browser.click:blocked,2002.930,97,197,true +cdb-aligned-devframe-final,devframe,locator,browser.click:blocked,2001.441,97,197,true +cdb-aligned-devframe-final,devframe,locator,browser.click:blocked,2001.192,97,197,true +cdb-aligned-websocket-parallel,websocket,ref,browser.fill,110.349,55,56,false +cdb-aligned-websocket-parallel,websocket,ref,browser.click,164.319,30,57,false +cdb-aligned-websocket-parallel,websocket,ref,browser.snapshot,693.576,18,456,false +cdb-aligned-websocket-parallel,websocket,ref,browser.batch,1049.033,131,605,false +cdb-aligned-websocket-parallel,websocket,ref,browser.fill,126.469,55,56,false +cdb-aligned-websocket-parallel,websocket,ref,browser.click,122.989,30,57,false +cdb-aligned-websocket-parallel,websocket,ref,browser.snapshot,712.034,18,456,false +cdb-aligned-websocket-parallel,websocket,ref,browser.batch,857.539,131,605,false +cdb-aligned-websocket-parallel,websocket,ref,browser.fill,108.178,55,56,false +cdb-aligned-websocket-parallel,websocket,ref,browser.click,123.634,30,57,false +cdb-aligned-websocket-parallel,websocket,ref,browser.snapshot,738.403,18,456,false +cdb-aligned-websocket-parallel,websocket,ref,browser.batch,881.006,131,605,false +cdb-aligned-websocket-parallel,websocket,ref,browser.fill,131.101,55,56,false +cdb-aligned-websocket-parallel,websocket,ref,browser.click,116.807,30,57,false +cdb-aligned-websocket-parallel,websocket,ref,browser.snapshot,689.484,18,456,false +cdb-aligned-websocket-parallel,websocket,ref,browser.batch,939.188,131,605,false +cdb-aligned-websocket-parallel,websocket,ref,browser.fill,207.175,55,56,false +cdb-aligned-websocket-parallel,websocket,ref,browser.click,181.065,30,57,false +cdb-aligned-websocket-parallel,websocket,ref,browser.snapshot,787.925,18,457,false +cdb-aligned-websocket-parallel,websocket,ref,browser.batch,1008.126,132,610,false +cdb-aligned-websocket-parallel,websocket,ref,browser.fill,119.565,56,56,false +cdb-aligned-websocket-parallel,websocket,ref,browser.click,99.139,31,57,false +cdb-aligned-websocket-parallel,websocket,ref,browser.snapshot,607.530,18,461,false +cdb-aligned-websocket-parallel,websocket,ref,browser.batch,973.006,133,610,false +cdb-aligned-websocket-parallel,websocket,ref,browser.fill,137.401,56,56,false +cdb-aligned-websocket-parallel,websocket,ref,browser.click,99.396,31,57,false +cdb-aligned-websocket-parallel,websocket,ref,browser.snapshot,637.585,18,461,false +cdb-aligned-websocket-parallel,websocket,ref,browser.batch,1044.072,133,610,false +cdb-aligned-websocket-parallel,websocket,ref,browser.fill,110.177,56,56,false +cdb-aligned-websocket-parallel,websocket,ref,browser.click,133.755,31,57,false +cdb-aligned-websocket-parallel,websocket,ref,browser.snapshot,786.440,18,461,false +cdb-aligned-websocket-parallel,websocket,ref,browser.batch,1079.344,133,610,false +cdb-aligned-websocket-parallel,websocket,ref,browser.fill,111.786,56,56,false +cdb-aligned-websocket-parallel,websocket,ref,browser.click,93.484,31,57,false +cdb-aligned-websocket-parallel,websocket,ref,browser.snapshot,766.016,18,461,false +cdb-aligned-websocket-parallel,websocket,ref,browser.batch,1177.653,133,610,false +cdb-aligned-websocket-parallel,websocket,ref,browser.fill,135.028,56,56,false +cdb-aligned-websocket-parallel,websocket,ref,browser.click,92.730,31,57,false +cdb-aligned-websocket-parallel,websocket,ref,browser.snapshot,951.966,18,461,false +cdb-aligned-websocket-parallel,websocket,ref,browser.batch,990.045,133,610,false +cdb-aligned-websocket-parallel,websocket,ref,browser.fill,103.306,56,56,false +cdb-aligned-websocket-parallel,websocket,ref,browser.click,99.970,31,57,false +cdb-aligned-websocket-parallel,websocket,ref,browser.snapshot,829.333,18,461,false +cdb-aligned-websocket-parallel,websocket,ref,browser.batch,1041.501,133,610,false +cdb-aligned-websocket-parallel,websocket,ref,browser.fill,113.457,56,56,false +cdb-aligned-websocket-parallel,websocket,ref,browser.click,102.575,31,57,false +cdb-aligned-websocket-parallel,websocket,ref,browser.snapshot,972.310,18,461,false +cdb-aligned-websocket-parallel,websocket,ref,browser.batch,1142.404,133,610,false +cdb-aligned-websocket-parallel,websocket,ref,browser.fill,110.661,56,56,false +cdb-aligned-websocket-parallel,websocket,ref,browser.click,100.616,31,57,false +cdb-aligned-websocket-parallel,websocket,ref,browser.snapshot,1019.410,18,461,false +cdb-aligned-websocket-parallel,websocket,ref,browser.batch,1057.046,133,610,false +cdb-aligned-websocket-parallel,websocket,ref,browser.fill,121.422,56,56,false +cdb-aligned-websocket-parallel,websocket,ref,browser.click,111.216,31,57,false +cdb-aligned-websocket-parallel,websocket,ref,browser.snapshot,1035.068,18,461,false +cdb-aligned-websocket-parallel,websocket,ref,browser.batch,1820.802,133,610,false +cdb-aligned-websocket-parallel,websocket,ref,browser.fill,116.403,56,56,false +cdb-aligned-websocket-parallel,websocket,ref,browser.click,118.669,31,57,false +cdb-aligned-websocket-parallel,websocket,ref,browser.snapshot,946.644,18,461,false +cdb-aligned-websocket-parallel,websocket,ref,browser.batch,950.472,133,610,false +cdb-aligned-websocket-parallel,websocket,ref,browser.fill,116.066,56,56,false +cdb-aligned-websocket-parallel,websocket,ref,browser.click,93.150,31,57,false +cdb-aligned-websocket-parallel,websocket,ref,browser.snapshot,1057.186,18,461,false +cdb-aligned-websocket-parallel,websocket,ref,browser.batch,957.435,133,610,false +cdb-aligned-websocket-parallel,websocket,ref,browser.fill,117.712,56,56,false +cdb-aligned-websocket-parallel,websocket,ref,browser.click,153.857,31,57,false +cdb-aligned-websocket-parallel,websocket,ref,browser.snapshot,850.697,18,461,false +cdb-aligned-websocket-parallel,websocket,ref,browser.batch,979.736,133,610,false +cdb-aligned-websocket-parallel,websocket,ref,browser.fill,138.779,56,56,false +cdb-aligned-websocket-parallel,websocket,ref,browser.click,91.395,31,57,false +cdb-aligned-websocket-parallel,websocket,ref,browser.snapshot,774.771,18,461,false +cdb-aligned-websocket-parallel,websocket,ref,browser.batch,1030.622,133,610,false +cdb-aligned-websocket-parallel,websocket,ref,browser.fill,101.021,56,56,false +cdb-aligned-websocket-parallel,websocket,ref,browser.click,91.131,31,57,false +cdb-aligned-websocket-parallel,websocket,ref,browser.snapshot,720.719,18,461,false +cdb-aligned-websocket-parallel,websocket,ref,browser.batch,1364.584,133,610,false +cdb-aligned-websocket-parallel,websocket,ref,browser.fill,106.906,56,56,false +cdb-aligned-websocket-parallel,websocket,ref,browser.click,105.362,31,57,false +cdb-aligned-websocket-parallel,websocket,ref,browser.snapshot,828.797,18,461,false +cdb-aligned-websocket-parallel,websocket,ref,browser.batch,908.623,133,610,false +cdb-aligned-websocket-parallel,websocket,ref,browser.fill,129.382,56,56,false +cdb-aligned-websocket-parallel,websocket,ref,browser.click,106.926,31,57,false +cdb-aligned-websocket-parallel,websocket,ref,browser.snapshot,768.416,18,461,false +cdb-aligned-websocket-parallel,websocket,ref,browser.batch,1048.596,133,610,false +cdb-aligned-websocket-parallel,websocket,ref,browser.fill,104.211,56,56,false +cdb-aligned-websocket-parallel,websocket,ref,browser.click,94.817,31,57,false +cdb-aligned-websocket-parallel,websocket,ref,browser.snapshot,633.342,18,461,false +cdb-aligned-websocket-parallel,websocket,ref,browser.batch,2214.394,133,610,false +cdb-aligned-websocket-parallel,websocket,ref,browser.fill,143.629,56,56,false +cdb-aligned-websocket-parallel,websocket,ref,browser.click,152.732,31,57,false +cdb-aligned-websocket-parallel,websocket,ref,browser.snapshot,954.444,18,461,false +cdb-aligned-websocket-parallel,websocket,ref,browser.batch,1186.991,133,610,false +cdb-aligned-websocket-parallel,websocket,ref,browser.fill,115.693,56,56,false +cdb-aligned-websocket-parallel,websocket,ref,browser.click,148.189,31,57,false +cdb-aligned-websocket-parallel,websocket,ref,browser.snapshot,777.599,18,461,false +cdb-aligned-websocket-parallel,websocket,ref,browser.batch,1054.380,133,610,false +cdb-aligned-websocket-parallel,websocket,ref,browser.fill,130.046,56,56,false +cdb-aligned-websocket-parallel,websocket,ref,browser.click,156.248,31,57,false +cdb-aligned-websocket-parallel,websocket,ref,browser.snapshot,731.703,18,461,false +cdb-aligned-websocket-parallel,websocket,ref,browser.batch,1171.236,133,610,false +cdb-aligned-websocket-parallel,websocket,ref,browser.fill,136.566,56,56,false +cdb-aligned-websocket-parallel,websocket,ref,browser.click,115.851,31,57,false +cdb-aligned-websocket-parallel,websocket,ref,browser.snapshot,700.691,18,461,false +cdb-aligned-websocket-parallel,websocket,ref,browser.batch,823.961,133,610,false +cdb-aligned-websocket-parallel,websocket,ref,browser.fill,134.948,56,56,false +cdb-aligned-websocket-parallel,websocket,ref,browser.click,189.183,31,57,false +cdb-aligned-websocket-parallel,websocket,ref,browser.snapshot,1415.783,18,461,false +cdb-aligned-websocket-parallel,websocket,ref,browser.batch,1145.585,133,610,false +cdb-aligned-websocket-parallel,websocket,ref,browser.fill,162.917,56,56,false +cdb-aligned-websocket-parallel,websocket,ref,browser.click,129.704,31,57,false +cdb-aligned-websocket-parallel,websocket,ref,browser.snapshot,920.290,18,461,false +cdb-aligned-websocket-parallel,websocket,ref,browser.batch,1005.237,133,610,false +cdb-aligned-websocket-parallel,websocket,ref,browser.fill,107.833,56,56,false +cdb-aligned-websocket-parallel,websocket,ref,browser.click,106.504,31,57,false +cdb-aligned-websocket-parallel,websocket,ref,browser.snapshot,678.908,18,461,false +cdb-aligned-websocket-parallel,websocket,ref,browser.batch,1014.632,133,610,false +cdb-aligned-websocket-parallel,websocket,ref,browser.fill,154.685,56,56,false +cdb-aligned-websocket-parallel,websocket,ref,browser.click,113.525,31,57,false +cdb-aligned-websocket-parallel,websocket,ref,browser.snapshot,784.899,18,461,false +cdb-aligned-websocket-parallel,websocket,ref,browser.batch,1175.897,133,610,false +cdb-aligned-websocket-parallel,websocket,ref,browser.click:blocked,2002.806,31,172,true +cdb-aligned-websocket-parallel,websocket,ref,browser.click:blocked,2001.768,31,172,true +cdb-aligned-websocket-parallel,websocket,ref,browser.click:blocked,2001.700,31,172,true +cdb-aligned-websocket-parallel,websocket,ref,browser.click:blocked,2002.180,31,172,true +cdb-aligned-websocket-parallel,websocket,ref,browser.click:blocked,2000.952,31,172,true +cdb-aligned-websocket-parallel,websocket,ref,browser.click:blocked,2001.833,31,172,true +cdb-aligned-websocket-parallel,websocket,ref,browser.click:blocked,2002.386,31,172,true +cdb-aligned-websocket-parallel,websocket,ref,browser.click:blocked,2001.544,31,172,true +cdb-aligned-websocket-parallel,websocket,ref,browser.click:blocked,2000.259,31,172,true +cdb-aligned-websocket-parallel,websocket,ref,browser.click:blocked,2002.446,31,172,true +cdb-aligned-websocket-parallel,websocket,ref,browser.click:blocked,2002.160,31,172,true +cdb-aligned-websocket-parallel,websocket,ref,browser.click:blocked,2002.779,31,172,true +cdb-aligned-websocket-parallel,websocket,ref,browser.click:blocked,2001.890,31,172,true +cdb-aligned-websocket-parallel,websocket,ref,browser.click:blocked,2006.350,31,172,true +cdb-aligned-websocket-parallel,websocket,ref,browser.click:blocked,2001.961,31,172,true +cdb-aligned-websocket-parallel,websocket,ref,browser.click:blocked,2001.752,31,172,true +cdb-aligned-websocket-parallel,websocket,ref,browser.click:blocked,2004.805,31,172,true +cdb-aligned-websocket-parallel,websocket,ref,browser.click:blocked,2002.063,31,172,true +cdb-aligned-websocket-parallel,websocket,ref,browser.click:blocked,2002.033,31,172,true +cdb-aligned-websocket-parallel,websocket,ref,browser.click:blocked,2000.879,31,172,true +cdb-aligned-websocket-parallel,websocket,ref,browser.click:blocked,2001.715,31,172,true +cdb-aligned-websocket-parallel,websocket,ref,browser.click:blocked,2003.600,31,172,true +cdb-aligned-websocket-parallel,websocket,ref,browser.click:blocked,2003.396,31,172,true +cdb-aligned-websocket-parallel,websocket,ref,browser.click:blocked,2000.732,31,172,true +cdb-aligned-websocket-parallel,websocket,ref,browser.click:blocked,2001.205,31,172,true +cdb-aligned-websocket-parallel,websocket,ref,browser.click:blocked,2001.392,31,172,true +cdb-aligned-websocket-parallel,websocket,ref,browser.click:blocked,2002.487,31,172,true +cdb-aligned-websocket-parallel,websocket,ref,browser.click:blocked,2002.153,31,172,true +cdb-aligned-websocket-parallel,websocket,ref,browser.click:blocked,2001.326,31,172,true +cdb-aligned-websocket-parallel,websocket,ref,browser.click:blocked,2002.353,31,172,true +cdb-aligned-websocket-parallel,websocket,locator,browser.fill,186.919,118,56,false +cdb-aligned-websocket-parallel,websocket,locator,browser.click,157.957,97,57,false +cdb-aligned-websocket-parallel,websocket,locator,browser.snapshot,624.641,18,461,false +cdb-aligned-websocket-parallel,websocket,locator,browser.batch,878.213,261,610,false +cdb-aligned-websocket-parallel,websocket,locator,browser.fill,170.292,118,56,false +cdb-aligned-websocket-parallel,websocket,locator,browser.click,171.458,97,57,false +cdb-aligned-websocket-parallel,websocket,locator,browser.snapshot,631.029,18,461,false +cdb-aligned-websocket-parallel,websocket,locator,browser.batch,918.305,261,610,false +cdb-aligned-websocket-parallel,websocket,locator,browser.fill,792.121,118,56,false +cdb-aligned-websocket-parallel,websocket,locator,browser.click,492.599,97,57,false +cdb-aligned-websocket-parallel,websocket,locator,browser.snapshot,1074.950,18,461,false +cdb-aligned-websocket-parallel,websocket,locator,browser.batch,894.465,261,610,false +cdb-aligned-websocket-parallel,websocket,locator,browser.fill,207.911,118,56,false +cdb-aligned-websocket-parallel,websocket,locator,browser.click,177.870,97,57,false +cdb-aligned-websocket-parallel,websocket,locator,browser.snapshot,646.864,18,461,false +cdb-aligned-websocket-parallel,websocket,locator,browser.batch,899.422,261,610,false +cdb-aligned-websocket-parallel,websocket,locator,browser.fill,167.055,118,56,false +cdb-aligned-websocket-parallel,websocket,locator,browser.click,175.318,97,57,false +cdb-aligned-websocket-parallel,websocket,locator,browser.snapshot,688.071,18,461,false +cdb-aligned-websocket-parallel,websocket,locator,browser.batch,944.081,261,610,false +cdb-aligned-websocket-parallel,websocket,locator,browser.fill,203.437,118,56,false +cdb-aligned-websocket-parallel,websocket,locator,browser.click,187.904,97,57,false +cdb-aligned-websocket-parallel,websocket,locator,browser.snapshot,781.198,18,461,false +cdb-aligned-websocket-parallel,websocket,locator,browser.batch,885.090,261,610,false +cdb-aligned-websocket-parallel,websocket,locator,browser.fill,181.080,118,56,false +cdb-aligned-websocket-parallel,websocket,locator,browser.click,174.575,97,57,false +cdb-aligned-websocket-parallel,websocket,locator,browser.snapshot,624.320,18,461,false +cdb-aligned-websocket-parallel,websocket,locator,browser.batch,875.409,261,610,false +cdb-aligned-websocket-parallel,websocket,locator,browser.fill,169.806,118,56,false +cdb-aligned-websocket-parallel,websocket,locator,browser.click,156.707,97,57,false +cdb-aligned-websocket-parallel,websocket,locator,browser.snapshot,846.582,18,461,false +cdb-aligned-websocket-parallel,websocket,locator,browser.batch,985.931,261,610,false +cdb-aligned-websocket-parallel,websocket,locator,browser.fill,244.171,118,56,false +cdb-aligned-websocket-parallel,websocket,locator,browser.click,370.548,97,57,false +cdb-aligned-websocket-parallel,websocket,locator,browser.snapshot,881.152,18,461,false +cdb-aligned-websocket-parallel,websocket,locator,browser.batch,1122.031,261,610,false +cdb-aligned-websocket-parallel,websocket,locator,browser.fill,305.344,118,56,false +cdb-aligned-websocket-parallel,websocket,locator,browser.click,268.498,97,57,false +cdb-aligned-websocket-parallel,websocket,locator,browser.snapshot,2400.201,18,461,false +cdb-aligned-websocket-parallel,websocket,locator,browser.batch,1168.382,261,610,false +cdb-aligned-websocket-parallel,websocket,locator,browser.fill,310.887,118,56,false +cdb-aligned-websocket-parallel,websocket,locator,browser.click,291.318,97,57,false +cdb-aligned-websocket-parallel,websocket,locator,browser.snapshot,992.076,18,461,false +cdb-aligned-websocket-parallel,websocket,locator,browser.batch,1083.283,261,610,false +cdb-aligned-websocket-parallel,websocket,locator,browser.fill,276.059,118,56,false +cdb-aligned-websocket-parallel,websocket,locator,browser.click,168.151,97,57,false +cdb-aligned-websocket-parallel,websocket,locator,browser.snapshot,738.523,18,461,false +cdb-aligned-websocket-parallel,websocket,locator,browser.batch,1038.702,261,610,false +cdb-aligned-websocket-parallel,websocket,locator,browser.fill,201.661,118,56,false +cdb-aligned-websocket-parallel,websocket,locator,browser.click,167.604,97,57,false +cdb-aligned-websocket-parallel,websocket,locator,browser.snapshot,1021.818,18,461,false +cdb-aligned-websocket-parallel,websocket,locator,browser.batch,1031.407,261,610,false +cdb-aligned-websocket-parallel,websocket,locator,browser.fill,186.432,118,56,false +cdb-aligned-websocket-parallel,websocket,locator,browser.click,184.637,97,57,false +cdb-aligned-websocket-parallel,websocket,locator,browser.snapshot,932.359,18,461,false +cdb-aligned-websocket-parallel,websocket,locator,browser.batch,1123.913,261,610,false +cdb-aligned-websocket-parallel,websocket,locator,browser.fill,200.625,118,56,false +cdb-aligned-websocket-parallel,websocket,locator,browser.click,186.374,97,57,false +cdb-aligned-websocket-parallel,websocket,locator,browser.snapshot,949.285,18,461,false +cdb-aligned-websocket-parallel,websocket,locator,browser.batch,1057.290,261,610,false +cdb-aligned-websocket-parallel,websocket,locator,browser.fill,176.076,118,56,false +cdb-aligned-websocket-parallel,websocket,locator,browser.click,159.764,97,57,false +cdb-aligned-websocket-parallel,websocket,locator,browser.snapshot,805.208,18,461,false +cdb-aligned-websocket-parallel,websocket,locator,browser.batch,1158.724,261,610,false +cdb-aligned-websocket-parallel,websocket,locator,browser.fill,184.754,118,56,false +cdb-aligned-websocket-parallel,websocket,locator,browser.click,242.464,97,57,false +cdb-aligned-websocket-parallel,websocket,locator,browser.snapshot,880.564,18,461,false +cdb-aligned-websocket-parallel,websocket,locator,browser.batch,1085.170,261,610,false +cdb-aligned-websocket-parallel,websocket,locator,browser.fill,175.074,118,56,false +cdb-aligned-websocket-parallel,websocket,locator,browser.click,149.245,97,57,false +cdb-aligned-websocket-parallel,websocket,locator,browser.snapshot,597.488,18,461,false +cdb-aligned-websocket-parallel,websocket,locator,browser.batch,898.255,261,610,false +cdb-aligned-websocket-parallel,websocket,locator,browser.fill,183.960,118,56,false +cdb-aligned-websocket-parallel,websocket,locator,browser.click,159.084,97,57,false +cdb-aligned-websocket-parallel,websocket,locator,browser.snapshot,658.645,18,461,false +cdb-aligned-websocket-parallel,websocket,locator,browser.batch,915.920,261,610,false +cdb-aligned-websocket-parallel,websocket,locator,browser.fill,172.895,118,56,false +cdb-aligned-websocket-parallel,websocket,locator,browser.click,170.232,97,57,false +cdb-aligned-websocket-parallel,websocket,locator,browser.snapshot,676.669,18,461,false +cdb-aligned-websocket-parallel,websocket,locator,browser.batch,912.837,261,610,false +cdb-aligned-websocket-parallel,websocket,locator,browser.fill,166.385,118,56,false +cdb-aligned-websocket-parallel,websocket,locator,browser.click,157.058,97,57,false +cdb-aligned-websocket-parallel,websocket,locator,browser.snapshot,623.304,18,461,false +cdb-aligned-websocket-parallel,websocket,locator,browser.batch,3187.630,261,610,false +cdb-aligned-websocket-parallel,websocket,locator,browser.fill,190.422,118,56,false +cdb-aligned-websocket-parallel,websocket,locator,browser.click,157.176,97,57,false +cdb-aligned-websocket-parallel,websocket,locator,browser.snapshot,704.043,18,461,false +cdb-aligned-websocket-parallel,websocket,locator,browser.batch,1003.968,261,610,false +cdb-aligned-websocket-parallel,websocket,locator,browser.fill,181.559,118,56,false +cdb-aligned-websocket-parallel,websocket,locator,browser.click,161.496,97,57,false +cdb-aligned-websocket-parallel,websocket,locator,browser.snapshot,655.429,18,461,false +cdb-aligned-websocket-parallel,websocket,locator,browser.batch,921.803,261,610,false +cdb-aligned-websocket-parallel,websocket,locator,browser.fill,209.655,118,56,false +cdb-aligned-websocket-parallel,websocket,locator,browser.click,172.741,97,57,false +cdb-aligned-websocket-parallel,websocket,locator,browser.snapshot,659.700,18,461,false +cdb-aligned-websocket-parallel,websocket,locator,browser.batch,928.558,261,610,false +cdb-aligned-websocket-parallel,websocket,locator,browser.fill,171.313,118,56,false +cdb-aligned-websocket-parallel,websocket,locator,browser.click,190.029,97,57,false +cdb-aligned-websocket-parallel,websocket,locator,browser.snapshot,587.254,18,461,false +cdb-aligned-websocket-parallel,websocket,locator,browser.batch,843.839,261,610,false +cdb-aligned-websocket-parallel,websocket,locator,browser.fill,166.008,118,56,false +cdb-aligned-websocket-parallel,websocket,locator,browser.click,333.554,97,57,false +cdb-aligned-websocket-parallel,websocket,locator,browser.snapshot,2649.651,18,461,false +cdb-aligned-websocket-parallel,websocket,locator,browser.batch,1096.055,261,610,false +cdb-aligned-websocket-parallel,websocket,locator,browser.fill,162.828,118,56,false +cdb-aligned-websocket-parallel,websocket,locator,browser.click,152.435,97,57,false +cdb-aligned-websocket-parallel,websocket,locator,browser.snapshot,604.902,18,461,false +cdb-aligned-websocket-parallel,websocket,locator,browser.batch,904.953,261,610,false +cdb-aligned-websocket-parallel,websocket,locator,browser.fill,176.077,118,56,false +cdb-aligned-websocket-parallel,websocket,locator,browser.click,153.202,97,57,false +cdb-aligned-websocket-parallel,websocket,locator,browser.snapshot,588.750,18,461,false +cdb-aligned-websocket-parallel,websocket,locator,browser.batch,848.441,261,610,false +cdb-aligned-websocket-parallel,websocket,locator,browser.fill,173.684,118,56,false +cdb-aligned-websocket-parallel,websocket,locator,browser.click,183.442,97,57,false +cdb-aligned-websocket-parallel,websocket,locator,browser.snapshot,634.614,18,461,false +cdb-aligned-websocket-parallel,websocket,locator,browser.batch,952.296,261,610,false +cdb-aligned-websocket-parallel,websocket,locator,browser.fill,172.646,118,56,false +cdb-aligned-websocket-parallel,websocket,locator,browser.click,154.571,97,57,false +cdb-aligned-websocket-parallel,websocket,locator,browser.snapshot,710.832,18,461,false +cdb-aligned-websocket-parallel,websocket,locator,browser.batch,891.158,261,610,false +cdb-aligned-websocket-parallel,websocket,locator,browser.click:blocked,2000.715,97,172,true +cdb-aligned-websocket-parallel,websocket,locator,browser.click:blocked,2001.658,97,172,true +cdb-aligned-websocket-parallel,websocket,locator,browser.click:blocked,2001.335,97,172,true +cdb-aligned-websocket-parallel,websocket,locator,browser.click:blocked,2001.192,97,172,true +cdb-aligned-websocket-parallel,websocket,locator,browser.click:blocked,1999.984,97,172,true +cdb-aligned-websocket-parallel,websocket,locator,browser.click:blocked,2000.683,97,172,true +cdb-aligned-websocket-parallel,websocket,locator,browser.click:blocked,2001.946,97,172,true +cdb-aligned-websocket-parallel,websocket,locator,browser.click:blocked,2000.643,97,172,true +cdb-aligned-websocket-parallel,websocket,locator,browser.click:blocked,2001.977,97,172,true +cdb-aligned-websocket-parallel,websocket,locator,browser.click:blocked,2002.659,97,172,true +cdb-aligned-websocket-parallel,websocket,locator,browser.click:blocked,2001.788,97,172,true +cdb-aligned-websocket-parallel,websocket,locator,browser.click:blocked,2001.821,97,172,true +cdb-aligned-websocket-parallel,websocket,locator,browser.click:blocked,2001.168,97,172,true +cdb-aligned-websocket-parallel,websocket,locator,browser.click:blocked,2001.263,97,172,true +cdb-aligned-websocket-parallel,websocket,locator,browser.click:blocked,1999.656,97,172,true +cdb-aligned-websocket-parallel,websocket,locator,browser.click:blocked,2002.116,97,172,true +cdb-aligned-websocket-parallel,websocket,locator,browser.click:blocked,2001.195,97,172,true +cdb-aligned-websocket-parallel,websocket,locator,browser.click:blocked,2002.438,97,172,true +cdb-aligned-websocket-parallel,websocket,locator,browser.click:blocked,2000.673,97,172,true +cdb-aligned-websocket-parallel,websocket,locator,browser.click:blocked,2002.514,97,172,true +cdb-aligned-websocket-parallel,websocket,locator,browser.click:blocked,2001.843,97,172,true +cdb-aligned-websocket-parallel,websocket,locator,browser.click:blocked,2001.219,97,172,true +cdb-aligned-websocket-parallel,websocket,locator,browser.click:blocked,2111.669,97,172,true +cdb-aligned-websocket-parallel,websocket,locator,browser.click:blocked,2001.559,97,172,true +cdb-aligned-websocket-parallel,websocket,locator,browser.click:blocked,2001.060,97,172,true +cdb-aligned-websocket-parallel,websocket,locator,browser.click:blocked,2002.314,97,172,true +cdb-aligned-websocket-parallel,websocket,locator,browser.click:blocked,2001.773,97,172,true +cdb-aligned-websocket-parallel,websocket,locator,browser.click:blocked,2001.696,97,172,true +cdb-aligned-websocket-parallel,websocket,locator,browser.click:blocked,2000.856,97,172,true +cdb-aligned-websocket-parallel,websocket,locator,browser.click:blocked,2001.205,97,172,true +cdb-aligned-devframe-parallel,devframe,ref,browser.fill,91.667,55,56,false +cdb-aligned-devframe-parallel,devframe,ref,browser.click,89.024,30,57,false +cdb-aligned-devframe-parallel,devframe,ref,browser.snapshot,624.362,18,456,false +cdb-aligned-devframe-parallel,devframe,ref,browser.batch,761.904,131,605,false +cdb-aligned-devframe-parallel,devframe,ref,browser.fill,95.069,55,56,false +cdb-aligned-devframe-parallel,devframe,ref,browser.click,90.354,30,57,false +cdb-aligned-devframe-parallel,devframe,ref,browser.snapshot,658.250,18,456,false +cdb-aligned-devframe-parallel,devframe,ref,browser.batch,790.348,131,605,false +cdb-aligned-devframe-parallel,devframe,ref,browser.fill,96.795,55,56,false +cdb-aligned-devframe-parallel,devframe,ref,browser.click,87.989,30,57,false +cdb-aligned-devframe-parallel,devframe,ref,browser.snapshot,614.183,18,456,false +cdb-aligned-devframe-parallel,devframe,ref,browser.batch,831.230,131,605,false +cdb-aligned-devframe-parallel,devframe,ref,browser.fill,96.125,55,56,false +cdb-aligned-devframe-parallel,devframe,ref,browser.click,86.960,30,57,false +cdb-aligned-devframe-parallel,devframe,ref,browser.snapshot,579.877,18,456,false +cdb-aligned-devframe-parallel,devframe,ref,browser.batch,728.778,131,605,false +cdb-aligned-devframe-parallel,devframe,ref,browser.fill,92.671,55,56,false +cdb-aligned-devframe-parallel,devframe,ref,browser.click,85.669,30,57,false +cdb-aligned-devframe-parallel,devframe,ref,browser.snapshot,596.535,18,457,false +cdb-aligned-devframe-parallel,devframe,ref,browser.batch,749.437,132,610,false +cdb-aligned-devframe-parallel,devframe,ref,browser.fill,88.100,56,56,false +cdb-aligned-devframe-parallel,devframe,ref,browser.click,85.844,31,57,false +cdb-aligned-devframe-parallel,devframe,ref,browser.snapshot,615.740,18,461,false +cdb-aligned-devframe-parallel,devframe,ref,browser.batch,811.460,133,610,false +cdb-aligned-devframe-parallel,devframe,ref,browser.fill,114.554,56,56,false +cdb-aligned-devframe-parallel,devframe,ref,browser.click,96.538,31,57,false +cdb-aligned-devframe-parallel,devframe,ref,browser.snapshot,869.858,18,461,false +cdb-aligned-devframe-parallel,devframe,ref,browser.batch,926.199,133,610,false +cdb-aligned-devframe-parallel,devframe,ref,browser.fill,121.933,56,56,false +cdb-aligned-devframe-parallel,devframe,ref,browser.click,89.147,31,57,false +cdb-aligned-devframe-parallel,devframe,ref,browser.snapshot,675.819,18,461,false +cdb-aligned-devframe-parallel,devframe,ref,browser.batch,826.541,133,610,false +cdb-aligned-devframe-parallel,devframe,ref,browser.fill,112.547,56,56,false +cdb-aligned-devframe-parallel,devframe,ref,browser.click,91.110,31,57,false +cdb-aligned-devframe-parallel,devframe,ref,browser.snapshot,646.759,18,461,false +cdb-aligned-devframe-parallel,devframe,ref,browser.batch,813.701,133,610,false +cdb-aligned-devframe-parallel,devframe,ref,browser.fill,97.617,56,56,false +cdb-aligned-devframe-parallel,devframe,ref,browser.click,90.717,31,57,false +cdb-aligned-devframe-parallel,devframe,ref,browser.snapshot,597.095,18,461,false +cdb-aligned-devframe-parallel,devframe,ref,browser.batch,806.210,133,610,false +cdb-aligned-devframe-parallel,devframe,ref,browser.fill,110.219,56,56,false +cdb-aligned-devframe-parallel,devframe,ref,browser.click,119.468,31,57,false +cdb-aligned-devframe-parallel,devframe,ref,browser.snapshot,629.906,18,461,false +cdb-aligned-devframe-parallel,devframe,ref,browser.batch,817.025,133,610,false +cdb-aligned-devframe-parallel,devframe,ref,browser.fill,94.006,56,56,false +cdb-aligned-devframe-parallel,devframe,ref,browser.click,94.755,31,57,false +cdb-aligned-devframe-parallel,devframe,ref,browser.snapshot,589.674,18,461,false +cdb-aligned-devframe-parallel,devframe,ref,browser.batch,739.625,133,610,false +cdb-aligned-devframe-parallel,devframe,ref,browser.fill,96.109,56,56,false +cdb-aligned-devframe-parallel,devframe,ref,browser.click,92.243,31,57,false +cdb-aligned-devframe-parallel,devframe,ref,browser.snapshot,580.142,18,461,false +cdb-aligned-devframe-parallel,devframe,ref,browser.batch,747.817,133,610,false +cdb-aligned-devframe-parallel,devframe,ref,browser.fill,92.797,56,56,false +cdb-aligned-devframe-parallel,devframe,ref,browser.click,90.340,31,57,false +cdb-aligned-devframe-parallel,devframe,ref,browser.snapshot,1434.461,18,461,false +cdb-aligned-devframe-parallel,devframe,ref,browser.batch,1369.162,133,610,false +cdb-aligned-devframe-parallel,devframe,ref,browser.fill,113.176,56,56,false +cdb-aligned-devframe-parallel,devframe,ref,browser.click,106.822,31,57,false +cdb-aligned-devframe-parallel,devframe,ref,browser.snapshot,624.884,18,461,false +cdb-aligned-devframe-parallel,devframe,ref,browser.batch,823.458,133,610,false +cdb-aligned-devframe-parallel,devframe,ref,browser.fill,90.845,56,56,false +cdb-aligned-devframe-parallel,devframe,ref,browser.click,89.941,31,57,false +cdb-aligned-devframe-parallel,devframe,ref,browser.snapshot,593.698,18,461,false +cdb-aligned-devframe-parallel,devframe,ref,browser.batch,735.836,133,610,false +cdb-aligned-devframe-parallel,devframe,ref,browser.fill,99.790,56,56,false +cdb-aligned-devframe-parallel,devframe,ref,browser.click,127.825,31,57,false +cdb-aligned-devframe-parallel,devframe,ref,browser.snapshot,608.446,18,461,false +cdb-aligned-devframe-parallel,devframe,ref,browser.batch,761.048,133,610,false +cdb-aligned-devframe-parallel,devframe,ref,browser.fill,95.144,56,56,false +cdb-aligned-devframe-parallel,devframe,ref,browser.click,87.736,31,57,false +cdb-aligned-devframe-parallel,devframe,ref,browser.snapshot,639.191,18,461,false +cdb-aligned-devframe-parallel,devframe,ref,browser.batch,741.015,133,610,false +cdb-aligned-devframe-parallel,devframe,ref,browser.fill,118.654,56,56,false +cdb-aligned-devframe-parallel,devframe,ref,browser.click,91.948,31,57,false +cdb-aligned-devframe-parallel,devframe,ref,browser.snapshot,585.938,18,461,false +cdb-aligned-devframe-parallel,devframe,ref,browser.batch,758.183,133,610,false +cdb-aligned-devframe-parallel,devframe,ref,browser.fill,104.214,56,56,false +cdb-aligned-devframe-parallel,devframe,ref,browser.click,84.800,31,57,false +cdb-aligned-devframe-parallel,devframe,ref,browser.snapshot,1931.217,18,461,false +cdb-aligned-devframe-parallel,devframe,ref,browser.batch,1278.607,133,610,false +cdb-aligned-devframe-parallel,devframe,ref,browser.fill,632.577,56,56,false +cdb-aligned-devframe-parallel,devframe,ref,browser.click,105.073,31,57,false +cdb-aligned-devframe-parallel,devframe,ref,browser.snapshot,660.242,18,461,false +cdb-aligned-devframe-parallel,devframe,ref,browser.batch,826.971,133,610,false +cdb-aligned-devframe-parallel,devframe,ref,browser.fill,92.506,56,56,false +cdb-aligned-devframe-parallel,devframe,ref,browser.click,89.643,31,57,false +cdb-aligned-devframe-parallel,devframe,ref,browser.snapshot,668.048,18,461,false +cdb-aligned-devframe-parallel,devframe,ref,browser.batch,796.927,133,610,false +cdb-aligned-devframe-parallel,devframe,ref,browser.fill,103.884,56,56,false +cdb-aligned-devframe-parallel,devframe,ref,browser.click,90.197,31,57,false +cdb-aligned-devframe-parallel,devframe,ref,browser.snapshot,671.967,18,461,false +cdb-aligned-devframe-parallel,devframe,ref,browser.batch,864.170,133,610,false +cdb-aligned-devframe-parallel,devframe,ref,browser.fill,97.514,56,56,false +cdb-aligned-devframe-parallel,devframe,ref,browser.click,106.835,31,57,false +cdb-aligned-devframe-parallel,devframe,ref,browser.snapshot,743.842,18,461,false +cdb-aligned-devframe-parallel,devframe,ref,browser.batch,978.128,133,610,false +cdb-aligned-devframe-parallel,devframe,ref,browser.fill,131.384,56,56,false +cdb-aligned-devframe-parallel,devframe,ref,browser.click,116.636,31,57,false +cdb-aligned-devframe-parallel,devframe,ref,browser.snapshot,711.136,18,461,false +cdb-aligned-devframe-parallel,devframe,ref,browser.batch,814.178,133,610,false +cdb-aligned-devframe-parallel,devframe,ref,browser.fill,96.480,56,56,false +cdb-aligned-devframe-parallel,devframe,ref,browser.click,88.202,31,57,false +cdb-aligned-devframe-parallel,devframe,ref,browser.snapshot,609.013,18,461,false +cdb-aligned-devframe-parallel,devframe,ref,browser.batch,745.690,133,610,false +cdb-aligned-devframe-parallel,devframe,ref,browser.fill,102.351,56,56,false +cdb-aligned-devframe-parallel,devframe,ref,browser.click,88.616,31,57,false +cdb-aligned-devframe-parallel,devframe,ref,browser.snapshot,633.731,18,461,false +cdb-aligned-devframe-parallel,devframe,ref,browser.batch,797.594,133,610,false +cdb-aligned-devframe-parallel,devframe,ref,browser.fill,90.237,56,56,false +cdb-aligned-devframe-parallel,devframe,ref,browser.click,89.141,31,57,false +cdb-aligned-devframe-parallel,devframe,ref,browser.snapshot,619.259,18,461,false +cdb-aligned-devframe-parallel,devframe,ref,browser.batch,779.927,133,610,false +cdb-aligned-devframe-parallel,devframe,ref,browser.fill,98.087,56,56,false +cdb-aligned-devframe-parallel,devframe,ref,browser.click,87.773,31,57,false +cdb-aligned-devframe-parallel,devframe,ref,browser.snapshot,605.417,18,461,false +cdb-aligned-devframe-parallel,devframe,ref,browser.batch,800.567,133,610,false +cdb-aligned-devframe-parallel,devframe,ref,browser.fill,94.995,56,56,false +cdb-aligned-devframe-parallel,devframe,ref,browser.click,88.496,31,57,false +cdb-aligned-devframe-parallel,devframe,ref,browser.snapshot,624.103,18,461,false +cdb-aligned-devframe-parallel,devframe,ref,browser.batch,763.434,133,610,false +cdb-aligned-devframe-parallel,devframe,ref,browser.click:blocked,2002.001,31,197,true +cdb-aligned-devframe-parallel,devframe,ref,browser.click:blocked,2002.302,31,197,true +cdb-aligned-devframe-parallel,devframe,ref,browser.click:blocked,2004.067,31,197,true +cdb-aligned-devframe-parallel,devframe,ref,browser.click:blocked,2003.002,31,197,true +cdb-aligned-devframe-parallel,devframe,ref,browser.click:blocked,2002.472,31,197,true +cdb-aligned-devframe-parallel,devframe,ref,browser.click:blocked,2003.026,31,197,true +cdb-aligned-devframe-parallel,devframe,ref,browser.click:blocked,2003.095,31,197,true +cdb-aligned-devframe-parallel,devframe,ref,browser.click:blocked,2001.926,31,197,true +cdb-aligned-devframe-parallel,devframe,ref,browser.click:blocked,2002.958,31,197,true +cdb-aligned-devframe-parallel,devframe,ref,browser.click:blocked,2002.513,31,197,true +cdb-aligned-devframe-parallel,devframe,ref,browser.click:blocked,2003.166,31,197,true +cdb-aligned-devframe-parallel,devframe,ref,browser.click:blocked,2006.542,31,197,true +cdb-aligned-devframe-parallel,devframe,ref,browser.click:blocked,2002.286,31,197,true +cdb-aligned-devframe-parallel,devframe,ref,browser.click:blocked,2001.347,31,197,true +cdb-aligned-devframe-parallel,devframe,ref,browser.click:blocked,2005.214,31,197,true +cdb-aligned-devframe-parallel,devframe,ref,browser.click:blocked,2007.669,31,197,true +cdb-aligned-devframe-parallel,devframe,ref,browser.click:blocked,2003.104,31,197,true +cdb-aligned-devframe-parallel,devframe,ref,browser.click:blocked,2004.251,31,197,true +cdb-aligned-devframe-parallel,devframe,ref,browser.click:blocked,2001.415,31,197,true +cdb-aligned-devframe-parallel,devframe,ref,browser.click:blocked,2003.275,31,197,true +cdb-aligned-devframe-parallel,devframe,ref,browser.click:blocked,2003.024,31,197,true +cdb-aligned-devframe-parallel,devframe,ref,browser.click:blocked,2002.451,31,197,true +cdb-aligned-devframe-parallel,devframe,ref,browser.click:blocked,2003.767,31,197,true +cdb-aligned-devframe-parallel,devframe,ref,browser.click:blocked,2001.129,31,197,true +cdb-aligned-devframe-parallel,devframe,ref,browser.click:blocked,2004.473,31,197,true +cdb-aligned-devframe-parallel,devframe,ref,browser.click:blocked,2002.208,31,197,true +cdb-aligned-devframe-parallel,devframe,ref,browser.click:blocked,2003.079,31,197,true +cdb-aligned-devframe-parallel,devframe,ref,browser.click:blocked,2005.112,31,197,true +cdb-aligned-devframe-parallel,devframe,ref,browser.click:blocked,2003.347,31,197,true +cdb-aligned-devframe-parallel,devframe,ref,browser.click:blocked,2017.630,31,197,true +cdb-aligned-devframe-parallel,devframe,locator,browser.fill,343.878,118,56,false +cdb-aligned-devframe-parallel,devframe,locator,browser.click,362.447,97,57,false +cdb-aligned-devframe-parallel,devframe,locator,browser.snapshot,649.495,18,461,false +cdb-aligned-devframe-parallel,devframe,locator,browser.batch,1200.727,261,610,false +cdb-aligned-devframe-parallel,devframe,locator,browser.fill,301.832,118,56,false +cdb-aligned-devframe-parallel,devframe,locator,browser.click,306.418,97,57,false +cdb-aligned-devframe-parallel,devframe,locator,browser.snapshot,612.640,18,461,false +cdb-aligned-devframe-parallel,devframe,locator,browser.batch,1185.483,261,610,false +cdb-aligned-devframe-parallel,devframe,locator,browser.fill,415.333,118,56,false +cdb-aligned-devframe-parallel,devframe,locator,browser.click,384.484,97,57,false +cdb-aligned-devframe-parallel,devframe,locator,browser.snapshot,605.616,18,461,false +cdb-aligned-devframe-parallel,devframe,locator,browser.batch,1110.596,261,610,false +cdb-aligned-devframe-parallel,devframe,locator,browser.fill,254.772,118,56,false +cdb-aligned-devframe-parallel,devframe,locator,browser.click,321.904,97,57,false +cdb-aligned-devframe-parallel,devframe,locator,browser.snapshot,607.333,18,461,false +cdb-aligned-devframe-parallel,devframe,locator,browser.batch,1110.273,261,610,false +cdb-aligned-devframe-parallel,devframe,locator,browser.fill,355.504,118,56,false +cdb-aligned-devframe-parallel,devframe,locator,browser.click,345.351,97,57,false +cdb-aligned-devframe-parallel,devframe,locator,browser.snapshot,650.820,18,461,false +cdb-aligned-devframe-parallel,devframe,locator,browser.batch,1250.533,261,610,false +cdb-aligned-devframe-parallel,devframe,locator,browser.fill,349.912,118,56,false +cdb-aligned-devframe-parallel,devframe,locator,browser.click,332.918,97,57,false +cdb-aligned-devframe-parallel,devframe,locator,browser.snapshot,615.227,18,461,false +cdb-aligned-devframe-parallel,devframe,locator,browser.batch,1147.195,261,610,false +cdb-aligned-devframe-parallel,devframe,locator,browser.fill,294.970,118,56,false +cdb-aligned-devframe-parallel,devframe,locator,browser.click,328.172,97,57,false +cdb-aligned-devframe-parallel,devframe,locator,browser.snapshot,590.474,18,461,false +cdb-aligned-devframe-parallel,devframe,locator,browser.batch,1163.189,261,610,false +cdb-aligned-devframe-parallel,devframe,locator,browser.fill,298.989,118,56,false +cdb-aligned-devframe-parallel,devframe,locator,browser.click,303.981,97,57,false +cdb-aligned-devframe-parallel,devframe,locator,browser.snapshot,609.896,18,461,false +cdb-aligned-devframe-parallel,devframe,locator,browser.batch,1139.502,261,610,false +cdb-aligned-devframe-parallel,devframe,locator,browser.fill,262.027,118,56,false +cdb-aligned-devframe-parallel,devframe,locator,browser.click,273.683,97,57,false +cdb-aligned-devframe-parallel,devframe,locator,browser.snapshot,738.023,18,461,false +cdb-aligned-devframe-parallel,devframe,locator,browser.batch,1194.548,261,610,false +cdb-aligned-devframe-parallel,devframe,locator,browser.fill,282.162,118,56,false +cdb-aligned-devframe-parallel,devframe,locator,browser.click,372.218,97,57,false +cdb-aligned-devframe-parallel,devframe,locator,browser.snapshot,743.125,18,461,false +cdb-aligned-devframe-parallel,devframe,locator,browser.batch,1151.916,261,610,false +cdb-aligned-devframe-parallel,devframe,locator,browser.fill,342.405,118,56,false +cdb-aligned-devframe-parallel,devframe,locator,browser.click,264.983,97,57,false +cdb-aligned-devframe-parallel,devframe,locator,browser.snapshot,654.609,18,461,false +cdb-aligned-devframe-parallel,devframe,locator,browser.batch,1128.951,261,610,false +cdb-aligned-devframe-parallel,devframe,locator,browser.fill,342.101,118,56,false +cdb-aligned-devframe-parallel,devframe,locator,browser.click,313.383,97,57,false +cdb-aligned-devframe-parallel,devframe,locator,browser.snapshot,634.689,18,461,false +cdb-aligned-devframe-parallel,devframe,locator,browser.batch,1043.351,261,610,false +cdb-aligned-devframe-parallel,devframe,locator,browser.fill,336.614,118,56,false +cdb-aligned-devframe-parallel,devframe,locator,browser.click,337.122,97,57,false +cdb-aligned-devframe-parallel,devframe,locator,browser.snapshot,611.129,18,461,false +cdb-aligned-devframe-parallel,devframe,locator,browser.batch,1087.987,261,610,false +cdb-aligned-devframe-parallel,devframe,locator,browser.fill,359.423,118,56,false +cdb-aligned-devframe-parallel,devframe,locator,browser.click,360.064,97,57,false +cdb-aligned-devframe-parallel,devframe,locator,browser.snapshot,657.165,18,461,false +cdb-aligned-devframe-parallel,devframe,locator,browser.batch,1268.609,261,610,false +cdb-aligned-devframe-parallel,devframe,locator,browser.fill,313.408,118,56,false +cdb-aligned-devframe-parallel,devframe,locator,browser.click,290.847,97,57,false +cdb-aligned-devframe-parallel,devframe,locator,browser.snapshot,593.930,18,461,false +cdb-aligned-devframe-parallel,devframe,locator,browser.batch,1147.832,261,610,false +cdb-aligned-devframe-parallel,devframe,locator,browser.fill,307.000,118,56,false +cdb-aligned-devframe-parallel,devframe,locator,browser.click,327.856,97,57,false +cdb-aligned-devframe-parallel,devframe,locator,browser.snapshot,650.090,18,461,false +cdb-aligned-devframe-parallel,devframe,locator,browser.batch,1123.516,261,610,false +cdb-aligned-devframe-parallel,devframe,locator,browser.fill,368.030,118,56,false +cdb-aligned-devframe-parallel,devframe,locator,browser.click,293.225,97,57,false +cdb-aligned-devframe-parallel,devframe,locator,browser.snapshot,589.074,18,461,false +cdb-aligned-devframe-parallel,devframe,locator,browser.batch,1170.615,261,610,false +cdb-aligned-devframe-parallel,devframe,locator,browser.fill,281.768,118,56,false +cdb-aligned-devframe-parallel,devframe,locator,browser.click,338.813,97,57,false +cdb-aligned-devframe-parallel,devframe,locator,browser.snapshot,604.913,18,461,false +cdb-aligned-devframe-parallel,devframe,locator,browser.batch,1129.521,261,610,false +cdb-aligned-devframe-parallel,devframe,locator,browser.fill,341.171,118,56,false +cdb-aligned-devframe-parallel,devframe,locator,browser.click,344.287,97,57,false +cdb-aligned-devframe-parallel,devframe,locator,browser.snapshot,614.663,18,461,false +cdb-aligned-devframe-parallel,devframe,locator,browser.batch,1111.778,261,610,false +cdb-aligned-devframe-parallel,devframe,locator,browser.fill,344.815,118,56,false +cdb-aligned-devframe-parallel,devframe,locator,browser.click,313.572,97,57,false +cdb-aligned-devframe-parallel,devframe,locator,browser.snapshot,597.834,18,461,false +cdb-aligned-devframe-parallel,devframe,locator,browser.batch,1171.382,261,610,false +cdb-aligned-devframe-parallel,devframe,locator,browser.fill,302.477,118,56,false +cdb-aligned-devframe-parallel,devframe,locator,browser.click,349.445,97,57,false +cdb-aligned-devframe-parallel,devframe,locator,browser.snapshot,929.356,18,461,false +cdb-aligned-devframe-parallel,devframe,locator,browser.batch,1134.051,261,610,false +cdb-aligned-devframe-parallel,devframe,locator,browser.fill,275.040,118,56,false +cdb-aligned-devframe-parallel,devframe,locator,browser.click,253.006,97,57,false +cdb-aligned-devframe-parallel,devframe,locator,browser.snapshot,636.430,18,461,false +cdb-aligned-devframe-parallel,devframe,locator,browser.batch,1253.700,261,610,false +cdb-aligned-devframe-parallel,devframe,locator,browser.fill,324.376,118,56,false +cdb-aligned-devframe-parallel,devframe,locator,browser.click,404.584,97,57,false +cdb-aligned-devframe-parallel,devframe,locator,browser.snapshot,624.552,18,461,false +cdb-aligned-devframe-parallel,devframe,locator,browser.batch,1120.869,261,610,false +cdb-aligned-devframe-parallel,devframe,locator,browser.fill,322.219,118,56,false +cdb-aligned-devframe-parallel,devframe,locator,browser.click,325.481,97,57,false +cdb-aligned-devframe-parallel,devframe,locator,browser.snapshot,651.269,18,461,false +cdb-aligned-devframe-parallel,devframe,locator,browser.batch,1169.016,261,610,false +cdb-aligned-devframe-parallel,devframe,locator,browser.fill,304.001,118,56,false +cdb-aligned-devframe-parallel,devframe,locator,browser.click,311.858,97,57,false +cdb-aligned-devframe-parallel,devframe,locator,browser.snapshot,628.319,18,461,false +cdb-aligned-devframe-parallel,devframe,locator,browser.batch,1229.689,261,610,false +cdb-aligned-devframe-parallel,devframe,locator,browser.fill,359.483,118,56,false +cdb-aligned-devframe-parallel,devframe,locator,browser.click,323.923,97,57,false +cdb-aligned-devframe-parallel,devframe,locator,browser.snapshot,634.239,18,461,false +cdb-aligned-devframe-parallel,devframe,locator,browser.batch,1200.666,261,610,false +cdb-aligned-devframe-parallel,devframe,locator,browser.fill,313.036,118,56,false +cdb-aligned-devframe-parallel,devframe,locator,browser.click,342.929,97,57,false +cdb-aligned-devframe-parallel,devframe,locator,browser.snapshot,610.055,18,461,false +cdb-aligned-devframe-parallel,devframe,locator,browser.batch,1185.509,261,610,false +cdb-aligned-devframe-parallel,devframe,locator,browser.fill,318.649,118,56,false +cdb-aligned-devframe-parallel,devframe,locator,browser.click,297.043,97,57,false +cdb-aligned-devframe-parallel,devframe,locator,browser.snapshot,601.170,18,461,false +cdb-aligned-devframe-parallel,devframe,locator,browser.batch,1081.488,261,610,false +cdb-aligned-devframe-parallel,devframe,locator,browser.fill,266.779,118,56,false +cdb-aligned-devframe-parallel,devframe,locator,browser.click,294.073,97,57,false +cdb-aligned-devframe-parallel,devframe,locator,browser.snapshot,609.492,18,461,false +cdb-aligned-devframe-parallel,devframe,locator,browser.batch,1175.130,261,610,false +cdb-aligned-devframe-parallel,devframe,locator,browser.fill,342.626,118,56,false +cdb-aligned-devframe-parallel,devframe,locator,browser.click,343.571,97,57,false +cdb-aligned-devframe-parallel,devframe,locator,browser.snapshot,613.587,18,461,false +cdb-aligned-devframe-parallel,devframe,locator,browser.batch,1098.826,261,610,false +cdb-aligned-devframe-parallel,devframe,locator,browser.click:blocked,2182.087,97,197,true +cdb-aligned-devframe-parallel,devframe,locator,browser.click:blocked,2001.650,97,197,true +cdb-aligned-devframe-parallel,devframe,locator,browser.click:blocked,2037.990,97,197,true +cdb-aligned-devframe-parallel,devframe,locator,browser.click:blocked,2045.566,97,197,true +cdb-aligned-devframe-parallel,devframe,locator,browser.click:blocked,2007.097,97,197,true +cdb-aligned-devframe-parallel,devframe,locator,browser.click:blocked,2130.200,97,197,true +cdb-aligned-devframe-parallel,devframe,locator,browser.click:blocked,2001.966,97,197,true +cdb-aligned-devframe-parallel,devframe,locator,browser.click:blocked,2002.780,97,197,true +cdb-aligned-devframe-parallel,devframe,locator,browser.click:blocked,2002.336,97,197,true +cdb-aligned-devframe-parallel,devframe,locator,browser.click:blocked,2002.173,97,197,true +cdb-aligned-devframe-parallel,devframe,locator,browser.click:blocked,2019.244,97,197,true +cdb-aligned-devframe-parallel,devframe,locator,browser.click:blocked,2002.413,97,197,true +cdb-aligned-devframe-parallel,devframe,locator,browser.click:blocked,2002.317,97,197,true +cdb-aligned-devframe-parallel,devframe,locator,browser.click:blocked,2037.278,97,197,true +cdb-aligned-devframe-parallel,devframe,locator,browser.click:blocked,2000.562,97,197,true +cdb-aligned-devframe-parallel,devframe,locator,browser.click:blocked,2002.319,97,197,true +cdb-aligned-devframe-parallel,devframe,locator,browser.click:blocked,2003.948,97,197,true +cdb-aligned-devframe-parallel,devframe,locator,browser.click:blocked,2003.444,97,197,true +cdb-aligned-devframe-parallel,devframe,locator,browser.click:blocked,2002.363,97,197,true +cdb-aligned-devframe-parallel,devframe,locator,browser.click:blocked,2000.596,97,197,true +cdb-aligned-devframe-parallel,devframe,locator,browser.click:blocked,2001.707,97,197,true +cdb-aligned-devframe-parallel,devframe,locator,browser.click:blocked,2004.356,97,197,true +cdb-aligned-devframe-parallel,devframe,locator,browser.click:blocked,2002.351,97,197,true +cdb-aligned-devframe-parallel,devframe,locator,browser.click:blocked,2000.900,97,197,true +cdb-aligned-devframe-parallel,devframe,locator,browser.click:blocked,2000.664,97,197,true +cdb-aligned-devframe-parallel,devframe,locator,browser.click:blocked,2001.348,97,197,true +cdb-aligned-devframe-parallel,devframe,locator,browser.click:blocked,2006.980,97,197,true +cdb-aligned-devframe-parallel,devframe,locator,browser.click:blocked,2002.055,97,197,true +cdb-aligned-devframe-parallel,devframe,locator,browser.click:blocked,2001.101,97,197,true +cdb-aligned-devframe-parallel,devframe,locator,browser.click:blocked,2000.630,97,197,true +cdb-aligned-websocket-attributed,websocket,ref,browser.fill,103.258,55,56,false +cdb-aligned-websocket-attributed,websocket,ref,browser.click,94.940,30,57,false +cdb-aligned-websocket-attributed,websocket,ref,browser.snapshot,636.357,18,456,false +cdb-aligned-websocket-attributed,websocket,ref,browser.batch,865.627,131,605,false +cdb-aligned-websocket-attributed,websocket,ref,browser.fill,102.306,55,56,false +cdb-aligned-websocket-attributed,websocket,ref,browser.click,94.840,30,57,false +cdb-aligned-websocket-attributed,websocket,ref,browser.snapshot,718.189,18,456,false +cdb-aligned-websocket-attributed,websocket,ref,browser.batch,953.304,131,605,false +cdb-aligned-websocket-attributed,websocket,ref,browser.fill,112.192,55,56,false +cdb-aligned-websocket-attributed,websocket,ref,browser.click,114.723,30,57,false +cdb-aligned-websocket-attributed,websocket,ref,browser.snapshot,1028.246,18,456,false +cdb-aligned-websocket-attributed,websocket,ref,browser.batch,1202.970,131,605,false +cdb-aligned-websocket-attributed,websocket,ref,browser.fill,98.377,55,56,false +cdb-aligned-websocket-attributed,websocket,ref,browser.click,92.004,30,57,false +cdb-aligned-websocket-attributed,websocket,ref,browser.snapshot,708.976,18,456,false +cdb-aligned-websocket-attributed,websocket,ref,browser.batch,856.520,131,605,false +cdb-aligned-websocket-attributed,websocket,ref,browser.fill,138.074,55,56,false +cdb-aligned-websocket-attributed,websocket,ref,browser.click,99.120,30,57,false +cdb-aligned-websocket-attributed,websocket,ref,browser.snapshot,634.194,18,457,false +cdb-aligned-websocket-attributed,websocket,ref,browser.batch,919.388,132,610,false +cdb-aligned-websocket-attributed,websocket,ref,browser.fill,115.756,56,56,false +cdb-aligned-websocket-attributed,websocket,ref,browser.click,91.099,31,57,false +cdb-aligned-websocket-attributed,websocket,ref,browser.snapshot,662.508,18,461,false +cdb-aligned-websocket-attributed,websocket,ref,browser.batch,826.678,133,610,false +cdb-aligned-websocket-attributed,websocket,ref,browser.fill,114.837,56,56,false +cdb-aligned-websocket-attributed,websocket,ref,browser.click,99.950,31,57,false +cdb-aligned-websocket-attributed,websocket,ref,browser.snapshot,627.123,18,461,false +cdb-aligned-websocket-attributed,websocket,ref,browser.batch,801.874,133,610,false +cdb-aligned-websocket-attributed,websocket,ref,browser.fill,103.737,56,56,false +cdb-aligned-websocket-attributed,websocket,ref,browser.click,96.900,31,57,false +cdb-aligned-websocket-attributed,websocket,ref,browser.snapshot,2427.297,18,461,false +cdb-aligned-websocket-attributed,websocket,ref,browser.batch,1237.249,133,610,false +cdb-aligned-websocket-attributed,websocket,ref,browser.fill,122.049,56,56,false +cdb-aligned-websocket-attributed,websocket,ref,browser.click,101.877,31,57,false +cdb-aligned-websocket-attributed,websocket,ref,browser.snapshot,823.780,18,461,false +cdb-aligned-websocket-attributed,websocket,ref,browser.batch,877.168,133,610,false +cdb-aligned-websocket-attributed,websocket,ref,browser.fill,112.004,56,56,false +cdb-aligned-websocket-attributed,websocket,ref,browser.click,93.773,31,57,false +cdb-aligned-websocket-attributed,websocket,ref,browser.snapshot,624.199,18,461,false +cdb-aligned-websocket-attributed,websocket,ref,browser.batch,811.891,133,610,false +cdb-aligned-websocket-attributed,websocket,ref,browser.fill,111.934,56,56,false +cdb-aligned-websocket-attributed,websocket,ref,browser.click,101.350,31,57,false +cdb-aligned-websocket-attributed,websocket,ref,browser.snapshot,654.585,18,461,false +cdb-aligned-websocket-attributed,websocket,ref,browser.batch,804.201,133,610,false +cdb-aligned-websocket-attributed,websocket,ref,browser.fill,100.958,56,56,false +cdb-aligned-websocket-attributed,websocket,ref,browser.click,94.201,31,57,false +cdb-aligned-websocket-attributed,websocket,ref,browser.snapshot,879.862,18,461,false +cdb-aligned-websocket-attributed,websocket,ref,browser.batch,827.983,133,610,false +cdb-aligned-websocket-attributed,websocket,ref,browser.fill,99.623,56,56,false +cdb-aligned-websocket-attributed,websocket,ref,browser.click,93.493,31,57,false +cdb-aligned-websocket-attributed,websocket,ref,browser.snapshot,673.934,18,461,false +cdb-aligned-websocket-attributed,websocket,ref,browser.batch,838.780,133,610,false +cdb-aligned-websocket-attributed,websocket,ref,browser.fill,105.428,56,56,false +cdb-aligned-websocket-attributed,websocket,ref,browser.click,98.773,31,57,false +cdb-aligned-websocket-attributed,websocket,ref,browser.snapshot,710.341,18,461,false +cdb-aligned-websocket-attributed,websocket,ref,browser.batch,835.751,133,610,false +cdb-aligned-websocket-attributed,websocket,ref,browser.fill,156.116,56,56,false +cdb-aligned-websocket-attributed,websocket,ref,browser.click,107.675,31,57,false +cdb-aligned-websocket-attributed,websocket,ref,browser.snapshot,650.444,18,461,false +cdb-aligned-websocket-attributed,websocket,ref,browser.batch,832.707,133,610,false +cdb-aligned-websocket-attributed,websocket,ref,browser.fill,105.839,56,56,false +cdb-aligned-websocket-attributed,websocket,ref,browser.click,95.926,31,57,false +cdb-aligned-websocket-attributed,websocket,ref,browser.snapshot,741.816,18,461,false +cdb-aligned-websocket-attributed,websocket,ref,browser.batch,1077.123,133,610,false +cdb-aligned-websocket-attributed,websocket,ref,browser.fill,151.081,56,56,false +cdb-aligned-websocket-attributed,websocket,ref,browser.click,266.554,31,57,false +cdb-aligned-websocket-attributed,websocket,ref,browser.snapshot,665.825,18,461,false +cdb-aligned-websocket-attributed,websocket,ref,browser.batch,826.429,133,610,false +cdb-aligned-websocket-attributed,websocket,ref,browser.fill,102.426,56,56,false +cdb-aligned-websocket-attributed,websocket,ref,browser.click,96.088,31,57,false +cdb-aligned-websocket-attributed,websocket,ref,browser.snapshot,622.195,18,461,false +cdb-aligned-websocket-attributed,websocket,ref,browser.batch,791.579,133,610,false +cdb-aligned-websocket-attributed,websocket,ref,browser.fill,103.570,56,56,false +cdb-aligned-websocket-attributed,websocket,ref,browser.click,97.441,31,57,false +cdb-aligned-websocket-attributed,websocket,ref,browser.snapshot,665.735,18,461,false +cdb-aligned-websocket-attributed,websocket,ref,browser.batch,786.593,133,610,false +cdb-aligned-websocket-attributed,websocket,ref,browser.fill,104.148,56,56,false +cdb-aligned-websocket-attributed,websocket,ref,browser.click,97.323,31,57,false +cdb-aligned-websocket-attributed,websocket,ref,browser.snapshot,647.717,18,461,false +cdb-aligned-websocket-attributed,websocket,ref,browser.batch,865.300,133,610,false +cdb-aligned-websocket-attributed,websocket,ref,browser.fill,105.431,56,56,false +cdb-aligned-websocket-attributed,websocket,ref,browser.click,96.118,31,57,false +cdb-aligned-websocket-attributed,websocket,ref,browser.snapshot,616.847,18,461,false +cdb-aligned-websocket-attributed,websocket,ref,browser.batch,829.604,133,610,false +cdb-aligned-websocket-attributed,websocket,ref,browser.fill,103.847,56,56,false +cdb-aligned-websocket-attributed,websocket,ref,browser.click,94.106,31,57,false +cdb-aligned-websocket-attributed,websocket,ref,browser.snapshot,603.722,18,461,false +cdb-aligned-websocket-attributed,websocket,ref,browser.batch,848.320,133,610,false +cdb-aligned-websocket-attributed,websocket,ref,browser.fill,109.601,56,56,false +cdb-aligned-websocket-attributed,websocket,ref,browser.click,118.011,31,57,false +cdb-aligned-websocket-attributed,websocket,ref,browser.snapshot,668.020,18,461,false +cdb-aligned-websocket-attributed,websocket,ref,browser.batch,875.843,133,610,false +cdb-aligned-websocket-attributed,websocket,ref,browser.fill,100.785,56,56,false +cdb-aligned-websocket-attributed,websocket,ref,browser.click,96.035,31,57,false +cdb-aligned-websocket-attributed,websocket,ref,browser.snapshot,647.468,18,461,false +cdb-aligned-websocket-attributed,websocket,ref,browser.batch,822.678,133,610,false +cdb-aligned-websocket-attributed,websocket,ref,browser.fill,109.784,56,56,false +cdb-aligned-websocket-attributed,websocket,ref,browser.click,104.624,31,57,false +cdb-aligned-websocket-attributed,websocket,ref,browser.snapshot,953.441,18,461,false +cdb-aligned-websocket-attributed,websocket,ref,browser.batch,1258.872,133,610,false +cdb-aligned-websocket-attributed,websocket,ref,browser.fill,220.714,56,56,false +cdb-aligned-websocket-attributed,websocket,ref,browser.click,130.249,31,57,false +cdb-aligned-websocket-attributed,websocket,ref,browser.snapshot,732.507,18,461,false +cdb-aligned-websocket-attributed,websocket,ref,browser.batch,857.794,133,610,false +cdb-aligned-websocket-attributed,websocket,ref,browser.fill,97.840,56,56,false +cdb-aligned-websocket-attributed,websocket,ref,browser.click,94.655,31,57,false +cdb-aligned-websocket-attributed,websocket,ref,browser.snapshot,610.141,18,461,false +cdb-aligned-websocket-attributed,websocket,ref,browser.batch,771.345,133,610,false +cdb-aligned-websocket-attributed,websocket,ref,browser.fill,100.159,56,56,false +cdb-aligned-websocket-attributed,websocket,ref,browser.click,94.854,31,57,false +cdb-aligned-websocket-attributed,websocket,ref,browser.snapshot,612.751,18,461,false +cdb-aligned-websocket-attributed,websocket,ref,browser.batch,801.646,133,610,false +cdb-aligned-websocket-attributed,websocket,ref,browser.fill,108.043,56,56,false +cdb-aligned-websocket-attributed,websocket,ref,browser.click,96.888,31,57,false +cdb-aligned-websocket-attributed,websocket,ref,browser.snapshot,619.244,18,461,false +cdb-aligned-websocket-attributed,websocket,ref,browser.batch,800.156,133,610,false +cdb-aligned-websocket-attributed,websocket,ref,browser.fill,101.590,56,56,false +cdb-aligned-websocket-attributed,websocket,ref,browser.click,93.134,31,57,false +cdb-aligned-websocket-attributed,websocket,ref,browser.snapshot,631.900,18,461,false +cdb-aligned-websocket-attributed,websocket,ref,browser.batch,810.773,133,610,false +cdb-aligned-websocket-attributed,websocket,ref,browser.click:blocked,2032.906,31,172,true +cdb-aligned-websocket-attributed,websocket,ref,browser.click:blocked,2000.576,31,172,true +cdb-aligned-websocket-attributed,websocket,ref,browser.click:blocked,2005.371,31,172,true +cdb-aligned-websocket-attributed,websocket,ref,browser.click:blocked,2046.032,31,172,true +cdb-aligned-websocket-attributed,websocket,ref,browser.click:blocked,2035.559,31,172,true +cdb-aligned-websocket-attributed,websocket,ref,browser.click:blocked,2001.917,31,172,true +cdb-aligned-websocket-attributed,websocket,ref,browser.click:blocked,2000.423,31,172,true +cdb-aligned-websocket-attributed,websocket,ref,browser.click:blocked,2002.062,31,172,true +cdb-aligned-websocket-attributed,websocket,ref,browser.click:blocked,2001.126,31,172,true +cdb-aligned-websocket-attributed,websocket,ref,browser.click:blocked,2021.679,31,172,true +cdb-aligned-websocket-attributed,websocket,ref,browser.click:blocked,2001.973,31,172,true +cdb-aligned-websocket-attributed,websocket,ref,browser.click:blocked,2000.557,31,172,true +cdb-aligned-websocket-attributed,websocket,ref,browser.click:blocked,2002.112,31,172,true +cdb-aligned-websocket-attributed,websocket,ref,browser.click:blocked,2002.362,31,172,true +cdb-aligned-websocket-attributed,websocket,ref,browser.click:blocked,2002.473,31,172,true +cdb-aligned-websocket-attributed,websocket,ref,browser.click:blocked,2001.013,31,172,true +cdb-aligned-websocket-attributed,websocket,ref,browser.click:blocked,2002.183,31,172,true +cdb-aligned-websocket-attributed,websocket,ref,browser.click:blocked,2001.452,31,172,true +cdb-aligned-websocket-attributed,websocket,ref,browser.click:blocked,2002.190,31,172,true +cdb-aligned-websocket-attributed,websocket,ref,browser.click:blocked,2003.883,31,172,true +cdb-aligned-websocket-attributed,websocket,ref,browser.click:blocked,2002.507,31,172,true +cdb-aligned-websocket-attributed,websocket,ref,browser.click:blocked,2003.331,31,172,true +cdb-aligned-websocket-attributed,websocket,ref,browser.click:blocked,2000.338,31,172,true +cdb-aligned-websocket-attributed,websocket,ref,browser.click:blocked,1999.777,31,172,true +cdb-aligned-websocket-attributed,websocket,ref,browser.click:blocked,2001.484,31,172,true +cdb-aligned-websocket-attributed,websocket,ref,browser.click:blocked,2519.523,31,172,true +cdb-aligned-websocket-attributed,websocket,ref,browser.click:blocked,2017.995,31,172,true +cdb-aligned-websocket-attributed,websocket,ref,browser.click:blocked,2002.014,31,172,true +cdb-aligned-websocket-attributed,websocket,ref,browser.click:blocked,2001.229,31,172,true +cdb-aligned-websocket-attributed,websocket,ref,browser.click:blocked,2001.118,31,172,true +cdb-aligned-websocket-attributed,websocket,locator,browser.fill,212.468,118,56,false +cdb-aligned-websocket-attributed,websocket,locator,browser.click,180.729,97,57,false +cdb-aligned-websocket-attributed,websocket,locator,browser.snapshot,635.706,18,461,false +cdb-aligned-websocket-attributed,websocket,locator,browser.batch,910.591,261,610,false +cdb-aligned-websocket-attributed,websocket,locator,browser.fill,184.127,118,56,false +cdb-aligned-websocket-attributed,websocket,locator,browser.click,185.058,97,57,false +cdb-aligned-websocket-attributed,websocket,locator,browser.snapshot,639.311,18,461,false +cdb-aligned-websocket-attributed,websocket,locator,browser.batch,1145.180,261,610,false +cdb-aligned-websocket-attributed,websocket,locator,browser.fill,167.843,118,56,false +cdb-aligned-websocket-attributed,websocket,locator,browser.click,163.053,97,57,false +cdb-aligned-websocket-attributed,websocket,locator,browser.snapshot,667.861,18,461,false +cdb-aligned-websocket-attributed,websocket,locator,browser.batch,2027.368,261,610,false +cdb-aligned-websocket-attributed,websocket,locator,browser.fill,307.883,118,56,false +cdb-aligned-websocket-attributed,websocket,locator,browser.click,293.147,97,57,false +cdb-aligned-websocket-attributed,websocket,locator,browser.snapshot,880.519,18,461,false +cdb-aligned-websocket-attributed,websocket,locator,browser.batch,1522.202,261,610,false +cdb-aligned-websocket-attributed,websocket,locator,browser.fill,353.454,118,56,false +cdb-aligned-websocket-attributed,websocket,locator,browser.click,239.123,97,57,false +cdb-aligned-websocket-attributed,websocket,locator,browser.snapshot,976.090,18,461,false +cdb-aligned-websocket-attributed,websocket,locator,browser.batch,953.504,261,610,false +cdb-aligned-websocket-attributed,websocket,locator,browser.fill,361.063,118,56,false +cdb-aligned-websocket-attributed,websocket,locator,browser.click,248.461,97,57,false +cdb-aligned-websocket-attributed,websocket,locator,browser.snapshot,761.562,18,461,false +cdb-aligned-websocket-attributed,websocket,locator,browser.batch,1207.103,261,610,false +cdb-aligned-websocket-attributed,websocket,locator,browser.fill,347.164,118,56,false +cdb-aligned-websocket-attributed,websocket,locator,browser.click,181.381,97,57,false +cdb-aligned-websocket-attributed,websocket,locator,browser.snapshot,838.934,18,461,false +cdb-aligned-websocket-attributed,websocket,locator,browser.batch,1011.430,261,610,false +cdb-aligned-websocket-attributed,websocket,locator,browser.fill,183.826,118,56,false +cdb-aligned-websocket-attributed,websocket,locator,browser.click,180.173,97,57,false +cdb-aligned-websocket-attributed,websocket,locator,browser.snapshot,643.552,18,461,false +cdb-aligned-websocket-attributed,websocket,locator,browser.batch,952.165,261,610,false +cdb-aligned-websocket-attributed,websocket,locator,browser.fill,194.353,118,56,false +cdb-aligned-websocket-attributed,websocket,locator,browser.click,165.087,97,57,false +cdb-aligned-websocket-attributed,websocket,locator,browser.snapshot,689.891,18,461,false +cdb-aligned-websocket-attributed,websocket,locator,browser.batch,995.261,261,610,false +cdb-aligned-websocket-attributed,websocket,locator,browser.fill,167.704,118,56,false +cdb-aligned-websocket-attributed,websocket,locator,browser.click,155.255,97,57,false +cdb-aligned-websocket-attributed,websocket,locator,browser.snapshot,591.904,18,461,false +cdb-aligned-websocket-attributed,websocket,locator,browser.batch,823.274,261,610,false +cdb-aligned-websocket-attributed,websocket,locator,browser.fill,170.573,118,56,false +cdb-aligned-websocket-attributed,websocket,locator,browser.click,161.509,97,57,false +cdb-aligned-websocket-attributed,websocket,locator,browser.snapshot,580.790,18,461,false +cdb-aligned-websocket-attributed,websocket,locator,browser.batch,834.140,261,610,false +cdb-aligned-websocket-attributed,websocket,locator,browser.fill,157.249,118,56,false +cdb-aligned-websocket-attributed,websocket,locator,browser.click,162.396,97,57,false +cdb-aligned-websocket-attributed,websocket,locator,browser.snapshot,623.441,18,461,false +cdb-aligned-websocket-attributed,websocket,locator,browser.batch,966.743,261,610,false +cdb-aligned-websocket-attributed,websocket,locator,browser.fill,244.014,118,56,false +cdb-aligned-websocket-attributed,websocket,locator,browser.click,245.461,97,57,false +cdb-aligned-websocket-attributed,websocket,locator,browser.snapshot,865.012,18,461,false +cdb-aligned-websocket-attributed,websocket,locator,browser.batch,1011.413,261,610,false +cdb-aligned-websocket-attributed,websocket,locator,browser.fill,154.837,118,56,false +cdb-aligned-websocket-attributed,websocket,locator,browser.click,166.189,97,57,false +cdb-aligned-websocket-attributed,websocket,locator,browser.snapshot,579.487,18,461,false +cdb-aligned-websocket-attributed,websocket,locator,browser.batch,950.288,261,610,false +cdb-aligned-websocket-attributed,websocket,locator,browser.fill,167.188,118,56,false +cdb-aligned-websocket-attributed,websocket,locator,browser.click,158.727,97,57,false +cdb-aligned-websocket-attributed,websocket,locator,browser.snapshot,652.158,18,461,false +cdb-aligned-websocket-attributed,websocket,locator,browser.batch,976.318,261,610,false +cdb-aligned-websocket-attributed,websocket,locator,browser.fill,228.838,118,56,false +cdb-aligned-websocket-attributed,websocket,locator,browser.click,174.955,97,57,false +cdb-aligned-websocket-attributed,websocket,locator,browser.snapshot,655.447,18,461,false +cdb-aligned-websocket-attributed,websocket,locator,browser.batch,985.387,261,610,false +cdb-aligned-websocket-attributed,websocket,locator,browser.fill,180.837,118,56,false +cdb-aligned-websocket-attributed,websocket,locator,browser.click,191.460,97,57,false +cdb-aligned-websocket-attributed,websocket,locator,browser.snapshot,800.734,18,461,false +cdb-aligned-websocket-attributed,websocket,locator,browser.batch,1046.955,261,610,false +cdb-aligned-websocket-attributed,websocket,locator,browser.fill,173.226,118,56,false +cdb-aligned-websocket-attributed,websocket,locator,browser.click,169.411,97,57,false +cdb-aligned-websocket-attributed,websocket,locator,browser.snapshot,796.289,18,461,false +cdb-aligned-websocket-attributed,websocket,locator,browser.batch,1000.694,261,610,false +cdb-aligned-websocket-attributed,websocket,locator,browser.fill,235.809,118,56,false +cdb-aligned-websocket-attributed,websocket,locator,browser.click,1076.385,97,57,false +cdb-aligned-websocket-attributed,websocket,locator,browser.snapshot,2238.115,18,461,false +cdb-aligned-websocket-attributed,websocket,locator,browser.batch,1145.318,261,610,false +cdb-aligned-websocket-attributed,websocket,locator,browser.fill,194.956,118,56,false +cdb-aligned-websocket-attributed,websocket,locator,browser.click,196.893,97,57,false +cdb-aligned-websocket-attributed,websocket,locator,browser.snapshot,883.455,18,461,false +cdb-aligned-websocket-attributed,websocket,locator,browser.batch,952.873,261,610,false +cdb-aligned-websocket-attributed,websocket,locator,browser.fill,167.168,118,56,false +cdb-aligned-websocket-attributed,websocket,locator,browser.click,165.311,97,57,false +cdb-aligned-websocket-attributed,websocket,locator,browser.snapshot,570.640,18,461,false +cdb-aligned-websocket-attributed,websocket,locator,browser.batch,1144.160,261,610,false +cdb-aligned-websocket-attributed,websocket,locator,browser.fill,178.802,118,56,false +cdb-aligned-websocket-attributed,websocket,locator,browser.click,163.722,97,57,false +cdb-aligned-websocket-attributed,websocket,locator,browser.snapshot,674.131,18,461,false +cdb-aligned-websocket-attributed,websocket,locator,browser.batch,971.938,261,610,false +cdb-aligned-websocket-attributed,websocket,locator,browser.fill,207.860,118,56,false +cdb-aligned-websocket-attributed,websocket,locator,browser.click,186.514,97,57,false +cdb-aligned-websocket-attributed,websocket,locator,browser.snapshot,3866.946,18,461,false +cdb-aligned-websocket-attributed,websocket,locator,browser.batch,1162.962,261,610,false +cdb-aligned-websocket-attributed,websocket,locator,browser.fill,178.180,118,56,false +cdb-aligned-websocket-attributed,websocket,locator,browser.click,158.624,97,57,false +cdb-aligned-websocket-attributed,websocket,locator,browser.snapshot,647.610,18,461,false +cdb-aligned-websocket-attributed,websocket,locator,browser.batch,1121.507,261,610,false +cdb-aligned-websocket-attributed,websocket,locator,browser.fill,189.461,118,56,false +cdb-aligned-websocket-attributed,websocket,locator,browser.click,170.357,97,57,false +cdb-aligned-websocket-attributed,websocket,locator,browser.snapshot,634.346,18,461,false +cdb-aligned-websocket-attributed,websocket,locator,browser.batch,873.718,261,610,false +cdb-aligned-websocket-attributed,websocket,locator,browser.fill,204.656,118,56,false +cdb-aligned-websocket-attributed,websocket,locator,browser.click,179.513,97,57,false +cdb-aligned-websocket-attributed,websocket,locator,browser.snapshot,766.758,18,461,false +cdb-aligned-websocket-attributed,websocket,locator,browser.batch,953.841,261,610,false +cdb-aligned-websocket-attributed,websocket,locator,browser.fill,180.443,118,56,false +cdb-aligned-websocket-attributed,websocket,locator,browser.click,179.044,97,57,false +cdb-aligned-websocket-attributed,websocket,locator,browser.snapshot,771.431,18,461,false +cdb-aligned-websocket-attributed,websocket,locator,browser.batch,998.275,261,610,false +cdb-aligned-websocket-attributed,websocket,locator,browser.fill,229.101,118,56,false +cdb-aligned-websocket-attributed,websocket,locator,browser.click,192.172,97,57,false +cdb-aligned-websocket-attributed,websocket,locator,browser.snapshot,1026.040,18,461,false +cdb-aligned-websocket-attributed,websocket,locator,browser.batch,1541.581,261,610,false +cdb-aligned-websocket-attributed,websocket,locator,browser.fill,168.885,118,56,false +cdb-aligned-websocket-attributed,websocket,locator,browser.click,157.564,97,57,false +cdb-aligned-websocket-attributed,websocket,locator,browser.snapshot,775.105,18,461,false +cdb-aligned-websocket-attributed,websocket,locator,browser.batch,889.216,261,610,false +cdb-aligned-websocket-attributed,websocket,locator,browser.fill,160.774,118,56,false +cdb-aligned-websocket-attributed,websocket,locator,browser.click,149.569,97,57,false +cdb-aligned-websocket-attributed,websocket,locator,browser.snapshot,654.205,18,461,false +cdb-aligned-websocket-attributed,websocket,locator,browser.batch,1013.100,261,610,false +cdb-aligned-websocket-attributed,websocket,locator,browser.click:blocked,2001.837,97,172,true +cdb-aligned-websocket-attributed,websocket,locator,browser.click:blocked,2001.719,97,172,true +cdb-aligned-websocket-attributed,websocket,locator,browser.click:blocked,2001.144,97,172,true +cdb-aligned-websocket-attributed,websocket,locator,browser.click:blocked,2000.463,97,172,true +cdb-aligned-websocket-attributed,websocket,locator,browser.click:blocked,2001.226,97,172,true +cdb-aligned-websocket-attributed,websocket,locator,browser.click:blocked,2001.052,97,172,true +cdb-aligned-websocket-attributed,websocket,locator,browser.click:blocked,2002.434,97,172,true +cdb-aligned-websocket-attributed,websocket,locator,browser.click:blocked,2036.551,97,172,true +cdb-aligned-websocket-attributed,websocket,locator,browser.click:blocked,2573.759,97,170,true +cdb-aligned-devframe-attributed,devframe,ref,browser.fill,110.440,55,56,false +cdb-aligned-devframe-attributed,devframe,ref,browser.click,90.657,30,57,false +cdb-aligned-devframe-attributed,devframe,ref,browser.snapshot,721.808,18,456,false +cdb-aligned-devframe-attributed,devframe,ref,browser.batch,822.184,131,605,false +cdb-aligned-devframe-attributed,devframe,ref,browser.fill,93.056,55,56,false +cdb-aligned-devframe-attributed,devframe,ref,browser.click,88.470,30,57,false +cdb-aligned-devframe-attributed,devframe,ref,browser.snapshot,739.285,18,456,false +cdb-aligned-devframe-attributed,devframe,ref,browser.batch,957.950,131,605,false +cdb-aligned-devframe-attributed,devframe,ref,browser.fill,104.982,55,56,false +cdb-aligned-devframe-attributed,devframe,ref,browser.click,92.730,30,57,false +cdb-aligned-devframe-attributed,devframe,ref,browser.snapshot,2256.093,18,456,false +cdb-aligned-devframe-attributed,devframe,ref,browser.batch,1439.097,131,605,false +cdb-aligned-devframe-attributed,devframe,ref,browser.fill,280.129,55,56,false +cdb-aligned-devframe-attributed,devframe,ref,browser.click,462.282,30,57,false +cdb-aligned-devframe-attributed,devframe,ref,browser.snapshot,1209.900,18,456,false +cdb-aligned-devframe-attributed,devframe,ref,browser.batch,1009.113,131,605,false +cdb-aligned-devframe-attributed,devframe,ref,browser.fill,178.800,55,56,false +cdb-aligned-devframe-attributed,devframe,ref,browser.click,232.504,30,57,false +cdb-aligned-devframe-attributed,devframe,ref,browser.snapshot,1004.255,18,457,false +cdb-aligned-devframe-attributed,devframe,ref,browser.batch,797.820,132,610,false +cdb-aligned-devframe-attributed,devframe,ref,browser.fill,96.974,56,56,false +cdb-aligned-devframe-attributed,devframe,ref,browser.click,95.580,31,57,false +cdb-aligned-devframe-attributed,devframe,ref,browser.snapshot,670.709,18,461,false +cdb-aligned-devframe-attributed,devframe,ref,browser.batch,760.048,133,610,false +cdb-aligned-devframe-attributed,devframe,ref,browser.fill,113.023,56,56,false +cdb-aligned-devframe-attributed,devframe,ref,browser.click,131.826,31,57,false +cdb-aligned-devframe-attributed,devframe,ref,browser.snapshot,807.619,18,461,false +cdb-aligned-devframe-attributed,devframe,ref,browser.batch,801.404,133,610,false +cdb-aligned-devframe-attributed,devframe,ref,browser.fill,103.343,56,56,false +cdb-aligned-devframe-attributed,devframe,ref,browser.click,87.824,31,57,false +cdb-aligned-devframe-attributed,devframe,ref,browser.snapshot,602.885,18,461,false +cdb-aligned-devframe-attributed,devframe,ref,browser.batch,744.745,133,610,false +cdb-aligned-devframe-attributed,devframe,ref,browser.fill,94.633,56,56,false +cdb-aligned-devframe-attributed,devframe,ref,browser.click,85.401,31,57,false +cdb-aligned-devframe-attributed,devframe,ref,browser.snapshot,616.120,18,461,false +cdb-aligned-devframe-attributed,devframe,ref,browser.batch,741.163,133,610,false +cdb-aligned-devframe-attributed,devframe,ref,browser.fill,92.208,56,56,false +cdb-aligned-devframe-attributed,devframe,ref,browser.click,85.464,31,57,false +cdb-aligned-devframe-attributed,devframe,ref,browser.snapshot,581.989,18,461,false +cdb-aligned-devframe-attributed,devframe,ref,browser.batch,757.196,133,610,false +cdb-aligned-devframe-attributed,devframe,ref,browser.fill,98.010,56,56,false +cdb-aligned-devframe-attributed,devframe,ref,browser.click,87.499,31,57,false +cdb-aligned-devframe-attributed,devframe,ref,browser.snapshot,870.831,18,461,false +cdb-aligned-devframe-attributed,devframe,ref,browser.batch,787.311,133,610,false +cdb-aligned-devframe-attributed,devframe,ref,browser.fill,104.512,56,56,false +cdb-aligned-devframe-attributed,devframe,ref,browser.click,92.850,31,57,false +cdb-aligned-devframe-attributed,devframe,ref,browser.snapshot,615.687,18,461,false +cdb-aligned-devframe-attributed,devframe,ref,browser.batch,740.645,133,610,false +cdb-aligned-devframe-attributed,devframe,ref,browser.fill,91.623,56,56,false +cdb-aligned-devframe-attributed,devframe,ref,browser.click,85.994,31,57,false +cdb-aligned-devframe-attributed,devframe,ref,browser.snapshot,598.325,18,461,false +cdb-aligned-devframe-attributed,devframe,ref,browser.batch,778.416,133,610,false +cdb-aligned-devframe-attributed,devframe,ref,browser.fill,93.969,56,56,false +cdb-aligned-devframe-attributed,devframe,ref,browser.click,89.438,31,57,false +cdb-aligned-devframe-attributed,devframe,ref,browser.snapshot,669.772,18,461,false +cdb-aligned-devframe-attributed,devframe,ref,browser.batch,801.962,133,610,false +cdb-aligned-devframe-attributed,devframe,ref,browser.fill,100.117,56,56,false +cdb-aligned-devframe-attributed,devframe,ref,browser.click,89.396,31,57,false +cdb-aligned-devframe-attributed,devframe,ref,browser.snapshot,608.082,18,461,false +cdb-aligned-devframe-attributed,devframe,ref,browser.batch,760.556,133,610,false +cdb-aligned-devframe-attributed,devframe,ref,browser.fill,94.479,56,56,false +cdb-aligned-devframe-attributed,devframe,ref,browser.click,83.616,31,57,false +cdb-aligned-devframe-attributed,devframe,ref,browser.snapshot,626.669,18,461,false +cdb-aligned-devframe-attributed,devframe,ref,browser.batch,786.056,133,610,false +cdb-aligned-devframe-attributed,devframe,ref,browser.fill,97.418,56,56,false +cdb-aligned-devframe-attributed,devframe,ref,browser.click,187.402,31,57,false +cdb-aligned-devframe-attributed,devframe,ref,browser.snapshot,1063.383,18,461,false +cdb-aligned-devframe-attributed,devframe,ref,browser.batch,983.652,133,610,false +cdb-aligned-devframe-attributed,devframe,ref,browser.fill,112.036,56,56,false +cdb-aligned-devframe-attributed,devframe,ref,browser.click,95.147,31,57,false +cdb-aligned-devframe-attributed,devframe,ref,browser.snapshot,1141.074,18,461,false +cdb-aligned-devframe-attributed,devframe,ref,browser.batch,857.076,133,610,false +cdb-aligned-devframe-attributed,devframe,ref,browser.fill,107.072,56,56,false +cdb-aligned-devframe-attributed,devframe,ref,browser.click,90.396,31,57,false +cdb-aligned-devframe-attributed,devframe,ref,browser.snapshot,673.533,18,461,false +cdb-aligned-devframe-attributed,devframe,ref,browser.batch,806.993,133,610,false +cdb-aligned-devframe-attributed,devframe,ref,browser.fill,101.375,56,56,false +cdb-aligned-devframe-attributed,devframe,ref,browser.click,90.411,31,57,false +cdb-aligned-devframe-attributed,devframe,ref,browser.snapshot,698.120,18,461,false +cdb-aligned-devframe-attributed,devframe,ref,browser.batch,795.315,133,610,false +cdb-aligned-devframe-attributed,devframe,ref,browser.fill,94.162,56,56,false +cdb-aligned-devframe-attributed,devframe,ref,browser.click,90.896,31,57,false +cdb-aligned-devframe-attributed,devframe,ref,browser.snapshot,599.906,18,461,false +cdb-aligned-devframe-attributed,devframe,ref,browser.batch,750.698,133,610,false +cdb-aligned-devframe-attributed,devframe,ref,browser.fill,902.169,56,56,false +cdb-aligned-devframe-attributed,devframe,ref,browser.click,346.727,31,57,false +cdb-aligned-devframe-attributed,devframe,ref,browser.snapshot,1639.793,18,461,false +cdb-aligned-devframe-attributed,devframe,ref,browser.batch,1337.507,133,610,false +cdb-aligned-devframe-attributed,devframe,ref,browser.fill,233.633,56,56,false +cdb-aligned-devframe-attributed,devframe,ref,browser.click,90.823,31,57,false +cdb-aligned-devframe-attributed,devframe,ref,browser.snapshot,772.451,18,461,false +cdb-aligned-devframe-attributed,devframe,ref,browser.batch,763.123,133,610,false +cdb-aligned-devframe-attributed,devframe,ref,browser.fill,98.890,56,56,false +cdb-aligned-devframe-attributed,devframe,ref,browser.click,100.829,31,57,false +cdb-aligned-devframe-attributed,devframe,ref,browser.snapshot,631.734,18,461,false +cdb-aligned-devframe-attributed,devframe,ref,browser.batch,763.105,133,610,false +cdb-aligned-devframe-attributed,devframe,ref,browser.fill,88.664,56,56,false +cdb-aligned-devframe-attributed,devframe,ref,browser.click,84.387,31,57,false +cdb-aligned-devframe-attributed,devframe,ref,browser.snapshot,652.093,18,461,false +cdb-aligned-devframe-attributed,devframe,ref,browser.batch,868.200,133,610,false +cdb-aligned-devframe-attributed,devframe,ref,browser.fill,109.213,56,56,false +cdb-aligned-devframe-attributed,devframe,ref,browser.click,100.560,31,57,false +cdb-aligned-devframe-attributed,devframe,ref,browser.snapshot,831.211,18,461,false +cdb-aligned-devframe-attributed,devframe,ref,browser.batch,912.531,133,610,false +cdb-aligned-devframe-attributed,devframe,ref,browser.fill,108.509,56,56,false +cdb-aligned-devframe-attributed,devframe,ref,browser.click,90.987,31,57,false +cdb-aligned-devframe-attributed,devframe,ref,browser.snapshot,629.139,18,461,false +cdb-aligned-devframe-attributed,devframe,ref,browser.batch,778.698,133,610,false +cdb-aligned-devframe-attributed,devframe,ref,browser.fill,94.863,56,56,false +cdb-aligned-devframe-attributed,devframe,ref,browser.click,86.156,31,57,false +cdb-aligned-devframe-attributed,devframe,ref,browser.snapshot,583.434,18,461,false +cdb-aligned-devframe-attributed,devframe,ref,browser.batch,748.927,133,610,false +cdb-aligned-devframe-attributed,devframe,ref,browser.fill,97.644,56,56,false +cdb-aligned-devframe-attributed,devframe,ref,browser.click,88.126,31,57,false +cdb-aligned-devframe-attributed,devframe,ref,browser.snapshot,662.971,18,461,false +cdb-aligned-devframe-attributed,devframe,ref,browser.batch,757.949,133,610,false +cdb-aligned-devframe-attributed,devframe,ref,browser.fill,98.878,56,56,false +cdb-aligned-devframe-attributed,devframe,ref,browser.click,85.476,31,57,false +cdb-aligned-devframe-attributed,devframe,ref,browser.snapshot,630.770,18,461,false +cdb-aligned-devframe-attributed,devframe,ref,browser.batch,847.830,133,610,false +cdb-aligned-devframe-attributed,devframe,ref,browser.click:blocked,2004.121,31,197,true +cdb-aligned-devframe-attributed,devframe,ref,browser.click:blocked,2005.765,31,197,true +cdb-aligned-devframe-attributed,devframe,ref,browser.click:blocked,2004.410,31,197,true +cdb-aligned-devframe-attributed,devframe,ref,browser.click:blocked,2001.359,31,197,true +cdb-aligned-devframe-attributed,devframe,ref,browser.click:blocked,2003.766,31,197,true +cdb-aligned-devframe-attributed,devframe,ref,browser.click:blocked,2003.704,31,197,true +cdb-aligned-devframe-attributed,devframe,ref,browser.click:blocked,2016.182,31,197,true +cdb-aligned-devframe-attributed,devframe,ref,browser.click:blocked,2041.797,31,197,true +cdb-aligned-devframe-attributed,devframe,ref,browser.click:blocked,2038.724,31,197,true +cdb-aligned-devframe-attributed,devframe,ref,browser.click:blocked,2003.229,31,197,true +cdb-aligned-devframe-attributed,devframe,ref,browser.click:blocked,2004.247,31,197,true +cdb-aligned-devframe-attributed,devframe,ref,browser.click:blocked,2002.555,31,197,true +cdb-aligned-devframe-attributed,devframe,ref,browser.click:blocked,2003.154,31,197,true +cdb-aligned-devframe-attributed,devframe,ref,browser.click:blocked,2002.456,31,197,true +cdb-aligned-devframe-attributed,devframe,ref,browser.click:blocked,2003.901,31,197,true +cdb-aligned-devframe-attributed,devframe,ref,browser.click:blocked,2004.741,31,197,true +cdb-aligned-devframe-attributed,devframe,ref,browser.click:blocked,2002.583,31,197,true +cdb-aligned-devframe-attributed,devframe,ref,browser.click:blocked,2003.995,31,197,true +cdb-aligned-devframe-attributed,devframe,ref,browser.click:blocked,2003.132,31,197,true +cdb-aligned-devframe-attributed,devframe,ref,browser.click:blocked,2003.159,31,197,true +cdb-aligned-devframe-attributed,devframe,ref,browser.click:blocked,2002.592,31,197,true +cdb-aligned-devframe-attributed,devframe,ref,browser.click:blocked,2004.591,31,197,true +cdb-aligned-devframe-attributed,devframe,ref,browser.click:blocked,2001.895,31,197,true +cdb-aligned-devframe-attributed,devframe,ref,browser.click:blocked,2003.955,31,197,true +cdb-aligned-devframe-attributed,devframe,ref,browser.click:blocked,2002.784,31,197,true +cdb-aligned-devframe-attributed,devframe,ref,browser.click:blocked,2003.850,31,197,true +cdb-aligned-devframe-attributed,devframe,ref,browser.click:blocked,2000.425,31,197,true +cdb-aligned-devframe-attributed,devframe,ref,browser.click:blocked,2008.724,31,197,true +cdb-aligned-devframe-attributed,devframe,ref,browser.click:blocked,2002.980,31,197,true +cdb-aligned-devframe-attributed,devframe,ref,browser.click:blocked,2001.551,31,197,true +cdb-aligned-devframe-attributed,devframe,locator,browser.fill,303.877,118,56,false +cdb-aligned-devframe-attributed,devframe,locator,browser.click,368.204,97,57,false +cdb-aligned-devframe-attributed,devframe,locator,browser.snapshot,629.837,18,461,false +cdb-aligned-devframe-attributed,devframe,locator,browser.batch,1239.024,261,610,false +cdb-aligned-devframe-attributed,devframe,locator,browser.fill,356.901,118,56,false +cdb-aligned-devframe-attributed,devframe,locator,browser.click,311.688,97,57,false +cdb-aligned-devframe-attributed,devframe,locator,browser.snapshot,756.532,18,461,false +cdb-aligned-devframe-attributed,devframe,locator,browser.batch,1060.857,261,610,false +cdb-aligned-devframe-attributed,devframe,locator,browser.fill,243.242,118,56,false +cdb-aligned-devframe-attributed,devframe,locator,browser.click,294.455,97,57,false +cdb-aligned-devframe-attributed,devframe,locator,browser.snapshot,665.342,18,461,false +cdb-aligned-devframe-attributed,devframe,locator,browser.batch,1177.068,261,610,false +cdb-aligned-devframe-attributed,devframe,locator,browser.fill,360.744,118,56,false +cdb-aligned-devframe-attributed,devframe,locator,browser.click,285.470,97,57,false +cdb-aligned-devframe-attributed,devframe,locator,browser.snapshot,717.336,18,461,false +cdb-aligned-devframe-attributed,devframe,locator,browser.batch,1195.321,261,610,false +cdb-aligned-devframe-attributed,devframe,locator,browser.fill,293.167,118,56,false +cdb-aligned-devframe-attributed,devframe,locator,browser.click,317.048,97,57,false +cdb-aligned-devframe-attributed,devframe,locator,browser.snapshot,593.219,18,461,false +cdb-aligned-devframe-attributed,devframe,locator,browser.batch,2643.726,261,610,false +cdb-aligned-devframe-attributed,devframe,locator,browser.fill,429.455,118,56,false +cdb-aligned-devframe-attributed,devframe,locator,browser.click,295.728,97,57,false +cdb-aligned-devframe-attributed,devframe,locator,browser.snapshot,1635.764,18,461,false +cdb-aligned-devframe-attributed,devframe,locator,browser.batch,1458.671,261,610,false +cdb-aligned-devframe-attributed,devframe,locator,browser.fill,387.471,118,56,false +cdb-aligned-devframe-attributed,devframe,locator,browser.click,323.123,97,57,false +cdb-aligned-devframe-attributed,devframe,locator,browser.snapshot,666.384,18,461,false +cdb-aligned-devframe-attributed,devframe,locator,browser.batch,1064.660,261,610,false +cdb-aligned-devframe-attributed,devframe,locator,browser.fill,270.303,118,56,false +cdb-aligned-devframe-attributed,devframe,locator,browser.click,275.276,97,57,false +cdb-aligned-devframe-attributed,devframe,locator,browser.snapshot,680.216,18,461,false +cdb-aligned-devframe-attributed,devframe,locator,browser.batch,1473.879,261,610,false +cdb-aligned-devframe-attributed,devframe,locator,browser.fill,365.888,118,56,false +cdb-aligned-devframe-attributed,devframe,locator,browser.click,319.909,97,57,false +cdb-aligned-devframe-attributed,devframe,locator,browser.snapshot,632.264,18,461,false +cdb-aligned-devframe-attributed,devframe,locator,browser.batch,1119.088,261,610,false +cdb-aligned-devframe-attributed,devframe,locator,browser.fill,383.702,118,56,false +cdb-aligned-devframe-attributed,devframe,locator,browser.click,356.810,97,57,false +cdb-aligned-devframe-attributed,devframe,locator,browser.snapshot,666.709,18,461,false +cdb-aligned-devframe-attributed,devframe,locator,browser.batch,1158.986,261,610,false +cdb-aligned-devframe-attributed,devframe,locator,browser.fill,257.458,118,56,false +cdb-aligned-devframe-attributed,devframe,locator,browser.click,359.001,97,57,false +cdb-aligned-devframe-attributed,devframe,locator,browser.snapshot,623.196,18,461,false +cdb-aligned-devframe-attributed,devframe,locator,browser.batch,1388.237,261,610,false +cdb-aligned-devframe-attributed,devframe,locator,browser.fill,359.351,118,56,false +cdb-aligned-devframe-attributed,devframe,locator,browser.click,354.814,97,57,false +cdb-aligned-devframe-attributed,devframe,locator,browser.snapshot,737.102,18,461,false +cdb-aligned-devframe-attributed,devframe,locator,browser.batch,1128.534,261,610,false +cdb-aligned-devframe-attributed,devframe,locator,browser.fill,287.749,118,56,false +cdb-aligned-devframe-attributed,devframe,locator,browser.click,345.686,97,57,false +cdb-aligned-devframe-attributed,devframe,locator,browser.snapshot,672.203,18,461,false +cdb-aligned-devframe-attributed,devframe,locator,browser.batch,1172.923,261,610,false +cdb-aligned-devframe-attributed,devframe,locator,browser.fill,254.040,118,56,false +cdb-aligned-devframe-attributed,devframe,locator,browser.click,347.707,97,57,false +cdb-aligned-devframe-attributed,devframe,locator,browser.snapshot,1248.886,18,461,false +cdb-aligned-devframe-attributed,devframe,locator,browser.batch,1467.135,261,610,false +cdb-aligned-devframe-attributed,devframe,locator,browser.fill,315.415,118,56,false +cdb-aligned-devframe-attributed,devframe,locator,browser.click,294.564,97,57,false +cdb-aligned-devframe-attributed,devframe,locator,browser.snapshot,654.075,18,461,false +cdb-aligned-devframe-attributed,devframe,locator,browser.batch,1156.689,261,610,false +cdb-aligned-devframe-attributed,devframe,locator,browser.fill,258.816,118,56,false +cdb-aligned-devframe-attributed,devframe,locator,browser.click,265.421,97,57,false +cdb-aligned-devframe-attributed,devframe,locator,browser.snapshot,803.581,18,461,false +cdb-aligned-devframe-attributed,devframe,locator,browser.batch,1122.498,261,610,false +cdb-aligned-devframe-attributed,devframe,locator,browser.fill,267.739,118,56,false +cdb-aligned-devframe-attributed,devframe,locator,browser.click,270.004,97,57,false +cdb-aligned-devframe-attributed,devframe,locator,browser.snapshot,714.137,18,461,false +cdb-aligned-devframe-attributed,devframe,locator,browser.batch,1143.344,261,610,false +cdb-aligned-devframe-attributed,devframe,locator,browser.fill,289.001,118,56,false +cdb-aligned-devframe-attributed,devframe,locator,browser.click,322.555,97,57,false +cdb-aligned-devframe-attributed,devframe,locator,browser.snapshot,586.084,18,461,false +cdb-aligned-devframe-attributed,devframe,locator,browser.batch,1291.474,261,610,false +cdb-aligned-devframe-attributed,devframe,locator,browser.fill,312.110,118,56,false +cdb-aligned-devframe-attributed,devframe,locator,browser.click,320.035,97,57,false +cdb-aligned-devframe-attributed,devframe,locator,browser.snapshot,704.532,18,461,false +cdb-aligned-devframe-attributed,devframe,locator,browser.batch,1108.865,261,610,false +cdb-aligned-devframe-attributed,devframe,locator,browser.fill,290.103,118,56,false +cdb-aligned-devframe-attributed,devframe,locator,browser.click,268.501,97,57,false +cdb-aligned-devframe-attributed,devframe,locator,browser.snapshot,615.002,18,461,false +cdb-aligned-devframe-attributed,devframe,locator,browser.batch,1198.607,261,610,false +cdb-aligned-devframe-attributed,devframe,locator,browser.fill,402.995,118,56,false +cdb-aligned-devframe-attributed,devframe,locator,browser.click,472.892,97,57,false +cdb-aligned-devframe-attributed,devframe,locator,browser.snapshot,804.879,18,461,false +cdb-aligned-devframe-attributed,devframe,locator,browser.batch,1276.565,261,610,false +cdb-aligned-devframe-attributed,devframe,locator,browser.fill,348.945,118,56,false +cdb-aligned-devframe-attributed,devframe,locator,browser.click,361.841,97,57,false +cdb-aligned-devframe-attributed,devframe,locator,browser.snapshot,676.687,18,461,false +cdb-aligned-devframe-attributed,devframe,locator,browser.batch,1130.966,261,610,false +cdb-aligned-devframe-attributed,devframe,locator,browser.fill,394.405,118,56,false +cdb-aligned-devframe-attributed,devframe,locator,browser.click,268.268,97,57,false +cdb-aligned-devframe-attributed,devframe,locator,browser.snapshot,623.011,18,461,false +cdb-aligned-devframe-attributed,devframe,locator,browser.batch,1095.799,261,610,false +cdb-aligned-devframe-attributed,devframe,locator,browser.fill,359.405,118,56,false +cdb-aligned-devframe-attributed,devframe,locator,browser.click,346.578,97,57,false +cdb-aligned-devframe-attributed,devframe,locator,browser.snapshot,614.547,18,461,false +cdb-aligned-devframe-attributed,devframe,locator,browser.batch,1160.295,261,610,false +cdb-aligned-devframe-attributed,devframe,locator,browser.fill,360.734,118,56,false +cdb-aligned-devframe-attributed,devframe,locator,browser.click,263.217,97,57,false +cdb-aligned-devframe-attributed,devframe,locator,browser.snapshot,623.370,18,461,false +cdb-aligned-devframe-attributed,devframe,locator,browser.batch,1128.175,261,610,false +cdb-aligned-devframe-attributed,devframe,locator,browser.fill,505.619,118,56,false +cdb-aligned-devframe-attributed,devframe,locator,browser.click,818.185,97,57,false +cdb-aligned-devframe-attributed,devframe,locator,browser.snapshot,2276.488,18,461,false +cdb-aligned-devframe-attributed,devframe,locator,browser.batch,4551.811,261,610,false +cdb-aligned-devframe-attributed,devframe,locator,browser.fill,370.456,118,56,false +cdb-aligned-devframe-attributed,devframe,locator,browser.click,348.321,97,57,false +cdb-aligned-devframe-attributed,devframe,locator,browser.snapshot,660.404,18,461,false +cdb-aligned-devframe-attributed,devframe,locator,browser.batch,1236.730,261,610,false +cdb-aligned-devframe-attributed,devframe,locator,browser.fill,368.570,118,56,false +cdb-aligned-devframe-attributed,devframe,locator,browser.click,369.121,97,57,false +cdb-aligned-devframe-attributed,devframe,locator,browser.snapshot,615.118,18,461,false +cdb-aligned-devframe-attributed,devframe,locator,browser.batch,1245.401,261,610,false +cdb-aligned-devframe-attributed,devframe,locator,browser.fill,272.896,118,56,false +cdb-aligned-devframe-attributed,devframe,locator,browser.click,275.137,97,57,false +cdb-aligned-devframe-attributed,devframe,locator,browser.snapshot,934.405,18,461,false +cdb-aligned-devframe-attributed,devframe,locator,browser.batch,1233.582,261,610,false +cdb-aligned-devframe-attributed,devframe,locator,browser.fill,307.179,118,56,false +cdb-aligned-devframe-attributed,devframe,locator,browser.click,319.777,97,57,false +cdb-aligned-devframe-attributed,devframe,locator,browser.snapshot,578.714,18,461,false +cdb-aligned-devframe-attributed,devframe,locator,browser.batch,1263.108,261,610,false +cdb-aligned-devframe-attributed,devframe,locator,browser.click:blocked,2000.826,97,197,true +cdb-aligned-devframe-attributed,devframe,locator,browser.click:blocked,2497.063,97,197,true +cdb-aligned-devframe-attributed,devframe,locator,browser.click:blocked,2524.500,97,197,true +cdb-aligned-devframe-attributed,devframe,locator,browser.click:blocked,2178.690,97,197,true +cdb-aligned-devframe-attributed,devframe,locator,browser.click:blocked,2008.372,97,197,true +cdb-aligned-devframe-attributed,devframe,locator,browser.click:blocked,2101.277,97,197,true +cdb-aligned-devframe-attributed,devframe,locator,browser.click:blocked,2000.696,97,197,true +cdb-aligned-devframe-attributed,devframe,locator,browser.click:blocked,3346.516,97,197,true +cdb-aligned-devframe-attributed,devframe,locator,browser.click:blocked,2068.698,97,197,true +cdb-aligned-devframe-attributed,devframe,locator,browser.click:blocked,2005.411,97,197,true +cdb-aligned-devframe-attributed,devframe,locator,browser.click:blocked,2063.155,97,197,true +cdb-aligned-devframe-attributed,devframe,locator,browser.click:blocked,2001.028,97,197,true +cdb-aligned-devframe-attributed,devframe,locator,browser.click:blocked,2005.101,97,197,true +cdb-aligned-devframe-attributed,devframe,locator,browser.click:blocked,2011.003,97,197,true +cdb-aligned-devframe-attributed,devframe,locator,browser.click:blocked,2016.366,97,197,true +cdb-aligned-devframe-attributed,devframe,locator,browser.click:blocked,2007.517,97,197,true +cdb-aligned-devframe-attributed,devframe,locator,browser.click:blocked,2125.550,97,197,true +cdb-aligned-devframe-attributed,devframe,locator,browser.click:blocked,2001.821,97,197,true +cdb-aligned-devframe-attributed,devframe,locator,browser.click:blocked,2001.916,97,197,true +cdb-aligned-devframe-attributed,devframe,locator,browser.click:blocked,2025.673,97,197,true +cdb-aligned-devframe-attributed,devframe,locator,browser.click:blocked,2002.753,97,197,true +cdb-aligned-devframe-attributed,devframe,locator,browser.click:blocked,2001.677,97,197,true +cdb-aligned-devframe-attributed,devframe,locator,browser.click:blocked,2002.447,97,197,true +cdb-aligned-devframe-attributed,devframe,locator,browser.click:blocked,2002.644,97,197,true +cdb-aligned-devframe-attributed,devframe,locator,browser.click:blocked,2057.001,97,197,true +cdb-aligned-devframe-attributed,devframe,locator,browser.click:blocked,2010.517,97,197,true +cdb-aligned-devframe-attributed,devframe,locator,browser.click:blocked,2004.027,97,197,true +cdb-aligned-devframe-attributed,devframe,locator,browser.click:blocked,2101.537,97,197,true +cdb-aligned-devframe-attributed,devframe,locator,browser.click:blocked,2032.145,97,197,true +cdb-aligned-devframe-attributed,devframe,locator,browser.click:blocked,2014.032,97,197,true diff --git a/docs/benchmarks/2026-09-09-aligned.md b/docs/benchmarks/2026-09-09-aligned.md new file mode 100644 index 0000000..3e06a36 --- /dev/null +++ b/docs/benchmarks/2026-09-09-aligned.md @@ -0,0 +1,114 @@ +# Aligned transport measurements and notification validation + +Recorded on 9 September 2026 using Chromium 151.0.7922.34, Node 24.20.0, macOS arm64 and an Apple M3 Pro. Both transports use the same public fixture: 10,000 DOM nodes, 200 levels, 20 closed shadow roots and three cross-origin frame levels. + +## Method + +Each transport runs sequentially with five warm-up iterations and 30 measured samples per operation and selector. A workflow fills the deep input, clicks its save control and captures an interactive snapshot. A matching batch performs the same two actions with observation. Persistent-overlay clicks are measured separately. Reference-based actions refresh disposable references after each observation; locator-based actions use identical exact accessible names. + +All timings are milliseconds. The action gate is p95 ≤500 ms; the snapshot gate is p95 ≤1,000 ms. Model time and application loading are excluded. CDB processing and transport round trips are included. Blocked clicks exercise the existing two-second deadline; their duration is not a successful-action speed improvement. Batch limits remain 20 actions and 30 seconds. + +The first aligned attempt failed during warm-up because the benchmark reused references invalidated by its own snapshot. The harness now consumes the latest observation. That setup failure is retained in the diagnostic evidence; it produced no measured samples. + +[Every measured sample](./2026-09-09-aligned.csv) is retained. [Earlier failed gates and lifecycle runs](./2026-09-09-lifecycle.md) remain unchanged evidence; the new measurements do not erase them. + +## Baseline + +| Transport | Selector | Fill | Click | Snapshot | Batch + observation | Individual workflow | Blocked click | +| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | +| WebSocket | ref | 191 | 163 | 752 | 1255 | 1037 | 2003 | +| WebSocket | locator | 339 | 286 | 902 | 1276 | 1631 | 2005 | +| Devframe | ref | 109 | 113 | 730 | 906 | 930 | 2009 | +| Devframe | locator | 451 | 456 | 767 | 1543 | 1518 | 2137 | + +## Gated runs before optimization + +| Transport | Selector | Fill | Click | Snapshot | Batch + observation | Individual workflow | Blocked click | +| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | +| WebSocket | ref | 132 | 137 | 692 | 951 | 916 | 2002 | +| WebSocket | locator | 257 | 241 | 717 | 1087 | 1141 | 2020 | +| Devframe | ref | 100 | 105 | 722 | 841 | 904 | 2007 | +| Devframe | locator | 454 | 522 | 849 | 1407 | 1686 | 2063 | + +The Devframe locator-click p95 of 522 ms failed the 500-ms gate. Diagnostics attributed the slow clicks mainly to discovery: one 521-ms sample spent 420 ms discovering candidates and 84 ms checking actionability. Independent frame accessibility queries ran serially. The slowest sample spent 609 ms in one frame's accessibility query alone. + +Discovery now reads up to four independent frame contexts concurrently, retaining frame order and awaiting all candidates before deciding uniqueness. The regression test failed before the change and now verifies the concurrency limit and complete ambiguity checks. No cross-action DOM cache, relaxed checks, input replay or deadline change was introduced. + +## Gated runs after optimization + +| Transport | Selector | Fill | Click | Snapshot | Batch + observation | Individual workflow | Blocked click | +| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | +| WebSocket | ref | 163 | 181 | 1057 | 1821 | 1268 | 2005 | +| WebSocket | locator | 311 | 371 | 2400 | 1168 | 2974 | 2003 | +| Devframe | ref | 131 | 119 | 1434 | 1279 | 1618 | 2008 | +| Devframe | locator | 368 | 384 | 743 | 1254 | 1405 | 2130 | + +Machine load varies between runs, so timing differences cannot be attributed entirely to parallel discovery. The change removes demonstrated serial work. These fixture results do not establish the cause of intermittent live integration stalls. + +Both runs pass the fill/click gates. Snapshot acceptance still fails: WebSocket p95 is 1,057 ms in the reference workflow and 2,400 ms in the locator workflow; Devframe is 1,434 ms in the reference workflow. The Devframe locator-workflow snapshot passes at 743 ms. + +## Repeat with Chrome command attribution + +The short existing command probe did not reproduce the snapshot failure: 30 snapshots had p95 743 ms, while Chrome's full-tree reads had p95 279 ms and a maximum of 373 ms. That diagnostic uses a different action order and omits batches and blocked actions, so it does not replace the aligned measurements. + +The aligned benchmark now also records time inside the extension's Chrome debugger binding. This test-only probe separates that boundary from the full command round trip without changing production behavior. + +| Transport | Selector | Fill | Click | Snapshot | Batch + observation | Individual workflow | Blocked click | +| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | +| WebSocket | ref | 156 | 130 | 1028 | 1237 | 1255 | 2046 | +| WebSocket | locator | incomplete | incomplete | incomplete | incomplete | incomplete | incomplete | +| Devframe | ref | 280 | 347 | 1640 | 1338 | 2454 | 2039 | +| Devframe | locator | 429 | 473 | 1636 | 2644 | 2361 | 2525 | + +## Recorded machine load + +The final attributed WebSocket run stopped after 279 of 300 measurements when a blocked click exhausted its actionability deadline before establishing the covering ancestor. Its pending command was `Page.getFrameTree`; input was not dispatched. The locator summary is incomplete because the selector's blocked-action loop did not finish. This failed run must not be replaced with an earlier passing result. + +The final attributed Devframe run completed all 300 measurements, but both snapshot gates failed at approximately 1,640 ms. Chrome debugger binding timings included a 2,390-ms `Accessibility.getFullAXTree`, a 1,308-ms `Accessibility.disable`, a 1,034-ms `Page.enable` and a 1,034-ms `Page.getFrameTree`. These measurements establish browser-boundary delays, not their underlying cause. CDB command round trips also include transport, serialization, authority and artifact work; the remaining time is not fully attributed. + +A concurrent process inspection found an unrelated desktop renderer consuming about 569% CPU and 15.8% of system memory. Subsequent desktop activity also varied. This is observed contention, not proof that it explains every delay. No process or profile changes were made. Performance validation remains incomplete; further acceptance measurements require a settled environment. + +Load averages are shown for 1 / 5 / 15 minutes. Builds and other test suites did not overlap measured runs. Five- and fifteen-minute load averages can still include earlier live work. + +| Run | Before | After | +| --- | --- | --- | +| WebSocket baseline | 7.3 / 7.4 / 8.1 | 6.8 / 8.0 / 8.2 | +| Devframe baseline | 5.8 / 7.7 / 8.1 | 13.7 / 9.4 / 8.6 | +| WebSocket before optimization | 26.5 / 41.1 / 30.3 | 7.8 / 22.1 / 24.7 | +| Devframe before optimization | 7.7 / 21.2 / 24.3 | 7.2 / 12.7 / 19.5 | +| WebSocket after optimization | 8.9 / 10.0 / 14.7 | 7.7 / 10.6 / 13.7 | +| Devframe after optimization | 9.0 / 10.6 / 13.6 | 6.5 / 8.5 / 11.8 | +| WebSocket Chrome attribution | 5.2 / 7.2 / 10.5 | 18.1 / 10.0 / 10.7 | +| Devframe Chrome attribution | 14.3 / 10.6 / 10.9 | 14.6 / 12.0 / 11.3 | + +## Snapshot attribution and workflow cost + +The opt-in snapshot channel distinguishes frame discovery, command round trips, retained-artifact reads and formatting. Command round trips combine Chrome, provider and transport work; these measurements do not isolate Chrome-internal execution from serialization or publication. Totals below include warm-ups and batch observations. + +- websocket: 568 frame captures, total command round trips 97911 ms, artifact reads 6945 ms, formatting 1540 ms. +- devframe: 568 frame captures, total command round trips 96934 ms, artifact reads 4741 ms, formatting 1826 ms. + +Estimated payload tokens below are serialized tool argument and response characters divided by four, averaged per complete workflow. This is not tokenizer output and excludes the catalogue, model messages and tool-call envelopes. Batches reduce three agent round trips to one; they do not guarantee less payload or lower native execution time. + +| Transport | Selector | Workflow | Estimated payload tokens | +| --- | --- | --- | ---: | +| websocket | ref | individual | 169 | +| websocket | ref | batch | 186 | +| websocket | locator | individual | 202 | +| websocket | locator | batch | 218 | +| devframe | ref | individual | 169 | +| devframe | ref | batch | 186 | +| devframe | locator | individual | 202 | +| devframe | locator | batch | 218 | + +## Notification verification + +The exact hub-ui 0.9.10 patch from Devframe PR #375 is exercised in the installed embedded and standalone browser bundles. Both hosts pass deletion, full reconciliation, changed descriptions, unchanged publications, manual dismissal and intentional resurfacing. Eight page-script tests cover direct Accept/default Review, stable message registrations, count changes, serialized updates and creation/expiry/disposal races. The MCP package's 67 unit regressions also pass. + +Workspace patches do not propagate to consumers of published CDB packages. See [patch provenance and removal conditions](../../patches/README.md). + +## Follow-up lifecycle matrix + +The independent 30-cycle matrix stopped in cycle 6 after 23 successful scenarios. Navigation, reconnect and cancellation each completed six times; revocation/reapproval completed five times, then its next click timed out before input. At failure, discovery had taken 1,150 ms and actionability 917 ms, with `Page.getLayoutMetrics` pending. Load rose from 12.6 / 12.0 / 11.3 to 44.7 / 18.8 / 13.8. + +[All follow-up lifecycle samples](./2026-09-09-followup-lifecycle.csv) retain the failure. The intended 120-scenario gate is not satisfied. The surrounding Chromium integration checks reached this matrix successfully, including closed shadow roots, cross-origin frames, overlays, overlapping grants and ordinary shared RPC traffic. No crash was observed; the intermittent stall cause remains unresolved. diff --git a/docs/benchmarks/2026-09-09-followup-lifecycle.csv b/docs/benchmarks/2026-09-09-followup-lifecycle.csv new file mode 100644 index 0000000..6ba75d7 --- /dev/null +++ b/docs/benchmarks/2026-09-09-followup-lifecycle.csv @@ -0,0 +1,25 @@ +cycle,scenario,milliseconds,result +1,navigation,484.227,passed +1,reconnect,592.164,passed +1,cancellation,580.741,passed +1,revocation,505.744,passed +2,navigation,584.895,passed +2,reconnect,567.728,passed +2,cancellation,446.052,passed +2,revocation,477.833,passed +3,navigation,487.889,passed +3,reconnect,560.592,passed +3,cancellation,532.009,passed +3,revocation,1028.839,passed +4,navigation,1283.496,passed +4,reconnect,1224.433,passed +4,cancellation,1086.099,passed +4,revocation,1209.525,passed +5,navigation,1905.258,passed +5,reconnect,847.273,passed +5,cancellation,1321.633,passed +5,revocation,1682.765,passed +6,navigation,1135.055,passed +6,reconnect,910.539,passed +6,cancellation,1756.493,passed +6,revocation,4219.532,failed diff --git a/docs/benchmarks/2026-09-09-lifecycle.md b/docs/benchmarks/2026-09-09-lifecycle.md index 0bd220b..1de4f6f 100644 --- a/docs/benchmarks/2026-09-09-lifecycle.md +++ b/docs/benchmarks/2026-09-09-lifecycle.md @@ -49,4 +49,4 @@ All measured actions completed successfully. The covered-action duration include ## Remaining validation -Performance acceptance is open. These public fixtures do not establish live validation in embedding applications. Upstream toast removal is fixed in [Devframe PR #375](https://github.com/devframes/devframe/pull/375). Downstream pins remain unchanged pending the compatible Vite upgrade; changing live notification details without resurfacing dismissed toasts remains deferred with that adoption. +These results record the earlier validation attempt; its failed measurements remain part of the evidence. Public fixtures do not establish live validation in embedding applications. Upstream toast removal is fixed in [Devframe PR #375](https://github.com/devframes/devframe/pull/375). The follow-up backports that fix through a version-specific pnpm patch while retaining dependency pins. Changed notification descriptions may resurface dismissed toasts using Devframe’s existing update behavior; dismissal persistence across meaningful updates is no longer a requirement. diff --git a/package.json b/package.json index 8ae6691..85a9c81 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,8 @@ "check:package": "turbo run check:package" }, "devDependencies": { + "@devframes/hub": "catalog:devtools", + "@devframes/hub-ui": "catalog:devtools", "@dvcol/cdb": "workspace:*", "@dvcol/cdb-automation-playwright": "workspace:*", "@dvcol/cdb-birpc": "workspace:*", diff --git a/packages/devframe/README.md b/packages/devframe/README.md index 74cf057..229edb1 100644 --- a/packages/devframe/README.md +++ b/packages/devframe/README.md @@ -52,3 +52,7 @@ The page script emits `cdb:review-request` with `{ requestId }` to request the e Hosts with a direct approval channel can set `approvalAction: 'accept'` on `createCdbPanel`. The page script then labels its action **Accept** and emits `cdb:accept-request`; the embedding application authenticates and handles the final approval. The default remains **Review request** and `cdb:review-request`. See the [runnable example](../../examples/devframe/README.md). + +Notification descriptions and approved-tab counts update through the existing Devframe message handle. Changed content retains its message ID and follows the host's normal notification behavior, including resurfacing a dismissed toast. Unchanged broker publications do not update the message. Request completion, expiry and disposal remove its message and command. + +The public validation workspace backports Devframe's toast-removal fix to hub-ui 0.9.10 using [a temporary pnpm patch](https://github.com/dvcol/chrome-debugger-bridge/blob/main/patches/README.md). This workspace patch is not inherited by consumers of the published CDB package; embedding applications using that hub-ui version must apply the patch themselves until adopting an upstream version containing the fix. diff --git a/packages/devframe/src/page-script.ts b/packages/devframe/src/page-script.ts index 9c27868..8f66489 100644 --- a/packages/devframe/src/page-script.ts +++ b/packages/devframe/src/page-script.ts @@ -25,7 +25,7 @@ export interface BrowserControlPageContext { register: (command: { id: string; title: string; source: 'client'; action: () => Promise; showInPalette: boolean }) => () => void; }; readonly messages: { - info: (message: string, options: { id: string; description: string; notify: boolean; autoDismiss: false; actions: { id: string; label: string; kind: 'command'; command: { id: string } }[] }) => Promise<{ dismiss: () => Promise }>; + info: (message: string, options: { id: string; description: string; notify: boolean; autoDismiss: false; actions: { id: string; label: string; kind: 'command'; command: { id: string } }[] }) => Promise<{ dismiss: () => Promise; update: (patch: { description: string }) => Promise }>; }; } @@ -36,28 +36,46 @@ export default async function setupBrowserControlPage(context: BrowserControlPag window.dispatchEvent(new CustomEvent(options.approvalAction === 'accept' ? browserControlAcceptEvent : browserControlReviewEvent, { detail: { requestId } })); }; const controller = createBrowserControlNotificationController({ onReview: request => review(request.id), onRevoke: async requestId => client.revokeScope(requestId) }); - const notifications = new Map void>(); + const notifications = new Map void; remove: () => void }>(); const prefix = `cdb:browser-control:${crypto.randomUUID()}`; const stopNotifications = controller.subscribe((state) => { const items = [ ...state.requests.map(request => ({ id: `request:${request.id}`, title: 'Browser control requested', description: `${request.principalLabel} requests ${request.level} access with ${request.navigation} navigation.`, label: options.approvalAction === 'accept' ? 'Accept' : 'Review request', action: async () => controller.review(request.id) })), ...Array.from(Map.groupBy(state.grants, grant => grant.requestId), ([requestId, grants]) => ({ id: `grant:${requestId}`, title: 'Browser control active', description: `${grants[0]!.principalLabel}: ${grants[0]!.level} access to ${grants.length} approved ${grants.length === 1 ? 'tab' : 'tabs'}.`, label: 'Stop control', action: async () => controller.revoke(requestId) })), ]; - for (const [id, remove] of notifications) { + for (const [id, notification] of notifications) { if (items.some(item => item.id === id)) continue; - remove(); + notification.remove(); notifications.delete(id); } for (const item of items) { - if (notifications.has(item.id)) continue; + const notification = notifications.get(item.id); + if (notification !== undefined) { + if (notification.description !== item.description) { + notification.description = item.description; + notification.update(item.description); + } + continue; + } const id = `${prefix}:${item.id}`; const unregister = context.commands.register({ id, title: item.label, source: 'client', action: item.action, showInPalette: false }); - const message = context.messages.info(item.title, { id, description: item.description, notify: true, autoDismiss: false, actions: [{ id: 'control', label: item.label, kind: 'command', command: { id } }] }); - notifications.set(item.id, () => { - unregister(); - void message.then(async handle => handle.dismiss()).catch(error => console.error('Unable to dismiss browser-control notification.', error)); + const message = context.messages.info(item.title, { id, description: item.description, notify: true, autoDismiss: false, actions: [{ id: 'control', label: item.label, kind: 'command', command: { id } }] }).catch(error => console.error('Unable to show browser-control notification.', error)); + let active = true; + let pending = Promise.resolve(); + notifications.set(item.id, { + description: item.description, + update(description) { + pending = pending.then(async () => { + const handle = await message; + if (active) await handle?.update({ description }); + }).catch(error => console.error('Unable to update browser-control notification.', error)); + }, + remove() { + active = false; + unregister(); + pending = pending.then(async () => (await message)?.dismiss()).catch(error => console.error('Unable to dismiss browser-control notification.', error)); + }, }); - void message.catch(error => console.error('Unable to show browser-control notification.', error)); } }); const receive = (event: MessageEvent): void => { @@ -70,7 +88,7 @@ export default async function setupBrowserControlPage(context: BrowserControlPag stopWatching?.(); document.documentElement.removeAttribute('data-cdb-notifications-ready'); stopNotifications(); - for (const remove of notifications.values()) remove(); + for (const notification of notifications.values()) notification.remove(); notifications.clear(); controller.dispose(); window.removeEventListener('message', receive); diff --git a/packages/devframe/test/page-script.test.ts b/packages/devframe/test/page-script.test.ts index 66f57a1..dc0cd9c 100644 --- a/packages/devframe/test/page-script.test.ts +++ b/packages/devframe/test/page-script.test.ts @@ -16,7 +16,7 @@ it.each(['review', 'accept'] as const)('uses native hub notifications with %s an const dismiss = vi.fn(async () => {}); const unregister = vi.fn(); const register = vi.fn(() => unregister); - const info = vi.fn(async () => ({ dismiss })); + const info = vi.fn(async () => ({ dismiss, update: vi.fn(async () => {}) })); const dispatch = vi.spyOn(window, 'dispatchEvent'); const client = vi.spyOn(panel, 'createBrowserControlPanelClient').mockReturnValue({ snapshot: () => state, @@ -51,3 +51,119 @@ it.each(['review', 'accept'] as const)('uses native hub notifications with %s an expect(stopWatching).toHaveBeenCalledOnce(); expect(document.documentElement.hasAttribute('data-cdb-notifications-ready')).toBe(false); }); + +async function notificationFixture(message = { dismiss: vi.fn(async () => {}), update: vi.fn(async (_patch: { description: string }) => {}) }, creation?: Promise) { + const state: BrokerState = { revision: 1, providers: [], principals: [], targets: [], grants: [], scopes: [], leases: [], requests: [{ id: 'request', principalId: 'principal', principalLabel: 'Agent', level: 'interact', navigation: 'same-origin', state: 'pending', createdAt: 0, expiresAt: null }] }; + let publish: (state: BrokerState) => void = () => {}; + const client = vi.spyOn(panel, 'createBrowserControlPanelClient').mockReturnValue({ snapshot: () => state, watch: (listener) => { + publish = listener; + listener(state); + return () => {}; + }, revokeScope: async () => {}, revokeGrant: async () => true, disconnectProvider: async () => true }); + const unregister = vi.fn(); + const register = vi.fn(() => unregister); + const info = vi.fn(async () => creation ?? message); + const dispose = await setupBrowserControlPage({ rpc: {} as BrowserControlPageContext['rpc'], current: { domElements: {} }, commands: { register }, messages: { info } }); + return { state, publish: (next: BrokerState) => publish(next), message, info, register, unregister, dispose: () => { + dispose(); + client.mockRestore(); + } }; +} + +it('updates changed descriptions through the existing message without registering another command', async () => { + expect.assertions(4); + const fixture = await notificationFixture(); + try { + fixture.publish({ ...fixture.state, revision: 2 }); + expect(fixture.message.update).not.toHaveBeenCalled(); + fixture.publish({ ...fixture.state, requests: [{ ...fixture.state.requests[0]!, principalLabel: 'Renamed agent' }] }); + await vi.waitUntil(() => fixture.message.update.mock.calls.length === 1); + expect(fixture.message.update).toHaveBeenCalledWith({ description: 'Renamed agent requests interact access with same-origin navigation.' }); + expect(fixture.info).toHaveBeenCalledOnce(); + expect(fixture.register).toHaveBeenCalledOnce(); + } finally { + fixture.dispose(); + } +}); + +it('updates overlapping-grant tab counts without notifying unchanged publications', async () => { + expect.assertions(3); + const fixture = await notificationFixture(); + const grant = { id: 'grant', requestId: 'request', principalId: 'principal', principalLabel: 'Agent', providerId: 'provider', targetId: 'one', targetGeneration: 1, level: 'interact' as const, navigation: 'same-origin' as const, approvedOrigin: 'https://example.test', createdAt: 0, state: 'active' as const }; + try { + fixture.publish({ ...fixture.state, requests: [], grants: [grant] }); + fixture.publish({ ...fixture.state, requests: [], grants: [grant, { ...grant, id: 'second', targetId: 'two' }] }); + await vi.waitUntil(() => fixture.message.update.mock.calls.length === 1); + expect(fixture.message.update).toHaveBeenLastCalledWith({ description: 'Agent: interact access to 2 approved tabs.' }); + fixture.publish({ ...fixture.state, revision: 3, requests: [], grants: [grant, { ...grant, id: 'second', targetId: 'two' }] }); + await Promise.resolve(); + expect(fixture.message.update).toHaveBeenCalledOnce(); + expect(fixture.info).toHaveBeenCalledTimes(2); + } finally { + fixture.dispose(); + } +}); + +it('serializes updates and dismisses after a pending update without publishing queued stale data', async () => { + expect.assertions(5); + const pending = Promise.withResolvers(); + const update = vi.fn(async (_patch: { description: string }) => pending.promise); + const fixture = await notificationFixture({ update, dismiss: vi.fn(async () => {}) }); + try { + fixture.publish({ ...fixture.state, requests: [{ ...fixture.state.requests[0]!, principalLabel: 'First' }] }); + await vi.waitUntil(() => update.mock.calls.length === 1); + fixture.publish({ ...fixture.state, requests: [{ ...fixture.state.requests[0]!, principalLabel: 'Second' }] }); + await Promise.resolve(); + expect(update).toHaveBeenCalledOnce(); + fixture.publish({ ...fixture.state, requests: [] }); + expect(fixture.unregister).toHaveBeenCalledOnce(); + expect(fixture.message.dismiss).not.toHaveBeenCalled(); + pending.resolve(); + await vi.waitUntil(() => fixture.message.dismiss.mock.calls.length === 1); + expect(update).toHaveBeenCalledOnce(); + expect(fixture.message.dismiss).toHaveBeenCalledOnce(); + } finally { + pending.resolve(); + fixture.dispose(); + } +}); + +it.each(['expiry', 'disposal'] as const)('cleans up delayed message creation after %s', async (ending) => { + expect.assertions(3); + const message = { dismiss: vi.fn(async () => {}), update: vi.fn(async (_patch: { description: string }) => {}) }; + const creation = Promise.withResolvers(); + const fixture = await notificationFixture(message, creation.promise); + try { + if (ending === 'expiry') fixture.publish({ ...fixture.state, requests: [{ ...fixture.state.requests[0]!, expiresAt: 1 }] }); + else fixture.dispose(); + expect(fixture.unregister).toHaveBeenCalledOnce(); + creation.resolve(message); + await vi.waitUntil(() => message.dismiss.mock.calls.length === 1); + expect(message.dismiss).toHaveBeenCalledOnce(); + expect(message.update).not.toHaveBeenCalled(); + } finally { + fixture.dispose(); + } +}); + +it('continues message updates after a rejected update and still removes the message', async () => { + expect.assertions(3); + const fixture = await notificationFixture(); + const failure = new Error('Host update failed'); + fixture.message.update.mockRejectedValueOnce(failure); + const report = vi.spyOn(console, 'error').mockImplementation(() => {}); + try { + fixture.publish({ ...fixture.state, requests: [{ ...fixture.state.requests[0]!, principalLabel: 'First' }] }); + await vi.waitUntil(() => report.mock.calls.length === 1); + fixture.publish({ ...fixture.state, requests: [{ ...fixture.state.requests[0]!, principalLabel: 'Second' }] }); + await vi.waitUntil(() => fixture.message.update.mock.calls.length === 2); + expect(fixture.message.update).toHaveBeenLastCalledWith({ description: 'Second requests interact access with same-origin navigation.' }); + fixture.publish({ ...fixture.state, requests: [] }); + await vi.waitUntil(() => fixture.message.dismiss.mock.calls.length === 1); + expect(fixture.message.dismiss).toHaveBeenCalledOnce(); + expect(report).toHaveBeenCalledWith('Unable to update browser-control notification.', failure); + } finally { + fixture.dispose(); + report.mockRestore(); + } +}); diff --git a/packages/mcp/README.md b/packages/mcp/README.md index 912afe8..b1b1e2c 100644 --- a/packages/mcp/README.md +++ b/packages/mcp/README.md @@ -11,3 +11,5 @@ import { createCdbToolSession } from '@dvcol/cdb-mcp'; ``` Create one tool session per authenticated principal and dispose it with that principal. See the [repository documentation](https://github.com/dvcol/chrome-debugger-bridge#readme) for the authorization model. + +Opt-in Node diagnostic channels keep execution details outside agent responses. `cdb.mcp.action` reports action phases, CDP command outcomes and dispatch boundaries. `cdb.mcp.snapshot` separates frame discovery, command round trips, artifact reads and formatting; command round trips include provider and transport time. `cdb.mcp.hit-test` reports element and ancestor-frame geometry and hit nodes for diagnosing coordinate mismatches. Treat hit-test records as private page diagnostics. diff --git a/packages/mcp/src/index.ts b/packages/mcp/src/index.ts index 1b18727..1eb6e91 100644 --- a/packages/mcp/src/index.ts +++ b/packages/mcp/src/index.ts @@ -266,6 +266,8 @@ interface InputActionScope { const inputActionScope = new AsyncLocalStorage(); const inputActionDiagnostics = channel('cdb.mcp.action'); +const hitTestDiagnostics = channel('cdb.mcp.hit-test'); +const snapshotDiagnostics = channel('cdb.mcp.snapshot'); const elementActionNames = new Set(['browser.click', 'browser.fill', 'browser.type', 'browser.press', 'browser.check', 'browser.uncheck', 'browser.focus', 'browser.hover', 'browser.scroll_into_view', 'browser.drag', 'browser.select_option']); function inputPhase(phase: InputPhase): void { @@ -1819,8 +1821,8 @@ async function collectAccessibilityCandidates( .map(session => ({ sessionId: session.id })), ]; })(); - const candidates: AccessibilityCandidate[] = []; - for (const context of contexts === undefined ? await accessibilityFrameContexts(client, target, resolvedContexts, signal) : resolvedContexts) { + const frameContexts = contexts === undefined ? await accessibilityFrameContexts(client, target, resolvedContexts, signal) : resolvedContexts; + const collectContext = async (context: LocatorContext): Promise => { const sessionAuthority = { ...authority, ...(context.sessionId === undefined ? {} : { sessionId: context.sessionId }), @@ -1866,10 +1868,8 @@ async function collectAccessibilityCandidates( ...(context.sessionId === undefined ? {} : { sessionId: context.sessionId }), targetId: target.id, }); - if (context.rootBackendNodeId === undefined) { - candidates.push(...contextCandidates); - continue; - } + if (context.rootBackendNodeId === undefined) return contextCandidates; + const candidates: AccessibilityCandidate[] = []; const rootCandidate: AccessibilityCandidate = { backendNodeId: context.rootBackendNodeId, generation: target.generation, @@ -1879,6 +1879,13 @@ async function collectAccessibilityCandidates( }; for (const candidate of contextCandidates) if (await candidateContains(client, target, rootCandidate, candidate, signal, containmentDocuments)) candidates.push(candidate); + return candidates; + }; + const candidates: AccessibilityCandidate[] = []; + /** Bound independent reads while retaining frame order and complete ambiguity checks. */ + for (let offset = 0; offset < frameContexts.length; offset += 4) { + const collected = await Promise.all(frameContexts.slice(offset, offset + 4).map(collectContext)); + candidates.push(...collected.flat()); } return candidates; } @@ -2488,6 +2495,7 @@ async function assertAncestorFrameHit( const parentViewport = commandResultValue(await execute('Page.getLayoutMetrics', {}, parent.sessionId ?? null)); const parentLayout = property(parentViewport, 'cssLayoutViewport') ?? property(parentViewport, 'layoutViewport'); const hit = commandResultValue(await execute('DOM.getNodeForLocation', { x: Math.round(mappedPoint.x + (numberValue(property(parentLayout, 'pageX')) ?? 0)), y: Math.round(mappedPoint.y + (numberValue(property(parentLayout, 'pageY')) ?? 0)), includeUserAgentShadowDOM: false }, parent.sessionId ?? null)); + if (hitTestDiagnostics.hasSubscribers) hitTestDiagnostics.publish({ phase: 'ancestor', frameId, sessionId: frame.sessionId, parentSessionId: parent.sessionId, backendNodeId, quad, viewport: layout, parentViewport: parentLayout, mappedPoint, hit }); if (property(hit, 'backendNodeId') !== backendNodeId && property(hit, 'frameId') !== frameId) throw new McpToolError('MCP_ELEMENT_COVERED', 'An ancestor frame is covered by another element.', undefined, true); frameId = frame.parentId; @@ -2565,6 +2573,10 @@ async function executeElementClick( y: candidatePoint.y + (numberValue(property(layout, 'pageY')) ?? 0), })); const hitBackendNodeId = numberValue(property(hitResult, 'backendNodeId')); + if (hitTestDiagnostics.hasSubscribers) { + const hitNode = hitBackendNodeId === undefined || hitBackendNodeId === element.backendNodeId ? undefined : commandResultValue(await execute('DOM.describeNode', { backendNodeId: hitBackendNodeId, depth: 0 })); + hitTestDiagnostics.publish({ phase: 'element', backendNodeId: element.backendNodeId, sessionId: element.sessionId, viewport: layout, geometry: commandResultValue(secondGeometry), point: candidatePoint, hit: hitResult, hitNode }); + } await assertElementHit(element.backendNodeId, hitBackendNodeId, execute); await assertAncestorFrameHit(element, candidatePoint, execute); point = candidatePoint; @@ -3741,6 +3753,7 @@ function createCdbToolDefinitionsForSession( const accessibilityMode = input.mode; const budget: AccessibilitySnapshotBudget = { remainingCharacters: Math.max(0, maximumCharacters - snapshotTruncationNotice.length - 128), remainingNodes: maximumNodes, truncated: false }; const captureAccessibility = async (sessionId?: string, frameId?: string): Promise => { + const started = performance.now(); const commandValue = await executeArtifactCommand( client, { ...targetAuthority, ...(sessionId === undefined ? {} : { sessionId }) }, @@ -3748,12 +3761,15 @@ function createCdbToolDefinitionsForSession( root === undefined ? (frameId === undefined ? {} : { frameId }) : { backendNodeId: root.backendNodeId }, context.mcpReq.signal, ); - return formatAccessibilityTree(await readableCommandValue( + const commandCompleted = performance.now(); + const readable = await readableCommandValue( client, targetAuthority, commandValue, context.mcpReq.signal, - ), { + ); + const readCompleted = performance.now(); + const formatted = formatAccessibilityTree(readable, { budget, generation: target.generation, maximumDepth: input.maximumDepth, @@ -3762,8 +3778,12 @@ function createCdbToolDefinitionsForSession( state: sessionState, targetId: target.id, }); + if (snapshotDiagnostics.hasSubscribers) snapshotDiagnostics.publish({ phase: 'frame', commandMilliseconds: commandCompleted - started, readMilliseconds: readCompleted - commandCompleted, formatMilliseconds: performance.now() - readCompleted, outputCharacters: formatted.length }); + return formatted; }; + const discoveryStarted = performance.now(); const contexts = root === undefined ? await accessibilityFrameContexts(client, target, [{}, ...iframeSessions.map(session => ({ sessionId: session.id }))], context.mcpReq.signal) : [{ ...(root.sessionId === undefined ? {} : { sessionId: root.sessionId }) }]; + if (snapshotDiagnostics.hasSubscribers) snapshotDiagnostics.publish({ phase: 'frame-discovery', milliseconds: performance.now() - discoveryStarted, frameCount: contexts.length }); const sections: string[] = []; for (const [index, frame] of contexts.entries()) { if (budget.remainingNodes === 0 || budget.remainingCharacters < 128) { diff --git a/packages/mcp/test/locator-session.test.ts b/packages/mcp/test/locator-session.test.ts index 3740cc5..8291a1c 100644 --- a/packages/mcp/test/locator-session.test.ts +++ b/packages/mcp/test/locator-session.test.ts @@ -660,3 +660,47 @@ it('never replays a locator action after pointer input may have dispatched', asy expect(JSON.parse(text(result))).toMatchObject({ code: 'MCP_ACTION_OUTCOME_UNKNOWN' }); expect(pointerCommands.map(command => command.parameters?.type)).toEqual(['mouseMoved', 'mousePressed']); }); + +it('bounds concurrent frame discovery and waits for every match before deciding ambiguity', async () => { + expect.assertions(4); + const releaseQueries = Promise.withResolvers(); + let queries = 0; + let inputs = 0; + const childSessions = Array.from({ length: 8 }, (_unused, index) => ({ id: `00000000-0000-4000-8000-${String(index).padStart(12, '0')}`, generation: 1, type: 'iframe' })); + const client = { + async acquireLease(request: { readonly requestedMethods: readonly string[] }) { + return lease(request.requestedMethods); + }, + async cancelCommand() {}, + async executeCommand(command: CdpCommand) { + if (command.method.startsWith('Input.')) inputs += 1; + if (command.method === 'Bridge.listChildSessions') return { value: { sessions: childSessions } }; + if (command.method === 'Accessibility.getRootAXNode') return { value: { node: { backendDOMNodeId: 1 } } }; + if (command.method === 'Accessibility.queryAXTree') { + queries += 1; + await releaseQueries.promise; + return { value: { nodes: [{ backendDOMNodeId: 20, childIds: [], ignored: false, name: { value: 'Save' }, nodeId: 'save', role: { value: 'button' } }] } }; + } + return { value: {} }; + }, + async listTargets() { + return [target]; + }, + async releaseLease() {}, + } as unknown as McpChromeDebuggerBridgeClient; + const session = createCdbToolSession({ client }); + const targetRef = session.projectTarget(target)?.targetRef; + const click = session.definitions.find(definition => definition.name === 'browser.click'); + if (targetRef === undefined || click === undefined) throw new Error('browser.click is missing.'); + const result = click.invoke({ targetRef, locator: { role: 'button', name: { match: 'exact', value: 'Save' } }, timeoutMilliseconds: 10_000 }); + try { + await expect.poll(() => queries).toBe(4); + expect(inputs).toBe(0); + releaseQueries.resolve(); + expect(text(await result)).toContain('MCP_LOCATOR_AMBIGUOUS'); + expect(queries).toBe(9); + } finally { + releaseQueries.resolve(); + await result; + } +}); diff --git a/patches/@devframes__hub-ui@0.9.10.patch b/patches/@devframes__hub-ui@0.9.10.patch new file mode 100644 index 0000000..e876d5f --- /dev/null +++ b/patches/@devframes__hub-ui@0.9.10.patch @@ -0,0 +1,32 @@ +diff --git a/dist/client/embedded.js b/dist/client/embedded.js +index 36d996e28ee08a16b58d3e9f768a626a3dc1f563..4f64573e650d4b653ee1a5c8cea8454351f695c7 100644 +--- a/dist/client/embedded.js ++++ b/dist/client/embedded.js +@@ -11431,8 +11431,15 @@ function aS(e) { + }), n = /* @__PURE__ */ new Map(), r = !0, i = null; + async function a() { + let a = await e.rpc.call("devframes:plugin:messages:list", i), o = 0; +- a.full && n.clear(); +- for (let e of a.removedIds) n.delete(e); ++ let removedIds = a.removedIds; ++ if (a.full) { ++ const retainedIds = new Set(a.entries.map(entry => entry.id)); ++ removedIds = [...n.keys()].filter(id => !retainedIds.has(id)); ++ } ++ for (const id of removedIds) { ++ n.delete(id); ++ eS(id); ++ } + for (let e of a.entries) { + let t = n.get(e.id); + t || o++, iS(e, t, r) && $x(e), n.set(e.id, e); +diff --git a/dist/client/standalone/assets/DockStandalone-3_CzgPVL.js b/dist/client/standalone/assets/DockStandalone-3_CzgPVL.js +index 76be7462d86e8015ad312dfb33350a99b38e9efd..df9a767468ddfdba10dcb3450dbbf4053ba0e2b7 100644 +--- a/dist/client/standalone/assets/DockStandalone-3_CzgPVL.js ++++ b/dist/client/standalone/assets/DockStandalone-3_CzgPVL.js +@@ -1,3 +1,3 @@ + import{$ as e,A as t,B as n,C as r,E as i,F as a,G as o,I as s,K as c,L as l,M as u,O as d,P as f,Q as p,R as m,S as h,T as g,U as _,V as v,Z as y,_ as b,b as x,c as S,d as C,f as w,g as T,h as E,j as ee,k as te,l as D,m as ne,nt as re,o as ie,p as ae,q as oe,r as se,rt as ce,s as le,t as ue,u as O,v as de,w as fe,x as pe,z as me}from"./popup-B_knQQry.js";import{$ as he,A as ge,B as _e,C as ve,D as ye,E as k,Et as be,F as A,G as xe,H as Se,I as j,J as Ce,K as we,L as M,M as N,N as P,O as Te,Ot as Ee,P as F,Q as De,R as I,S as Oe,T as ke,U as Ae,V as je,W as Me,X as L,Y as Ne,Z as Pe,_ as Fe,_t as R,at as z,b as Ie,ct as Le,d as Re,dt as B,et as ze,f as Be,ft as Ve,g as He,gt as Ue,h as We,ht as V,it as Ge,j as Ke,k as H,l as qe,lt as Je,m as Ye,mt as U,nt as Xe,ot as Ze,p as Qe,pt as W,q as G,rt as $e,st as et,tt as K,u as tt,ut as nt,v as rt,vt as q,w as it,x as at,xt as ot,y as st,z as J}from"./index-BGXSUn0A.js";var ct=`*,:before,:after{box-sizing:border-box;border-style:solid;border-width:0;border-color:var(--un-hub-default-border-color,#e5e7eb)}:before,:after{--un-hub-content:""}html,:host{-webkit-text-size-adjust:100%;-moz-tab-size:4;tab-size:4;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5}body{line-height:inherit;margin:0}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-feature-settings:normal;font-variation-settings:normal;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-feature-settings:inherit;font-variation-settings:inherit;font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button;background-color:#0000;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{margin:0;padding:0;list-style:none}dialog{padding:0}textarea{resize:vertical}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}:root{--un-hub-text-opacity:100%}:host{--app-scrollbar-size:6px;--app-scrollbar-radius:1px;--app-scrollbar-thumb:#8884;--app-scrollbar-thumb-hover:#8885}::-webkit-scrollbar{width:var(--app-scrollbar-size)}::-webkit-scrollbar:horizontal{height:var(--app-scrollbar-size)}::-webkit-scrollbar-corner{background:0 0}::-webkit-scrollbar-thumb{background-color:var(--app-scrollbar-thumb);border-radius:var(--app-scrollbar-radius);transition:background .2s}::-webkit-scrollbar-thumb:hover{background-color:var(--app-scrollbar-thumb-hover)}::-webkit-scrollbar-track{border-radius:var(--app-scrollbar-radius);background:0 0}#devframes-anchor{z-index:2147483644;box-sizing:border-box;transform-origin:50%;width:0;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-size:15px;position:fixed;transform:translate(-50%,-50%)rotate(0)}#devframes-anchor #devframes-dock-container{width:max-content;height:var(--devframes-dock-height,40px);min-width:var(--devframes-dock-min-width,100px);display:flex;position:absolute;top:0;left:0;transform:translate(-50%,-50%)}#devframes-anchor.devframes-vertical #devframes-dock-container{transition-property:all;transition-duration:.5s;transition-timing-function:cubic-bezier(.4,0,.2,1);transform:translate(-50%,-50%)rotate(90deg)}#devframes-anchor #devframes-dock{touch-action:none;-webkit-user-select:none;user-select:none;--un-hub-border-opacity:.13;border-width:1px;border-color:rgb(153 153 153/var(--un-hub-border-opacity));--un-hub-backdrop-blur:blur(7px);height:100%;backdrop-filter:var(--un-hub-backdrop-blur) var(--un-hub-backdrop-brightness) var(--un-hub-backdrop-contrast) var(--un-hub-backdrop-grayscale) var(--un-hub-backdrop-hue-rotate) var(--un-hub-backdrop-invert) var(--un-hub-backdrop-opacity) var(--un-hub-backdrop-saturate) var(--un-hub-backdrop-sepia);--un-hub-text-opacity:1;color:rgb(51 51 51/var(--un-hub-text-opacity));--un-hub-shadow:var(--un-hub-shadow-inset) 0 1px 3px 0 var(--un-hub-shadow-color,#0000001a),var(--un-hub-shadow-inset) 0 1px 2px -1px var(--un-hub-shadow-color,#0000001a);box-shadow:var(--un-hub-ring-offset-shadow), var(--un-hub-ring-shadow), var(--un-hub-shadow);height:var(--devframes-dock-height,40px);width:calc-size(max-content, size);background-color:#fff;border-radius:.5rem;margin:auto;transition-property:all;transition-duration:.5s;transition-timing-function:cubic-bezier(.4,0,.2,1);display:flex}.dark #devframes-anchor #devframes-dock{--un-hub-text-opacity:1;color:rgb(255 255 255/var(--un-hub-text-opacity));background-color:#111c}#devframes-anchor.devframes-minimized #devframes-dock{width:var(--devframes-dock-minimized-size,22px);height:var(--devframes-dock-minimized-size,22px);border-radius:9999px;padding:2px 0}#devframes-anchor:hover #devframes-glowing{opacity:.6}#devframes-anchor #devframes-glowing{pointer-events:none;opacity:0;width:var(--devframes-dock-glow-size,160px);height:var(--devframes-dock-glow-size,160px);filter:blur(var(--devframes-dock-glow-blur,60px));background-image:linear-gradient(45deg, var(--colors-primary-300,#adc77f), var(--colors-primary-500,#517158), var(--colors-primary-DEFAULT,#486954));border-radius:9999px;transition-property:all;transition-duration:1s;transition-timing-function:cubic-bezier(0,0,.2,1);position:absolute;top:0;left:0;transform:translate(-50%,-50%)}#devframes-edge-panel{transition-property:all;transition-duration:.5s;transition-timing-function:cubic-bezier(.16,1,.3,1)}@media print{#devframes-anchor{display:none}}.devframes-resize-handle-horizontal{cursor:ns-resize;border-radius:.375rem;height:10px;margin-top:-5px;margin-bottom:-5px;position:absolute;left:6px;right:6px}.devframes-resize-handle-vertical{cursor:ew-resize;border-radius:.375rem;width:10px;margin-left:-5px;margin-right:-5px;position:absolute;top:6px;bottom:0}.devframes-resize-handle-corner{border-radius:.375rem;width:14px;height:14px;margin:-6px;position:absolute}.devframes-resize-handle{z-index:30}.devframes-resize-handle:hover{background-color:#9ca3af1a}@keyframes devframes-shake{0%,to{transform:translate(0)}20%,60%{transform:translate(-5px)}40%,80%{transform:translate(5px)}}.devframes-shake{animation:.4s cubic-bezier(.36,.07,.19,.97) devframes-shake}@media (prefers-reduced-motion:reduce){.devframes-shake{animation:none}#devframes-edge-panel{transition:none!important}}*,:before,:after,::backdrop{--un-hub-rotate:0;--un-hub-rotate-x:0;--un-hub-rotate-y:0;--un-hub-rotate-z:0;--un-hub-scale-x:1;--un-hub-scale-y:1;--un-hub-scale-z:1;--un-hub-skew-x:0;--un-hub-skew-y:0;--un-hub-translate-x:0;--un-hub-translate-y:0;--un-hub-translate-z:0;--un-hub-pan-x: ;--un-hub-pan-y: ;--un-hub-pinch-zoom: ;--un-hub-scroll-snap-strictness:proximity;--un-hub-ordinal: ;--un-hub-slashed-zero: ;--un-hub-numeric-figure: ;--un-hub-numeric-spacing: ;--un-hub-numeric-fraction: ;--un-hub-border-spacing-x:0;--un-hub-border-spacing-y:0;--un-hub-ring-offset-shadow:0 0 #0000;--un-hub-ring-shadow:0 0 #0000;--un-hub-shadow-inset: ;--un-hub-shadow:0 0 #0000;--un-hub-ring-inset: ;--un-hub-ring-offset-width:0px;--un-hub-ring-offset-color:#fff;--un-hub-ring-width:0px;--un-hub-ring-color:#93c5fd80;--un-hub-blur: ;--un-hub-brightness: ;--un-hub-contrast: ;--un-hub-drop-shadow: ;--un-hub-grayscale: ;--un-hub-hue-rotate: ;--un-hub-invert: ;--un-hub-saturate: ;--un-hub-sepia: ;--un-hub-backdrop-blur: ;--un-hub-backdrop-brightness: ;--un-hub-backdrop-contrast: ;--un-hub-backdrop-grayscale: ;--un-hub-backdrop-hue-rotate: ;--un-hub-backdrop-invert: ;--un-hub-backdrop-opacity: ;--un-hub-backdrop-saturate: ;--un-hub-backdrop-sepia: }.i-catppuccin\\:folder{background:url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 16 16' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='none' stroke='%23cad3f5' stroke-linecap='round' stroke-linejoin='round' d='M4.5 4.5H12c.83 0 1.5.67 1.5 1.5v6c0 .83-.67 1.5-1.5 1.5H2A1.5 1.5 0 0 1 .5 12V3.5a1 1 0 0 1 1-1h5a1 1 0 0 1 1 1v1'/%3E%3C/svg%3E") 0 0/100% 100% no-repeat;width:1.11em;height:1.11em}.i-ph-arrow-clockwise{--un-hub-icon:url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='M240 56v48a8 8 0 0 1-8 8h-48a8 8 0 0 1 0-16h27.4l-26.59-24.36l-.25-.24a80 80 0 1 0-1.67 114.78a8 8 0 0 1 11 11.63A95.44 95.44 0 0 1 128 224h-1.32a96 96 0 1 1 69.07-164L224 85.8V56a8 8 0 1 1 16 0'/%3E%3C/svg%3E");-webkit-mask:var(--un-hub-icon) no-repeat;mask:var(--un-hub-icon) no-repeat;color:inherit;background-color:currentColor;width:1.11em;height:1.11em;-webkit-mask-size:100% 100%;mask-size:100% 100%}.i-ph-arrow-clockwise-duotone{--un-hub-icon:url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M216 128a88 88 0 1 1-88-88a88 88 0 0 1 88 88' opacity='.2'/%3E%3Cpath d='M240 56v48a8 8 0 0 1-8 8h-48a8 8 0 0 1 0-16h27.4l-26.59-24.36l-.25-.24a80 80 0 1 0-1.67 114.78a8 8 0 0 1 11 11.63A95.44 95.44 0 0 1 128 224h-1.32a96 96 0 1 1 69.07-164L224 85.8V56a8 8 0 1 1 16 0'/%3E%3C/g%3E%3C/svg%3E");-webkit-mask:var(--un-hub-icon) no-repeat;mask:var(--un-hub-icon) no-repeat;color:inherit;background-color:currentColor;width:1.11em;height:1.11em;-webkit-mask-size:100% 100%;mask-size:100% 100%}.i-ph-arrow-counter-clockwise{--un-hub-icon:url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='M224 128a96 96 0 0 1-94.71 96H128a95.38 95.38 0 0 1-65.9-26.2a8 8 0 0 1 11-11.63a80 80 0 1 0-1.67-114.78a3 3 0 0 1-.26.25L44.59 96H72a8 8 0 0 1 0 16H24a8 8 0 0 1-8-8V56a8 8 0 0 1 16 0v29.8L60.25 60A96 96 0 0 1 224 128'/%3E%3C/svg%3E");-webkit-mask:var(--un-hub-icon) no-repeat;mask:var(--un-hub-icon) no-repeat;color:inherit;background-color:currentColor;width:1.11em;height:1.11em;-webkit-mask-size:100% 100%;mask-size:100% 100%}.i-ph-arrow-square-out-duotone{--un-hub-icon:url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M184 80v128a8 8 0 0 1-8 8H48a8 8 0 0 1-8-8V80a8 8 0 0 1 8-8h128a8 8 0 0 1 8 8' opacity='.2'/%3E%3Cpath d='M224 104a8 8 0 0 1-16 0V59.32l-66.33 66.34a8 8 0 0 1-11.32-11.32L196.68 48H152a8 8 0 0 1 0-16h64a8 8 0 0 1 8 8Zm-40 24a8 8 0 0 0-8 8v72H48V80h72a8 8 0 0 0 0-16H48a16 16 0 0 0-16 16v128a16 16 0 0 0 16 16h128a16 16 0 0 0 16-16v-72a8 8 0 0 0-8-8'/%3E%3C/g%3E%3C/svg%3E");-webkit-mask:var(--un-hub-icon) no-repeat;mask:var(--un-hub-icon) no-repeat;color:inherit;background-color:currentColor;width:1.11em;height:1.11em;-webkit-mask-size:100% 100%;mask-size:100% 100%}.i-ph-arrows-counter-clockwise-duotone{--un-hub-icon:url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M216 128a88 88 0 1 1-88-88a88 88 0 0 1 88 88' opacity='.2'/%3E%3Cpath d='M88 104H40a8 8 0 0 1-8-8V48a8 8 0 0 1 16 0v28.69l14.63-14.63A95.43 95.43 0 0 1 130 33.94h.53a95.36 95.36 0 0 1 67.07 27.33a8 8 0 0 1-11.18 11.44a79.52 79.52 0 0 0-55.89-22.77h-.45a79.56 79.56 0 0 0-56.14 23.43L59.31 88H88a8 8 0 0 1 0 16m128 48h-48a8 8 0 0 0 0 16h28.69l-14.63 14.63a79.56 79.56 0 0 1-56.13 23.43h-.45a79.52 79.52 0 0 1-55.89-22.77a8 8 0 1 0-11.18 11.44a95.36 95.36 0 0 0 67.07 27.33h.52a95.43 95.43 0 0 0 67.36-28.12L208 179.31V208a8 8 0 0 0 16 0v-48a8 8 0 0 0-8-8'/%3E%3C/g%3E%3C/svg%3E");-webkit-mask:var(--un-hub-icon) no-repeat;mask:var(--un-hub-icon) no-repeat;color:inherit;background-color:currentColor;width:1.11em;height:1.11em;-webkit-mask-size:100% 100%;mask-size:100% 100%}.i-ph-arrows-out-duotone{--un-hub-icon:url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M208 48v160H48V48Z' opacity='.2'/%3E%3Cpath d='M216 48v48a8 8 0 0 1-16 0V67.31l-42.34 42.35a8 8 0 0 1-11.32-11.32L188.69 56H160a8 8 0 0 1 0-16h48a8 8 0 0 1 8 8M98.34 146.34L56 188.69V160a8 8 0 0 0-16 0v48a8 8 0 0 0 8 8h48a8 8 0 0 0 0-16H67.31l42.35-42.34a8 8 0 0 0-11.32-11.32M208 152a8 8 0 0 0-8 8v28.69l-42.34-42.35a8 8 0 0 0-11.32 11.32L188.69 200H160a8 8 0 0 0 0 16h48a8 8 0 0 0 8-8v-48a8 8 0 0 0-8-8M67.31 56H96a8 8 0 0 0 0-16H48a8 8 0 0 0-8 8v48a8 8 0 0 0 16 0V67.31l42.34 42.35a8 8 0 0 0 11.32-11.32Z'/%3E%3C/g%3E%3C/svg%3E");-webkit-mask:var(--un-hub-icon) no-repeat;mask:var(--un-hub-icon) no-repeat;color:inherit;background-color:currentColor;width:1.11em;height:1.11em;-webkit-mask-size:100% 100%;mask-size:100% 100%}.i-ph-cards-three-duotone{--un-hub-icon:url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M216 104v96a8 8 0 0 1-8 8H48a8 8 0 0 1-8-8v-96a8 8 0 0 1 8-8h160a8 8 0 0 1 8 8' opacity='.2'/%3E%3Cpath d='M208 88H48a16 16 0 0 0-16 16v96a16 16 0 0 0 16 16h160a16 16 0 0 0 16-16v-96a16 16 0 0 0-16-16m0 112H48v-96h160zM48 64a8 8 0 0 1 8-8h144a8 8 0 0 1 0 16H56a8 8 0 0 1-8-8m16-32a8 8 0 0 1 8-8h112a8 8 0 0 1 0 16H72a8 8 0 0 1-8-8'/%3E%3C/g%3E%3C/svg%3E");-webkit-mask:var(--un-hub-icon) no-repeat;mask:var(--un-hub-icon) no-repeat;color:inherit;background-color:currentColor;width:1.11em;height:1.11em;-webkit-mask-size:100% 100%;mask-size:100% 100%}.i-ph-caret-down,.i-ph\\:caret-down{--un-hub-icon:url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='m213.66 101.66l-80 80a8 8 0 0 1-11.32 0l-80-80a8 8 0 0 1 11.32-11.32L128 164.69l74.34-74.35a8 8 0 0 1 11.32 11.32'/%3E%3C/svg%3E");-webkit-mask:var(--un-hub-icon) no-repeat;mask:var(--un-hub-icon) no-repeat;color:inherit;background-color:currentColor;width:1.11em;height:1.11em;-webkit-mask-size:100% 100%;mask-size:100% 100%}.i-ph-caret-left,.i-ph\\:caret-left{--un-hub-icon:url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='M165.66 202.34a8 8 0 0 1-11.32 11.32l-80-80a8 8 0 0 1 0-11.32l80-80a8 8 0 0 1 11.32 11.32L91.31 128Z'/%3E%3C/svg%3E");-webkit-mask:var(--un-hub-icon) no-repeat;mask:var(--un-hub-icon) no-repeat;color:inherit;background-color:currentColor;width:1.11em;height:1.11em;-webkit-mask-size:100% 100%;mask-size:100% 100%}.i-ph-caret-right,.i-ph\\:caret-right{--un-hub-icon:url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='m181.66 133.66l-80 80a8 8 0 0 1-11.32-11.32L164.69 128L90.34 53.66a8 8 0 0 1 11.32-11.32l80 80a8 8 0 0 1 0 11.32'/%3E%3C/svg%3E");-webkit-mask:var(--un-hub-icon) no-repeat;mask:var(--un-hub-icon) no-repeat;color:inherit;background-color:currentColor;width:1.11em;height:1.11em;-webkit-mask-size:100% 100%;mask-size:100% 100%}.i-ph-caret-up{--un-hub-icon:url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='M213.66 165.66a8 8 0 0 1-11.32 0L128 91.31l-74.34 74.35a8 8 0 0 1-11.32-11.32l80-80a8 8 0 0 1 11.32 0l80 80a8 8 0 0 1 0 11.32'/%3E%3C/svg%3E");-webkit-mask:var(--un-hub-icon) no-repeat;mask:var(--un-hub-icon) no-repeat;color:inherit;background-color:currentColor;width:1.11em;height:1.11em;-webkit-mask-size:100% 100%;mask-size:100% 100%}.i-ph-check-bold,.i-ph\\:check-bold{--un-hub-icon:url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='m232.49 80.49l-128 128a12 12 0 0 1-17 0l-56-56a12 12 0 1 1 17-17L96 183L215.51 63.51a12 12 0 0 1 17 17Z'/%3E%3C/svg%3E");-webkit-mask:var(--un-hub-icon) no-repeat;mask:var(--un-hub-icon) no-repeat;color:inherit;background-color:currentColor;width:1.11em;height:1.11em;-webkit-mask-size:100% 100%;mask-size:100% 100%}.i-ph-circle-notch{--un-hub-icon:url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='M232 128a104 104 0 0 1-208 0c0-41 23.81-78.36 60.66-95.27a8 8 0 0 1 6.68 14.54C60.15 61.59 40 93.27 40 128a88 88 0 0 0 176 0c0-34.73-20.15-66.41-51.34-80.73a8 8 0 0 1 6.68-14.54C208.19 49.64 232 87 232 128'/%3E%3C/svg%3E");-webkit-mask:var(--un-hub-icon) no-repeat;mask:var(--un-hub-icon) no-repeat;color:inherit;background-color:currentColor;width:1.11em;height:1.11em;-webkit-mask-size:100% 100%;mask-size:100% 100%}.i-ph-dots-six-vertical{--un-hub-icon:url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='M104 60a12 12 0 1 1-12-12a12 12 0 0 1 12 12m60 12a12 12 0 1 0-12-12a12 12 0 0 0 12 12m-72 44a12 12 0 1 0 12 12a12 12 0 0 0-12-12m72 0a12 12 0 1 0 12 12a12 12 0 0 0-12-12m-72 68a12 12 0 1 0 12 12a12 12 0 0 0-12-12m72 0a12 12 0 1 0 12 12a12 12 0 0 0-12-12'/%3E%3C/svg%3E");-webkit-mask:var(--un-hub-icon) no-repeat;mask:var(--un-hub-icon) no-repeat;color:inherit;background-color:currentColor;width:1.11em;height:1.11em;-webkit-mask-size:100% 100%;mask-size:100% 100%}.i-ph-dots-three-outline-duotone{--un-hub-icon:url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M152 128a24 24 0 1 1-24-24a24 24 0 0 1 24 24M48 104a24 24 0 1 0 24 24a24 24 0 0 0-24-24m160 0a24 24 0 1 0 24 24a24 24 0 0 0-24-24' opacity='.2'/%3E%3Cpath d='M128 96a32 32 0 1 0 32 32a32 32 0 0 0-32-32m0 48a16 16 0 1 1 16-16a16 16 0 0 1-16 16M48 96a32 32 0 1 0 32 32a32 32 0 0 0-32-32m0 48a16 16 0 1 1 16-16a16 16 0 0 1-16 16m160-48a32 32 0 1 0 32 32a32 32 0 0 0-32-32m0 48a16 16 0 1 1 16-16a16 16 0 0 1-16 16'/%3E%3C/g%3E%3C/svg%3E");-webkit-mask:var(--un-hub-icon) no-repeat;mask:var(--un-hub-icon) no-repeat;color:inherit;background-color:currentColor;width:1.11em;height:1.11em;-webkit-mask-size:100% 100%;mask-size:100% 100%}.i-ph-eye-slash{--un-hub-icon:url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='M53.92 34.62a8 8 0 1 0-11.84 10.76l19.24 21.17C25 88.84 9.38 123.2 8.69 124.76a8 8 0 0 0 0 6.5c.35.79 8.82 19.57 27.65 38.4C61.43 194.74 93.12 208 128 208a127.1 127.1 0 0 0 52.07-10.83l22 24.21a8 8 0 1 0 11.84-10.76Zm47.33 75.84l41.67 45.85a32 32 0 0 1-41.67-45.85M128 192c-30.78 0-57.67-11.19-79.93-33.25A133.2 133.2 0 0 1 25 128c4.69-8.79 19.66-33.39 47.35-49.38l18 19.75a48 48 0 0 0 63.66 70l14.73 16.2A112 112 0 0 1 128 192m6-95.43a8 8 0 0 1 3-15.72a48.16 48.16 0 0 1 38.77 42.64a8 8 0 0 1-7.22 8.71a6 6 0 0 1-.75 0a8 8 0 0 1-8-7.26A32.09 32.09 0 0 0 134 96.57m113.28 34.69c-.42.94-10.55 23.37-33.36 43.8a8 8 0 1 1-10.67-11.92a132.8 132.8 0 0 0 27.8-35.14a133.2 133.2 0 0 0-23.12-30.77C185.67 75.19 158.78 64 128 64a118.4 118.4 0 0 0-19.36 1.57A8 8 0 1 1 106 49.79A134 134 0 0 1 128 48c34.88 0 66.57 13.26 91.66 38.35c18.83 18.83 27.3 37.62 27.65 38.41a8 8 0 0 1 0 6.5Z'/%3E%3C/svg%3E");-webkit-mask:var(--un-hub-icon) no-repeat;mask:var(--un-hub-icon) no-repeat;color:inherit;background-color:currentColor;width:1.11em;height:1.11em;-webkit-mask-size:100% 100%;mask-size:100% 100%}.i-ph-eye-slash-duotone{--un-hub-icon:url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M128 56c-80 0-112 72-112 72s32 72 112 72s112-72 112-72s-32-72-112-72m0 112a40 40 0 1 1 40-40a40 40 0 0 1-40 40' opacity='.2'/%3E%3Cpath d='M53.92 34.62a8 8 0 1 0-11.84 10.76l19.24 21.17C25 88.84 9.38 123.2 8.69 124.76a8 8 0 0 0 0 6.5c.35.79 8.82 19.57 27.65 38.4C61.43 194.74 93.12 208 128 208a127.1 127.1 0 0 0 52.07-10.83l22 24.21a8 8 0 1 0 11.84-10.76Zm47.33 75.84l41.67 45.85a32 32 0 0 1-41.67-45.85M128 192c-30.78 0-57.67-11.19-79.93-33.25A133.2 133.2 0 0 1 25 128c4.69-8.79 19.66-33.39 47.35-49.38l18 19.75a48 48 0 0 0 63.66 70l14.73 16.2A112 112 0 0 1 128 192m6-95.43a8 8 0 0 1 3-15.72a48.16 48.16 0 0 1 38.77 42.64a8 8 0 0 1-7.22 8.71a6 6 0 0 1-.75 0a8 8 0 0 1-8-7.26A32.09 32.09 0 0 0 134 96.57m113.28 34.69c-.42.94-10.55 23.37-33.36 43.8a8 8 0 1 1-10.67-11.92a132.8 132.8 0 0 0 27.8-35.14a133.2 133.2 0 0 0-23.12-30.77C185.67 75.19 158.78 64 128 64a118.4 118.4 0 0 0-19.36 1.57A8 8 0 1 1 106 49.79A134 134 0 0 1 128 48c34.88 0 66.57 13.26 91.66 38.35c18.83 18.83 27.3 37.62 27.65 38.41a8 8 0 0 1 0 6.5Z'/%3E%3C/g%3E%3C/svg%3E");-webkit-mask:var(--un-hub-icon) no-repeat;mask:var(--un-hub-icon) no-repeat;color:inherit;background-color:currentColor;width:1.11em;height:1.11em;-webkit-mask-size:100% 100%;mask-size:100% 100%}.i-ph-globe{--un-hub-icon:url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='M128 24a104 104 0 1 0 104 104A104.12 104.12 0 0 0 128 24m88 104a87.6 87.6 0 0 1-3.33 24h-38.51a157.4 157.4 0 0 0 0-48h38.51a87.6 87.6 0 0 1 3.33 24m-114 40h52a115.1 115.1 0 0 1-26 45a115.3 115.3 0 0 1-26-45m-3.9-16a140.8 140.8 0 0 1 0-48h59.88a140.8 140.8 0 0 1 0 48ZM40 128a87.6 87.6 0 0 1 3.33-24h38.51a157.4 157.4 0 0 0 0 48H43.33A87.6 87.6 0 0 1 40 128m114-40h-52a115.1 115.1 0 0 1 26-45a115.3 115.3 0 0 1 26 45m52.33 0h-35.62a135.3 135.3 0 0 0-22.3-45.6A88.29 88.29 0 0 1 206.37 88Zm-98.74-45.6A135.3 135.3 0 0 0 85.29 88H49.63a88.29 88.29 0 0 1 57.96-45.6M49.63 168h35.66a135.3 135.3 0 0 0 22.3 45.6A88.29 88.29 0 0 1 49.63 168m98.78 45.6a135.3 135.3 0 0 0 22.3-45.6h35.66a88.29 88.29 0 0 1-57.96 45.6'/%3E%3C/svg%3E");-webkit-mask:var(--un-hub-icon) no-repeat;mask:var(--un-hub-icon) no-repeat;color:inherit;background-color:currentColor;width:1.11em;height:1.11em;-webkit-mask-size:100% 100%;mask-size:100% 100%}.i-ph-keyboard-duotone{--un-hub-icon:url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M232 64v128a8 8 0 0 1-8 8H32a8 8 0 0 1-8-8V64a8 8 0 0 1 8-8h192a8 8 0 0 1 8 8' opacity='.2'/%3E%3Cpath d='M224 48H32a16 16 0 0 0-16 16v128a16 16 0 0 0 16 16h192a16 16 0 0 0 16-16V64a16 16 0 0 0-16-16m0 144H32V64h192zm-16-64a8 8 0 0 1-8 8H56a8 8 0 0 1 0-16h144a8 8 0 0 1 8 8m0-32a8 8 0 0 1-8 8H56a8 8 0 0 1 0-16h144a8 8 0 0 1 8 8M72 160a8 8 0 0 1-8 8h-8a8 8 0 0 1 0-16h8a8 8 0 0 1 8 8m96 0a8 8 0 0 1-8 8H96a8 8 0 0 1 0-16h64a8 8 0 0 1 8 8m40 0a8 8 0 0 1-8 8h-8a8 8 0 0 1 0-16h8a8 8 0 0 1 8 8'/%3E%3C/g%3E%3C/svg%3E");-webkit-mask:var(--un-hub-icon) no-repeat;mask:var(--un-hub-icon) no-repeat;color:inherit;background-color:currentColor;width:1.11em;height:1.11em;-webkit-mask-size:100% 100%;mask-size:100% 100%}.i-ph-laptop-duotone{--un-hub-icon:url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M216 72v104H40V72a16 16 0 0 1 16-16h144a16 16 0 0 1 16 16' opacity='.2'/%3E%3Cpath d='M232 168h-8V72a24 24 0 0 0-24-24H56a24 24 0 0 0-24 24v96h-8a8 8 0 0 0-8 8v16a24 24 0 0 0 24 24h176a24 24 0 0 0 24-24v-16a8 8 0 0 0-8-8M48 72a8 8 0 0 1 8-8h144a8 8 0 0 1 8 8v96H48Zm176 120a8 8 0 0 1-8 8H40a8 8 0 0 1-8-8v-8h192ZM152 88a8 8 0 0 1-8 8h-32a8 8 0 0 1 0-16h32a8 8 0 0 1 8 8'/%3E%3C/g%3E%3C/svg%3E");-webkit-mask:var(--un-hub-icon) no-repeat;mask:var(--un-hub-icon) no-repeat;color:inherit;background-color:currentColor;width:1.11em;height:1.11em;-webkit-mask-size:100% 100%;mask-size:100% 100%}.i-ph-layout-duotone{--un-hub-icon:url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M104 104v104H40a8 8 0 0 1-8-8v-96Z' opacity='.2'/%3E%3Cpath d='M216 40H40a16 16 0 0 0-16 16v144a16 16 0 0 0 16 16h176a16 16 0 0 0 16-16V56a16 16 0 0 0-16-16m0 16v40H40V56ZM40 112h56v88H40Zm176 88H112v-88h104z'/%3E%3C/g%3E%3C/svg%3E");-webkit-mask:var(--un-hub-icon) no-repeat;mask:var(--un-hub-icon) no-repeat;color:inherit;background-color:currentColor;width:1.11em;height:1.11em;-webkit-mask-size:100% 100%;mask-size:100% 100%}.i-ph-magnifying-glass-duotone{--un-hub-icon:url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M192 112a80 80 0 1 1-80-80a80 80 0 0 1 80 80' opacity='.2'/%3E%3Cpath d='m229.66 218.34l-50.06-50.06a88.21 88.21 0 1 0-11.32 11.31l50.06 50.07a8 8 0 0 0 11.32-11.32M40 112a72 72 0 1 1 72 72a72.08 72.08 0 0 1-72-72'/%3E%3C/g%3E%3C/svg%3E");-webkit-mask:var(--un-hub-icon) no-repeat;mask:var(--un-hub-icon) no-repeat;color:inherit;background-color:currentColor;width:1.11em;height:1.11em;-webkit-mask-size:100% 100%;mask-size:100% 100%}.i-ph-moon-duotone{--un-hub-icon:url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M227.89 147.89A96 96 0 1 1 108.11 28.11a96.09 96.09 0 0 0 119.78 119.78' opacity='.2'/%3E%3Cpath d='M233.54 142.23a8 8 0 0 0-8-2a88.08 88.08 0 0 1-109.8-109.8a8 8 0 0 0-10-10a104.84 104.84 0 0 0-52.91 37A104 104 0 0 0 136 224a103.1 103.1 0 0 0 62.52-20.88a104.84 104.84 0 0 0 37-52.91a8 8 0 0 0-1.98-7.98m-44.64 48.11A88 88 0 0 1 65.66 67.11a89 89 0 0 1 31.4-26A106 106 0 0 0 96 56a104.11 104.11 0 0 0 104 104a106 106 0 0 0 14.92-1.06a89 89 0 0 1-26.02 31.4'/%3E%3C/g%3E%3C/svg%3E");-webkit-mask:var(--un-hub-icon) no-repeat;mask:var(--un-hub-icon) no-repeat;color:inherit;background-color:currentColor;width:1.11em;height:1.11em;-webkit-mask-size:100% 100%;mask-size:100% 100%}.i-ph-paint-brush-duotone{--un-hub-icon:url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M224 32c0 32.81-31.64 67.43-58.64 91.05A84.4 84.4 0 0 0 133 90.64c23.57-27 58.19-58.64 91-58.64' opacity='.2'/%3E%3Cpath d='M232 32a8 8 0 0 0-8-8c-44.08 0-89.31 49.71-114.43 82.63A60 60 0 0 0 32 164c0 30.88-19.54 44.73-20.47 45.37A8 8 0 0 0 16 224h76a60 60 0 0 0 57.37-77.57C182.3 121.31 232 76.08 232 32M92 208H34.63C41.38 198.41 48 183.92 48 164a44 44 0 1 1 44 44m32.42-94.45q5.14-6.66 10.09-12.55A76.2 76.2 0 0 1 155 121.49q-5.9 4.94-12.55 10.09a60.5 60.5 0 0 0-18.03-18.03m42.7-2.68a92.6 92.6 0 0 0-22-22c31.78-34.53 55.75-45 69.9-47.91c-2.85 14.16-13.37 38.13-47.9 69.91'/%3E%3C/g%3E%3C/svg%3E");-webkit-mask:var(--un-hub-icon) no-repeat;mask:var(--un-hub-icon) no-repeat;color:inherit;background-color:currentColor;width:1.11em;height:1.11em;-webkit-mask-size:100% 100%;mask-size:100% 100%}.i-ph-push-pin{--un-hub-icon:url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='m235.32 81.37l-60.69-60.68a16 16 0 0 0-22.63 0l-53.63 53.8c-10.66-3.34-35-7.37-60.4 13.14a16 16 0 0 0-1.29 23.78L85 159.71l-42.66 42.63a8 8 0 0 0 11.32 11.32L96.29 171l48.29 48.29A16 16 0 0 0 155.9 224h1.13a15.93 15.93 0 0 0 11.64-6.33c19.64-26.1 17.75-47.32 13.19-60L235.33 104a16 16 0 0 0-.01-22.63M224 92.69l-57.27 57.46a8 8 0 0 0-1.49 9.22c9.46 18.93-1.8 38.59-9.34 48.62L48 100.08c12.08-9.74 23.64-12.31 32.48-12.31A40.1 40.1 0 0 1 96.81 91a8 8 0 0 0 9.25-1.51L163.32 32L224 92.68Z'/%3E%3C/svg%3E");-webkit-mask:var(--un-hub-icon) no-repeat;mask:var(--un-hub-icon) no-repeat;color:inherit;background-color:currentColor;width:1.11em;height:1.11em;-webkit-mask-size:100% 100%;mask-size:100% 100%}.i-ph-push-pin-fill{--un-hub-icon:url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='m235.33 104l-53.47 53.65c4.56 12.67 6.45 33.89-13.19 60A15.93 15.93 0 0 1 157 224h-1.13a16 16 0 0 1-11.32-4.69L96.29 171l-42.63 42.66a8 8 0 0 1-11.32-11.32L85 159.71l-48.3-48.3A16 16 0 0 1 38 87.63c25.42-20.51 49.75-16.48 60.4-13.14L152 20.7a16 16 0 0 1 22.63 0l60.69 60.68a16 16 0 0 1 .01 22.62'/%3E%3C/svg%3E");-webkit-mask:var(--un-hub-icon) no-repeat;mask:var(--un-hub-icon) no-repeat;color:inherit;background-color:currentColor;width:1.11em;height:1.11em;-webkit-mask-size:100% 100%;mask-size:100% 100%}.i-ph-rocket-launch-duotone{--un-hub-icon:url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M184 120v61.65a8 8 0 0 1-2.34 5.65l-34.35 34.35a8 8 0 0 1-13.57-4.53L128 176Zm-48-48H74.35a8 8 0 0 0-5.65 2.34l-34.35 34.35a8 8 0 0 0 4.53 13.57L80 128ZM40 216c37.65 0 50.69-19.69 54.56-28.18l-26.38-26.38C59.69 165.31 40 178.35 40 216' opacity='.2'/%3E%3Cpath d='M223.85 47.12a16 16 0 0 0-15-15c-12.58-.75-44.73.4-71.41 27.07L132.69 64H74.36A15.9 15.9 0 0 0 63 68.68L28.7 103a16 16 0 0 0 9.07 27.16l38.47 5.37l44.21 44.21l5.37 38.49a15.94 15.94 0 0 0 10.78 12.92a16.1 16.1 0 0 0 5.1.83a15.9 15.9 0 0 0 11.3-4.68l34.32-34.3a15.9 15.9 0 0 0 4.68-11.36v-58.33l4.77-4.77c26.68-26.68 27.83-58.83 27.08-71.42M74.36 80h42.33l-39.53 39.52L40 114.34Zm74.41-9.45a76.65 76.65 0 0 1 59.11-22.47a76.46 76.46 0 0 1-22.42 59.16L128 164.68L91.32 128ZM176 181.64L141.67 216l-5.19-37.17L176 139.31Zm-74.16 9.5C97.34 201 82.29 224 40 224a8 8 0 0 1-8-8c0-42.29 23-57.34 32.86-61.85a8 8 0 0 1 6.64 14.56c-6.43 2.93-20.62 12.36-23.12 38.91c26.55-2.5 36-16.69 38.91-23.12a8 8 0 1 1 14.56 6.64Z'/%3E%3C/g%3E%3C/svg%3E");-webkit-mask:var(--un-hub-icon) no-repeat;mask:var(--un-hub-icon) no-repeat;color:inherit;background-color:currentColor;width:1.11em;height:1.11em;-webkit-mask-size:100% 100%;mask-size:100% 100%}.i-ph-shield-check-duotone{--un-hub-icon:url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M216 56v56c0 96-88 120-88 120s-88-24-88-120V56a8 8 0 0 1 8-8h160a8 8 0 0 1 8 8' opacity='.2'/%3E%3Cpath d='M208 40H48a16 16 0 0 0-16 16v56c0 52.72 25.52 84.67 46.93 102.19c23.06 18.86 46 25.26 47 25.53a8 8 0 0 0 4.2 0c1-.27 23.91-6.67 47-25.53C198.48 196.67 224 164.72 224 112V56a16 16 0 0 0-16-16m0 72c0 37.07-13.66 67.16-40.6 89.42a129.3 129.3 0 0 1-39.4 22.2a128.3 128.3 0 0 1-38.92-21.81C61.82 179.51 48 149.3 48 112V56h160ZM82.34 141.66a8 8 0 0 1 11.32-11.32L112 148.69l50.34-50.35a8 8 0 0 1 11.32 11.32l-56 56a8 8 0 0 1-11.32 0Z'/%3E%3C/g%3E%3C/svg%3E");-webkit-mask:var(--un-hub-icon) no-repeat;mask:var(--un-hub-icon) no-repeat;color:inherit;background-color:currentColor;width:1.11em;height:1.11em;-webkit-mask-size:100% 100%;mask-size:100% 100%}.i-ph-sign-out-duotone{--un-hub-icon:url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M224 56v144a16 16 0 0 1-16 16H48V40h160a16 16 0 0 1 16 16' opacity='.2'/%3E%3Cpath d='M120 216a8 8 0 0 1-8 8H48a8 8 0 0 1-8-8V40a8 8 0 0 1 8-8h64a8 8 0 0 1 0 16H56v160h56a8 8 0 0 1 8 8m109.66-93.66l-40-40a8 8 0 0 0-11.32 11.32L204.69 120H112a8 8 0 0 0 0 16h92.69l-26.35 26.34a8 8 0 0 0 11.32 11.32l40-40a8 8 0 0 0 0-11.32'/%3E%3C/g%3E%3C/svg%3E");-webkit-mask:var(--un-hub-icon) no-repeat;mask:var(--un-hub-icon) no-repeat;color:inherit;background-color:currentColor;width:1.11em;height:1.11em;-webkit-mask-size:100% 100%;mask-size:100% 100%}.i-ph-spinner-gap-duotone{--un-hub-icon:url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M224 128a96 96 0 1 1-96-96a96 96 0 0 1 96 96' opacity='.2'/%3E%3Cpath d='M136 32v32a8 8 0 0 1-16 0V32a8 8 0 0 1 16 0m88 88h-32a8 8 0 0 0 0 16h32a8 8 0 0 0 0-16m-45.09 47.6a8 8 0 0 0-11.31 11.31l22.62 22.63a8 8 0 0 0 11.32-11.32ZM128 184a8 8 0 0 0-8 8v32a8 8 0 0 0 16 0v-32a8 8 0 0 0-8-8m-50.91-16.4l-22.63 22.62a8 8 0 0 0 11.32 11.32l22.62-22.63a8 8 0 0 0-11.31-11.31M72 128a8 8 0 0 0-8-8H32a8 8 0 0 0 0 16h32a8 8 0 0 0 8-8m-6.22-73.54a8 8 0 0 0-11.32 11.32L77.09 88.4A8 8 0 0 0 88.4 77.09Z'/%3E%3C/g%3E%3C/svg%3E");-webkit-mask:var(--un-hub-icon) no-repeat;mask:var(--un-hub-icon) no-repeat;color:inherit;background-color:currentColor;width:1.11em;height:1.11em;-webkit-mask-size:100% 100%;mask-size:100% 100%}.i-ph-square-half-bottom-duotone{--un-hub-icon:url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M208 128v72a8 8 0 0 1-8 8H56a8 8 0 0 1-8-8v-72Z' opacity='.2'/%3E%3Cpath d='M200 40H56a16 16 0 0 0-16 16v144a16 16 0 0 0 16 16h144a16 16 0 0 0 16-16V56a16 16 0 0 0-16-16m0 16v64H56V56Zm0 144H56v-64h144z'/%3E%3C/g%3E%3C/svg%3E");-webkit-mask:var(--un-hub-icon) no-repeat;mask:var(--un-hub-icon) no-repeat;color:inherit;background-color:currentColor;width:1.11em;height:1.11em;-webkit-mask-size:100% 100%;mask-size:100% 100%}.i-ph-sun-duotone{--un-hub-icon:url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M184 128a56 56 0 1 1-56-56a56 56 0 0 1 56 56' opacity='.2'/%3E%3Cpath d='M120 40V16a8 8 0 0 1 16 0v24a8 8 0 0 1-16 0m72 88a64 64 0 1 1-64-64a64.07 64.07 0 0 1 64 64m-16 0a48 48 0 1 0-48 48a48.05 48.05 0 0 0 48-48M58.34 69.66a8 8 0 0 0 11.32-11.32l-16-16a8 8 0 0 0-11.32 11.32Zm0 116.68l-16 16a8 8 0 0 0 11.32 11.32l16-16a8 8 0 0 0-11.32-11.32M192 72a8 8 0 0 0 5.66-2.34l16-16a8 8 0 0 0-11.32-11.32l-16 16A8 8 0 0 0 192 72m5.66 114.34a8 8 0 0 0-11.32 11.32l16 16a8 8 0 0 0 11.32-11.32ZM48 128a8 8 0 0 0-8-8H16a8 8 0 0 0 0 16h24a8 8 0 0 0 8-8m80 80a8 8 0 0 0-8 8v24a8 8 0 0 0 16 0v-24a8 8 0 0 0-8-8m112-88h-24a8 8 0 0 0 0 16h24a8 8 0 0 0 0-16'/%3E%3C/g%3E%3C/svg%3E");-webkit-mask:var(--un-hub-icon) no-repeat;mask:var(--un-hub-icon) no-repeat;color:inherit;background-color:currentColor;width:1.11em;height:1.11em;-webkit-mask-size:100% 100%;mask-size:100% 100%}.i-ph-warning-duotone,.i-ph\\:warning-duotone{--un-hub-icon:url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M215.46 216H40.54c-12.62 0-20.54-13.21-14.41-23.91l87.46-151.87c6.3-11 22.52-11 28.82 0l87.46 151.87c6.13 10.7-1.79 23.91-14.41 23.91' opacity='.2'/%3E%3Cpath d='M236.8 188.09L149.35 36.22a24.76 24.76 0 0 0-42.7 0L19.2 188.09a23.51 23.51 0 0 0 0 23.72A24.35 24.35 0 0 0 40.55 224h174.9a24.35 24.35 0 0 0 21.33-12.19a23.51 23.51 0 0 0 .02-23.72m-13.87 15.71a8.5 8.5 0 0 1-7.48 4.2H40.55a8.5 8.5 0 0 1-7.48-4.2a7.59 7.59 0 0 1 0-7.72l87.45-151.87a8.75 8.75 0 0 1 15 0l87.45 151.87a7.59 7.59 0 0 1-.04 7.72M120 144v-40a8 8 0 0 1 16 0v40a8 8 0 0 1-16 0m20 36a12 12 0 1 1-12-12a12 12 0 0 1 12 12'/%3E%3C/g%3E%3C/svg%3E");-webkit-mask:var(--un-hub-icon) no-repeat;mask:var(--un-hub-icon) no-repeat;color:inherit;background-color:currentColor;width:1.11em;height:1.11em;-webkit-mask-size:100% 100%;mask-size:100% 100%}.i-ph-warning-fill{--un-hub-icon:url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='M236.8 188.09L149.35 36.22a24.76 24.76 0 0 0-42.7 0L19.2 188.09a23.51 23.51 0 0 0 0 23.72A24.35 24.35 0 0 0 40.55 224h174.9a24.35 24.35 0 0 0 21.33-12.19a23.51 23.51 0 0 0 .02-23.72M120 104a8 8 0 0 1 16 0v40a8 8 0 0 1-16 0Zm8 88a12 12 0 1 1 12-12a12 12 0 0 1-12 12'/%3E%3C/svg%3E");-webkit-mask:var(--un-hub-icon) no-repeat;mask:var(--un-hub-icon) no-repeat;color:inherit;background-color:currentColor;width:1.11em;height:1.11em;-webkit-mask-size:100% 100%;mask-size:100% 100%}.i-ph-wrench-duotone{--un-hub-icon:url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M224 96a64 64 0 0 1-94.94 56L73 217a24 24 0 0 1-34-34l65-56.06a64 64 0 0 1 80-90.29L144 80l5.66 26.34L176 112l43.35-40A63.8 63.8 0 0 1 224 96' opacity='.2'/%3E%3Cpath d='M226.76 69a8 8 0 0 0-12.84-2.88l-40.3 37.19l-17.23-3.7l-3.7-17.23l37.19-40.3A8 8 0 0 0 187 29.24A72 72 0 0 0 88 96a72.3 72.3 0 0 0 6 28.94L33.79 177c-.15.12-.29.26-.43.39a32 32 0 0 0 45.26 45.26c.13-.13.27-.28.39-.42L131.06 162A72 72 0 0 0 232 96a71.6 71.6 0 0 0-5.24-27M160 152a56.14 56.14 0 0 1-27.07-7a8 8 0 0 0-9.92 1.77l-55.9 64.74a16 16 0 0 1-22.62-22.62L109.18 133a8 8 0 0 0 1.77-9.93a56 56 0 0 1 58.36-82.31l-31.2 33.81a8 8 0 0 0-1.94 7.1l5.66 26.33a8 8 0 0 0 6.14 6.14l26.35 5.66a8 8 0 0 0 7.1-1.94l33.81-31.2A56.06 56.06 0 0 1 160 152'/%3E%3C/g%3E%3C/svg%3E");-webkit-mask:var(--un-hub-icon) no-repeat;mask:var(--un-hub-icon) no-repeat;color:inherit;background-color:currentColor;width:1.11em;height:1.11em;-webkit-mask-size:100% 100%;mask-size:100% 100%}.i-ph-x,.i-ph\\:x{--un-hub-icon:url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='M205.66 194.34a8 8 0 0 1-11.32 11.32L128 139.31l-66.34 66.35a8 8 0 0 1-11.32-11.32L116.69 128L50.34 61.66a8 8 0 0 1 11.32-11.32L128 116.69l66.34-66.35a8 8 0 0 1 11.32 11.32L139.31 128Z'/%3E%3C/svg%3E");-webkit-mask:var(--un-hub-icon) no-repeat;mask:var(--un-hub-icon) no-repeat;color:inherit;background-color:currentColor;width:1.11em;height:1.11em;-webkit-mask-size:100% 100%;mask-size:100% 100%}.i-ph\\:bug-duotone{--un-hub-icon:url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M208 128v16a80 80 0 0 1-160 0v-16Z' opacity='.2'/%3E%3Cpath d='M144 92a12 12 0 1 1 12 12a12 12 0 0 1-12-12m-44-12a12 12 0 1 0 12 12a12 12 0 0 0-12-12m116 64a87.8 87.8 0 0 1-3 23l22.24 9.72A8 8 0 0 1 232 192a7.9 7.9 0 0 1-3.2-.67L207.38 182a88 88 0 0 1-158.76 0l-21.42 9.33a7.9 7.9 0 0 1-3.2.67a8 8 0 0 1-3.2-15.33L43 167a87.8 87.8 0 0 1-3-23v-8H16a8 8 0 0 1 0-16h24v-8a87.8 87.8 0 0 1 3-23l-22.2-9.67a8 8 0 1 1 6.4-14.66L48.62 74a88 88 0 0 1 158.76 0l21.42-9.36a8 8 0 0 1 6.4 14.66L213 89.05a87.8 87.8 0 0 1 3 23v8h24a8 8 0 0 1 0 16h-24ZM56 120h144v-8a72 72 0 0 0-144 0Zm64 95.54V136H56v8a72.08 72.08 0 0 0 64 71.54M200 144v-8h-64v79.54A72.08 72.08 0 0 0 200 144'/%3E%3C/g%3E%3C/svg%3E");-webkit-mask:var(--un-hub-icon) no-repeat;mask:var(--un-hub-icon) no-repeat;color:inherit;background-color:currentColor;width:1.11em;height:1.11em;-webkit-mask-size:100% 100%;mask-size:100% 100%}.i-ph\\:caret-up-fill{--un-hub-icon:url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='M215.39 163.06A8 8 0 0 1 208 168H48a8 8 0 0 1-5.66-13.66l80-80a8 8 0 0 1 11.32 0l80 80a8 8 0 0 1 1.73 8.72'/%3E%3C/svg%3E");-webkit-mask:var(--un-hub-icon) no-repeat;mask:var(--un-hub-icon) no-repeat;color:inherit;background-color:currentColor;width:1.11em;height:1.11em;-webkit-mask-size:100% 100%;mask-size:100% 100%}.i-ph\\:check{--un-hub-icon:url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='m229.66 77.66l-128 128a8 8 0 0 1-11.32 0l-56-56a8 8 0 0 1 11.32-11.32L96 188.69L218.34 66.34a8 8 0 0 1 11.32 11.32'/%3E%3C/svg%3E");-webkit-mask:var(--un-hub-icon) no-repeat;mask:var(--un-hub-icon) no-repeat;color:inherit;background-color:currentColor;width:1.11em;height:1.11em;-webkit-mask-size:100% 100%;mask-size:100% 100%}.i-ph\\:check-circle{--un-hub-icon:url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='M173.66 98.34a8 8 0 0 1 0 11.32l-56 56a8 8 0 0 1-11.32 0l-24-24a8 8 0 0 1 11.32-11.32L112 148.69l50.34-50.35a8 8 0 0 1 11.32 0M232 128A104 104 0 1 1 128 24a104.11 104.11 0 0 1 104 104m-16 0a88 88 0 1 0-88 88a88.1 88.1 0 0 0 88-88'/%3E%3C/svg%3E");-webkit-mask:var(--un-hub-icon) no-repeat;mask:var(--un-hub-icon) no-repeat;color:inherit;background-color:currentColor;width:1.11em;height:1.11em;-webkit-mask-size:100% 100%;mask-size:100% 100%}.i-ph\\:check-circle-duotone{--un-hub-icon:url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M224 128a96 96 0 1 1-96-96a96 96 0 0 1 96 96' opacity='.2'/%3E%3Cpath d='M173.66 98.34a8 8 0 0 1 0 11.32l-56 56a8 8 0 0 1-11.32 0l-24-24a8 8 0 0 1 11.32-11.32L112 148.69l50.34-50.35a8 8 0 0 1 11.32 0M232 128A104 104 0 1 1 128 24a104.11 104.11 0 0 1 104 104m-16 0a88 88 0 1 0-88 88a88.1 88.1 0 0 0 88-88'/%3E%3C/g%3E%3C/svg%3E");-webkit-mask:var(--un-hub-icon) no-repeat;mask:var(--un-hub-icon) no-repeat;color:inherit;background-color:currentColor;width:1.11em;height:1.11em;-webkit-mask-size:100% 100%;mask-size:100% 100%}.i-ph\\:circle-notch-duotone{--un-hub-icon:url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M224 128a96 96 0 1 1-96-96a96 96 0 0 1 96 96' opacity='.2'/%3E%3Cpath d='M232 128a104 104 0 0 1-208 0c0-41 23.81-78.36 60.66-95.27a8 8 0 0 1 6.68 14.54C60.15 61.59 40 93.27 40 128a88 88 0 0 0 176 0c0-34.73-20.15-66.41-51.34-80.73a8 8 0 0 1 6.68-14.54C208.19 49.64 232 87 232 128'/%3E%3C/g%3E%3C/svg%3E");-webkit-mask:var(--un-hub-icon) no-repeat;mask:var(--un-hub-icon) no-repeat;color:inherit;background-color:currentColor;width:1.11em;height:1.11em;-webkit-mask-size:100% 100%;mask-size:100% 100%}.i-ph\\:cloud-warning-duotone{--un-hub-icon:url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M240 127.62a80 80 0 0 1-80 80H72A56 56 0 1 1 85.92 97.36v.1A80 80 0 0 1 240 127.62' opacity='.2'/%3E%3Cpath d='M160 40a88.09 88.09 0 0 0-78.71 48.67A64 64 0 1 0 72 216h88a88 88 0 0 0 0-176m0 160H72a48 48 0 0 1 0-96c1.1 0 2.2 0 3.29.11A88 88 0 0 0 72 128a8 8 0 0 0 16 0a72 72 0 1 1 72 72m-8-72V88a8 8 0 0 1 16 0v40a8 8 0 0 1-16 0m20 36a12 12 0 1 1-12-12a12 12 0 0 1 12 12'/%3E%3C/g%3E%3C/svg%3E");-webkit-mask:var(--un-hub-icon) no-repeat;mask:var(--un-hub-icon) no-repeat;color:inherit;background-color:currentColor;width:1.11em;height:1.11em;-webkit-mask-size:100% 100%;mask-size:100% 100%}.i-ph\\:copy{--un-hub-icon:url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='M216 32H88a8 8 0 0 0-8 8v40H40a8 8 0 0 0-8 8v128a8 8 0 0 0 8 8h128a8 8 0 0 0 8-8v-40h40a8 8 0 0 0 8-8V40a8 8 0 0 0-8-8m-56 176H48V96h112Zm48-48h-32V88a8 8 0 0 0-8-8H96V48h112Z'/%3E%3C/svg%3E");-webkit-mask:var(--un-hub-icon) no-repeat;mask:var(--un-hub-icon) no-repeat;color:inherit;background-color:currentColor;width:1.11em;height:1.11em;-webkit-mask-size:100% 100%;mask-size:100% 100%}.i-ph\\:info-duotone{--un-hub-icon:url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M224 128a96 96 0 1 1-96-96a96 96 0 0 1 96 96' opacity='.2'/%3E%3Cpath d='M144 176a8 8 0 0 1-8 8a16 16 0 0 1-16-16v-40a8 8 0 0 1 0-16a16 16 0 0 1 16 16v40a8 8 0 0 1 8 8m88-48A104 104 0 1 1 128 24a104.11 104.11 0 0 1 104 104m-16 0a88 88 0 1 0-88 88a88.1 88.1 0 0 0 88-88m-92-32a12 12 0 1 0-12-12a12 12 0 0 0 12 12'/%3E%3C/g%3E%3C/svg%3E");-webkit-mask:var(--un-hub-icon) no-repeat;mask:var(--un-hub-icon) no-repeat;color:inherit;background-color:currentColor;width:1.11em;height:1.11em;-webkit-mask-size:100% 100%;mask-size:100% 100%}.i-ph\\:magnifying-glass{--un-hub-icon:url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='m229.66 218.34l-50.07-50.06a88.11 88.11 0 1 0-11.31 11.31l50.06 50.07a8 8 0 0 0 11.32-11.32M40 112a72 72 0 1 1 72 72a72.08 72.08 0 0 1-72-72'/%3E%3C/svg%3E");-webkit-mask:var(--un-hub-icon) no-repeat;mask:var(--un-hub-icon) no-repeat;color:inherit;background-color:currentColor;width:1.11em;height:1.11em;-webkit-mask-size:100% 100%;mask-size:100% 100%}.i-ph\\:minus{--un-hub-icon:url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='M224 128a8 8 0 0 1-8 8H40a8 8 0 0 1 0-16h176a8 8 0 0 1 8 8'/%3E%3C/svg%3E");-webkit-mask:var(--un-hub-icon) no-repeat;mask:var(--un-hub-icon) no-repeat;color:inherit;background-color:currentColor;width:1.11em;height:1.11em;-webkit-mask-size:100% 100%;mask-size:100% 100%}.i-ph\\:plus{--un-hub-icon:url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='M224 128a8 8 0 0 1-8 8h-80v80a8 8 0 0 1-16 0v-80H40a8 8 0 0 1 0-16h80V40a8 8 0 0 1 16 0v80h80a8 8 0 0 1 8 8'/%3E%3C/svg%3E");-webkit-mask:var(--un-hub-icon) no-repeat;mask:var(--un-hub-icon) no-repeat;color:inherit;background-color:currentColor;width:1.11em;height:1.11em;-webkit-mask-size:100% 100%;mask-size:100% 100%}.i-ph\\:puzzle-piece-duotone{--un-hub-icon:url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M204 168a28 28 0 0 0 12-2.69V208a8 8 0 0 1-8 8H64a8 8 0 0 1-8-8v-42.69a28 28 0 1 1 0-50.62V72a8 8 0 0 1 8-8h46.69a28 28 0 1 1 50.61 0H208a8 8 0 0 1 8 8v42.69A28 28 0 1 0 204 168' opacity='.2'/%3E%3Cpath d='M220.27 158.54a8 8 0 0 0-7.7-.46a20 20 0 1 1 0-36.16a8 8 0 0 0 11.43-7.23V72a16 16 0 0 0-16-16h-36.22a35 35 0 0 0 .22-4a36.15 36.15 0 0 0-11.36-26.25a36 36 0 0 0-60.55 23.63a36.6 36.6 0 0 0 .14 6.62H64a16 16 0 0 0-16 16v32.22a35 35 0 0 0-4-.22a36.12 36.12 0 0 0-26.24 11.36a35.7 35.7 0 0 0-9.69 27a36.08 36.08 0 0 0 33.31 33.6a36.6 36.6 0 0 0 6.62-.14V208a16 16 0 0 0 16 16h144a16 16 0 0 0 16-16v-42.69a8 8 0 0 0-3.73-6.77M208 208H64v-42.69a8 8 0 0 0-11.43-7.23a20 20 0 1 1 0-36.16A8 8 0 0 0 64 114.69V72h46.69a8 8 0 0 0 7.23-11.43a20 20 0 1 1 36.16 0A8 8 0 0 0 161.31 72H208v32.23a35.7 35.7 0 0 0-6.62-.14A36 36 0 0 0 204 176a35 35 0 0 0 4-.22Z'/%3E%3C/g%3E%3C/svg%3E");-webkit-mask:var(--un-hub-icon) no-repeat;mask:var(--un-hub-icon) no-repeat;color:inherit;background-color:currentColor;width:1.11em;height:1.11em;-webkit-mask-size:100% 100%;mask-size:100% 100%}.i-ph\\:seal-check{--un-hub-icon:url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='M225.86 102.82c-3.77-3.94-7.67-8-9.14-11.57c-1.36-3.27-1.44-8.69-1.52-13.94c-.15-9.76-.31-20.82-8-28.51s-18.75-7.85-28.51-8c-5.25-.08-10.67-.16-13.94-1.52c-3.56-1.47-7.63-5.37-11.57-9.14C146.28 23.51 138.44 16 128 16s-18.27 7.51-25.18 14.14c-3.94 3.77-8 7.67-11.57 9.14c-3.25 1.36-8.69 1.44-13.94 1.52c-9.76.15-20.82.31-28.51 8s-7.8 18.75-8 28.51c-.08 5.25-.16 10.67-1.52 13.94c-1.47 3.56-5.37 7.63-9.14 11.57C23.51 109.72 16 117.56 16 128s7.51 18.27 14.14 25.18c3.77 3.94 7.67 8 9.14 11.57c1.36 3.27 1.44 8.69 1.52 13.94c.15 9.76.31 20.82 8 28.51s18.75 7.85 28.51 8c5.25.08 10.67.16 13.94 1.52c3.56 1.47 7.63 5.37 11.57 9.14c6.9 6.63 14.74 14.14 25.18 14.14s18.27-7.51 25.18-14.14c3.94-3.77 8-7.67 11.57-9.14c3.27-1.36 8.69-1.44 13.94-1.52c9.76-.15 20.82-.31 28.51-8s7.85-18.75 8-28.51c.08-5.25.16-10.67 1.52-13.94c1.47-3.56 5.37-7.63 9.14-11.57c6.63-6.9 14.14-14.74 14.14-25.18s-7.51-18.27-14.14-25.18m-11.55 39.29c-4.79 5-9.75 10.17-12.38 16.52c-2.52 6.1-2.63 13.07-2.73 19.82c-.1 7-.21 14.33-3.32 17.43s-10.39 3.22-17.43 3.32c-6.75.1-13.72.21-19.82 2.73c-6.35 2.63-11.52 7.59-16.52 12.38S132 224 128 224s-9.15-4.92-14.11-9.69s-10.17-9.75-16.52-12.38c-6.1-2.52-13.07-2.63-19.82-2.73c-7-.1-14.33-.21-17.43-3.32s-3.22-10.39-3.32-17.43c-.1-6.75-.21-13.72-2.73-19.82c-2.63-6.35-7.59-11.52-12.38-16.52S32 132 32 128s4.92-9.15 9.69-14.11s9.75-10.17 12.38-16.52c2.52-6.1 2.63-13.07 2.73-19.82c.1-7 .21-14.33 3.32-17.43s10.39-3.22 17.43-3.32c6.75-.1 13.72-.21 19.82-2.73c6.35-2.63 11.52-7.59 16.52-12.38S124 32 128 32s9.15 4.92 14.11 9.69s10.17 9.75 16.52 12.38c6.1 2.52 13.07 2.63 19.82 2.73c7 .1 14.33.21 17.43 3.32s3.22 10.39 3.32 17.43c.1 6.75.21 13.72 2.73 19.82c2.63 6.35 7.59 11.52 12.38 16.52S224 124 224 128s-4.92 9.15-9.69 14.11m-40.65-43.77a8 8 0 0 1 0 11.32l-56 56a8 8 0 0 1-11.32 0l-24-24a8 8 0 0 1 11.32-11.32L112 148.69l50.34-50.35a8 8 0 0 1 11.32 0'/%3E%3C/svg%3E");-webkit-mask:var(--un-hub-icon) no-repeat;mask:var(--un-hub-icon) no-repeat;color:inherit;background-color:currentColor;width:1.11em;height:1.11em;-webkit-mask-size:100% 100%;mask-size:100% 100%}.i-ph\\:text-bolder{--un-hub-icon:url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='M170.5 115.7A44 44 0 0 0 140 40H64a7.9 7.9 0 0 0-8 8v152a8 8 0 0 0 8 8h88a48 48 0 0 0 18.5-92.3ZM72 56h68a28 28 0 0 1 0 56H72Zm80 136H72v-64h80a32 32 0 0 1 0 64Z'/%3E%3C/svg%3E");-webkit-mask:var(--un-hub-icon) no-repeat;mask:var(--un-hub-icon) no-repeat;color:inherit;background-color:currentColor;width:1.11em;height:1.11em;-webkit-mask-size:100% 100%;mask-size:100% 100%}.i-ph\\:x-circle-duotone{--un-hub-icon:url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M224 128a96 96 0 1 1-96-96a96 96 0 0 1 96 96' opacity='.2'/%3E%3Cpath d='M165.66 101.66L139.31 128l26.35 26.34a8 8 0 0 1-11.32 11.32L128 139.31l-26.34 26.35a8 8 0 0 1-11.32-11.32L116.69 128l-26.35-26.34a8 8 0 0 1 11.32-11.32L128 116.69l26.34-26.35a8 8 0 0 1 11.32 11.32M232 128A104 104 0 1 1 128 24a104.11 104.11 0 0 1 104 104m-16 0a88 88 0 1 0-88 88a88.1 88.1 0 0 0 88-88'/%3E%3C/g%3E%3C/svg%3E");-webkit-mask:var(--un-hub-icon) no-repeat;mask:var(--un-hub-icon) no-repeat;color:inherit;background-color:currentColor;width:1.11em;height:1.11em;-webkit-mask-size:100% 100%;mask-size:100% 100%}.container{width:100%}.btn-action:disabled,.btn-action-sm:disabled{pointer-events:none;opacity:.3!important}.btn-icon:disabled,.btn-icon-compact:disabled{pointer-events:none;opacity:.3}.btn-primary:disabled{pointer-events:none;opacity:.5}.btn-text:disabled{pointer-events:none;opacity:.3!important}.z-command-palette{z-index:2147483646}.z-dock-confirm,.z-dock-toast{z-index:2147483647}.z-drawer-backdrop{z-index:80}.z-drawer-content{z-index:90}.z-dropdown{z-index:40}.z-floating-anchor{z-index:2147483644}.z-floating-tooltip{z-index:2147483645}.z-modal-backdrop{z-index:60}.z-modal-content{z-index:70}.z-nav{z-index:30}.z-toast{z-index:50}.btn-icon{opacity:.65;outline-offset:2px;border-radius:9999px;outline:2px solid #0000;justify-content:center;align-items:center;width:2.25rem;height:2.25rem;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1);display:flex}.btn-icon-compact{opacity:.65;outline-offset:2px;border-radius:.25rem;outline:2px solid #0000;justify-content:center;align-items:center;width:1.5rem;height:1.5rem;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1);display:flex}.btn-action{--un-hub-border-opacity:.13;border-width:1px;border-color:rgb(153 153 153/var(--un-hub-border-opacity));opacity:.75;outline-offset:2px;border-radius:.25rem;outline:2px solid #0000;align-items:center;gap:.5rem;padding:.25rem .5rem;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1);display:flex}.btn-action-sm{--un-hub-border-opacity:.13;border-width:1px;border-color:rgb(153 153 153/var(--un-hub-border-opacity));opacity:.75;outline-offset:2px;border-radius:.25rem;outline:2px solid #0000;align-items:center;gap:.5rem;padding:.25rem .5rem;font-size:.875rem;line-height:1.25rem;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1);display:flex}.btn-primary{--un-hub-bg-opacity:1;background-color:rgb(from var(--colors-primary-500,#578861) r g b / var(--un-hub-bg-opacity));--un-hub-text-opacity:1;color:rgb(255 255 255/var(--un-hub-text-opacity));outline-offset:2px;border-width:1px;border-color:#0000;border-radius:.25rem;outline:2px solid #0000;align-items:center;gap:.5rem;padding:.25rem .5rem;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1);display:flex}.badge{border-radius:.375rem;align-items:center;gap:.25rem;padding:.125rem .375rem;font-size:.75rem;font-weight:500;line-height:1;display:inline-flex}.btn-text{opacity:.75;outline-offset:2px;border-width:1px;border-color:#0000;border-radius:.25rem;outline:2px solid #0000;align-items:center;gap:.5rem;padding:.25rem .5rem;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1);display:inline-flex}.border-mute{--un-hub-border-opacity:.07;border-color:rgb(153 153 153/var(--un-hub-border-opacity))}.dark .data-\\[selected\\]\\:btn-action-active[data-selected],.dark .data-\\[state\\=on\\]\\:btn-action-active[data-state=on]{--un-hub-text-opacity:1;color:rgb(from var(--colors-primary-300,#93c69d) r g b / var(--un-hub-text-opacity));border-color:rgb(from var(--colors-primary-400,#75a67e) r g b / .25)!important}.data-\\[selected\\]\\:btn-action-active[data-selected],.data-\\[state\\=on\\]\\:btn-action-active[data-state=on]{--un-hub-bg-opacity:.19;background-color:rgb(153 153 153/var(--un-hub-bg-opacity));--un-hub-text-opacity:1;color:rgb(from var(--colors-primary-600,#40704b) r g b / var(--un-hub-text-opacity));border-color:rgb(from var(--colors-primary-600,#40704b) r g b / .25)!important;opacity:1!important}.hover\\:border-base:hover{--un-hub-border-opacity:.13;border-color:rgb(153 153 153/var(--un-hub-border-opacity))}.bg-active,.data-\\[state\\=checked\\]\\:bg-active[data-state=checked],.data-\\[state\\=open\\]\\:bg-active[data-state=open]{--un-hub-bg-opacity:.19;background-color:rgb(153 153 153/var(--un-hub-bg-opacity))}.bg-ambient{--un-hub-bg-opacity:.15;background-color:rgb(153 153 153/var(--un-hub-bg-opacity))}.bg-dock-glass,.bg-glass,.bg-toast-glass{--un-hub-backdrop-blur:blur(7px);backdrop-filter:var(--un-hub-backdrop-blur) var(--un-hub-backdrop-brightness) var(--un-hub-backdrop-contrast) var(--un-hub-backdrop-grayscale) var(--un-hub-backdrop-hue-rotate) var(--un-hub-backdrop-invert) var(--un-hub-backdrop-opacity) var(--un-hub-backdrop-saturate) var(--un-hub-backdrop-sepia);background-color:#ffffffa6}.dark .bg-dock-glass,.dark .bg-toast-glass{background-color:#111c}.dark .bg-glass{background-color:#11111180}.bg-glass\\:75{--un-hub-backdrop-blur:blur(7px);backdrop-filter:var(--un-hub-backdrop-blur) var(--un-hub-backdrop-brightness) var(--un-hub-backdrop-contrast) var(--un-hub-backdrop-grayscale) var(--un-hub-backdrop-hue-rotate) var(--un-hub-backdrop-invert) var(--un-hub-backdrop-opacity) var(--un-hub-backdrop-saturate) var(--un-hub-backdrop-sepia);background-color:#fffffffa}.dark .bg-glass\\:75{background-color:#111111bf}.bg-glass\\:80{--un-hub-backdrop-blur:blur(7px);backdrop-filter:var(--un-hub-backdrop-blur) var(--un-hub-backdrop-brightness) var(--un-hub-backdrop-contrast) var(--un-hub-backdrop-grayscale) var(--un-hub-backdrop-hue-rotate) var(--un-hub-backdrop-invert) var(--un-hub-backdrop-opacity) var(--un-hub-backdrop-saturate) var(--un-hub-backdrop-sepia);background-color:#fff}.dark .bg-glass\\:80{background-color:#111c}.bg-hover,.data-\\[highlighted\\]\\:bg-hover[data-highlighted]{--un-hub-bg-opacity:.13;background-color:rgb(153 153 153/var(--un-hub-bg-opacity))}.bg-raised,.data-\\[state\\=active\\]\\:bg-raised[data-state=active],.data-\\[state\\=on\\]\\:bg-raised[data-state=on]{background-color:#ffffffa6}.dark .bg-raised,.dark .data-\\[state\\=active\\]\\:bg-raised[data-state=active],.dark .data-\\[state\\=on\\]\\:bg-raised[data-state=on]{background-color:#ffffff0f}.bg-sunken{background-color:#0000000a}.dark .bg-sunken{background-color:#0003}.hover\\:bg-active:hover{--un-hub-bg-opacity:.19;background-color:rgb(153 153 153/var(--un-hub-bg-opacity))}.hover\\:bg-hover:hover{--un-hub-bg-opacity:.13;background-color:rgb(153 153 153/var(--un-hub-bg-opacity))}.bg-toast-glass:hover{background-color:#ffffffc7}.dark .bg-toast-glass:hover{background-color:#111111e6}.btn-action:hover,.btn-action-sm:hover,.btn-icon:hover,.btn-icon-compact:hover{--un-hub-bg-opacity:.13;background-color:rgb(153 153 153/var(--un-hub-bg-opacity));opacity:1}.btn-primary:hover{--un-hub-bg-opacity:1;background-color:rgb(from var(--colors-primary-600,#40704b) r g b / var(--un-hub-bg-opacity))}.btn-text:hover{--un-hub-bg-opacity:.13;background-color:rgb(153 153 153/var(--un-hub-bg-opacity));opacity:1}.bg-gradient-more{--un-hub-gradient-from-position:0%;--un-hub-gradient-from:rgb(255 255 255/var(--un-hub-from-opacity,1)) var(--un-hub-gradient-from-position);--un-hub-gradient-to-position:100%;--un-hub-gradient-to:#fff0;--un-hub-gradient-stops:var(--un-hub-gradient-from), #fffc var(--un-hub-gradient-via-position), var(--un-hub-gradient-to);--un-hub-gradient-via-position:50%;--un-hub-gradient-shape:to top in oklch;--un-hub-gradient:var(--un-hub-gradient-shape), var(--un-hub-gradient-stops);background-image:linear-gradient(var(--un-hub-gradient))}.dark .bg-gradient-more{--un-hub-gradient-from-position:0%;--un-hub-gradient-from:rgb(17 17 17/var(--un-hub-from-opacity,1)) var(--un-hub-gradient-from-position);--un-hub-gradient-to-position:100%;--un-hub-gradient-to:#1110;--un-hub-gradient-stops:var(--un-hub-gradient-from), #111c var(--un-hub-gradient-via-position), var(--un-hub-gradient-to);--un-hub-gradient-via-position:50%}.text-micro{font-size:10px;line-height:1.4}.color-active,.data-\\[state\\=active\\]\\:color-active[data-state=active],.data-\\[state\\=checked\\]\\:color-active[data-state=checked]{--un-hub-text-opacity:1;color:rgb(from var(--colors-primary-600,#40704b) r g b / var(--un-hub-text-opacity))}.dark .color-active,.dark .data-\\[state\\=active\\]\\:color-active[data-state=active],.dark .data-\\[state\\=checked\\]\\:color-active[data-state=checked]{--un-hub-text-opacity:1;color:rgb(from var(--colors-primary-300,#93c69d) r g b / var(--un-hub-text-opacity))}.color-base,.data-\\[state\\=active\\]\\:color-base[data-state=active],.data-\\[state\\=on\\]\\:color-base[data-state=on]{--un-hub-text-opacity:1;color:rgb(38 38 38/var(--un-hub-text-opacity))}.dark .color-base,.dark .data-\\[state\\=active\\]\\:color-base[data-state=active],.dark .data-\\[state\\=on\\]\\:color-base[data-state=on]{--un-hub-text-opacity:1;color:rgb(229 229 229/var(--un-hub-text-opacity))}.dark .hover\\:color-active:hover{--un-hub-text-opacity:1;color:rgb(from var(--colors-primary-300,#93c69d) r g b / var(--un-hub-text-opacity))}.hover\\:color-active:hover{--un-hub-text-opacity:1;color:rgb(from var(--colors-primary-600,#40704b) r g b / var(--un-hub-text-opacity))}.dark .hover\\:color-base:hover{--un-hub-text-opacity:1;color:rgb(229 229 229/var(--un-hub-text-opacity))}.hover\\:color-base:hover{--un-hub-text-opacity:1;color:rgb(38 38 38/var(--un-hub-text-opacity))}.dark .btn-icon,.dark .btn-icon-compact,.dark .op-fade{opacity:.55}.op-fade{opacity:.65}.dark .op-mute{opacity:.25}.op-mute{opacity:.3}.dark .disabled\\:op-mute:disabled{opacity:.25}.disabled\\:op-mute:disabled{opacity:.3}.dark .placeholder\\:op-mute::placeholder{opacity:.25}.placeholder\\:op-mute::placeholder{opacity:.3}.btn-action:focus-visible,.btn-action-sm:focus-visible,.btn-icon:focus-visible,.btn-icon-compact:focus-visible,.btn-primary:focus-visible,.btn-text:focus-visible{--un-hub-ring-width:2px;--un-hub-ring-offset-shadow:var(--un-hub-ring-inset) 0 0 0 var(--un-hub-ring-offset-width) var(--un-hub-ring-offset-color);--un-hub-ring-shadow:var(--un-hub-ring-inset) 0 0 0 calc(var(--un-hub-ring-width) + var(--un-hub-ring-offset-width)) var(--un-hub-ring-color);box-shadow:var(--un-hub-ring-offset-shadow), var(--un-hub-ring-shadow), var(--un-hub-shadow);--un-hub-ring-color:rgb(from var(--colors-primary-500,#578861) r g b / .4)}.dark .icon-catppuccin{--un-hub-brightness:brightness(1);filter:var(--un-hub-blur) var(--un-hub-brightness) var(--un-hub-contrast) var(--un-hub-drop-shadow) var(--un-hub-grayscale) var(--un-hub-hue-rotate) var(--un-hub-invert) var(--un-hub-saturate) var(--un-hub-sepia);--un-hub-hue-rotate:hue-rotate(0deg);--un-hub-invert:invert(0)}.icon-catppuccin{--un-hub-brightness:brightness(.8);filter:var(--un-hub-blur) var(--un-hub-brightness) var(--un-hub-contrast) var(--un-hub-drop-shadow) var(--un-hub-grayscale) var(--un-hub-hue-rotate) var(--un-hub-invert) var(--un-hub-saturate) var(--un-hub-sepia);--un-hub-hue-rotate:hue-rotate(180deg);--un-hub-invert:invert(1)}@media (width>=640px){.container{max-width:640px}}@media (width>=768px){.container{max-width:768px}}@media (width>=1024px){.container{max-width:1024px}}@media (width>=1280px){.container{max-width:1280px}}@media (width>=1536px){.container{max-width:1536px}}@property --scroll-fade-t{syntax:"";inherits:false;initial-value:0}@property --scroll-fade-b{syntax:"";inherits:false;initial-value:0}@property --scroll-fade-s{syntax:"";inherits:false;initial-value:0}@property --scroll-fade-e{syntax:"";inherits:false;initial-value:0}@property --scroll-fade-mask{syntax:"*";inherits:false}@property --shimmer-angle{syntax:"";inherits:true;initial-value:20deg}@property --shimmer-image{syntax:"*";inherits:false}@property --shimmer-text-fill{syntax:"*";inherits:false}.bg-dots{background-image:radial-gradient(#88888840 1px,#0000 1px);background-size:16px 16px}.bg-grid{background-image:linear-gradient(90deg,#88888826 1px,#0000 1px),linear-gradient(#88888826 1px,#0000 1px);background-size:16px 16px}.shimmer{--_spread:var(--shimmer-spread,calc(3ch + 40px));--_base:currentColor;--_highlight:var(--shimmer-color,oklch(from currentColor l c h / calc(alpha * .2)));background-image:var(--shimmer-image,linear-gradient(calc(90deg + var(--shimmer-angle)),var(--_base) calc(50% - var(--_spread)),color-mix(in oklch,var(--_highlight),var(--_base) 50%) calc(50% - var(--_spread) * .5),var(--_highlight) 50%,color-mix(in oklch,var(--_highlight),var(--_base) 50%) calc(50% + var(--_spread) * .5),var(--_base) calc(50% + var(--_spread))));background-repeat:no-repeat;background-size:calc(200% + var(--_spread) * 2) 100%;-webkit-text-fill-color:var(--shimmer-text-fill,transparent);animation:tw-shimmer var(--shimmer-duration,2s) linear infinite;background-position:0 0;-webkit-background-clip:text;background-clip:text}:where(html.dark) .shimmer{--_highlight:var(--shimmer-color,oklch(from currentColor max(.8,calc(l + .4)) c h / calc(alpha + .4)))}.shimmer:where([dir=rtl],[dir=rtl] *){animation-direction:reverse}@keyframes tw-shimmer{0%{background-position:100% 0}to{background-position:0 0}}@media (prefers-reduced-motion:reduce){.shimmer{-webkit-text-fill-color:currentColor;background-image:none;animation:none}}.sr-only{clip:rect(0,0,0,0);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.pointer-events-auto{pointer-events:auto}.data-\\[disabled\\]\\:pointer-events-none[data-disabled],.pointer-events-none,.disabled\\:pointer-events-none:disabled{pointer-events:none}.visible{visibility:visible}.invisible{visibility:hidden}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.sticky{position:sticky}.static{position:static}.inset-0{inset:0}.inset-x-0{left:0;right:0}.bottom-0{bottom:0}.bottom-4{bottom:1rem}.bottom-4px{bottom:4px}.left-0{left:0}.left-1\\.5{left:.375rem}.left-1\\/2{left:50%}.left-2{left:.5rem}.left-2\\.5{left:.625rem}.left-4{left:1rem}.left-5px{left:5px}.right--1px{right:-1px}.right-0{right:0}.right-0\\.5{right:.125rem}.right-2{right:.5rem}.right-4{right:1rem}.top--32px{top:-32px}.top-0{top:0}.top-0\\.5{top:.125rem}.top-1{top:.25rem}.top-1\\/2{top:50%}.top-4{top:1rem}.top-4px{top:4px}.grid{display:grid}.cols-\\[max-content_1fr\\]{grid-template-columns:max-content 1fr}.m-auto,.ma{margin:auto}.m1{margin:.25rem}.mx--1{margin-left:-.25rem;margin-right:-.25rem}.mx--2{margin-left:-.5rem;margin-right:-.5rem}.mx-1{margin-left:.25rem;margin-right:.25rem}.mx-2{margin-left:.5rem;margin-right:.5rem}.mx-auto{margin-left:auto;margin-right:auto}.my{margin-top:1rem;margin-bottom:1rem}.my-1,.my1{margin-top:.25rem;margin-bottom:.25rem}.my-2{margin-top:.5rem;margin-bottom:.5rem}.my0\\.5{margin-top:.125rem;margin-bottom:.125rem}.-mb-px{margin-bottom:-1px}.mb-1{margin-bottom:.25rem}.mb-1\\.5{margin-bottom:.375rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.ml-0\\.5{margin-left:.125rem}.ml-1{margin-left:.25rem}.ml-1\\.5{margin-left:.375rem}.ml-2{margin-left:.5rem}.ml-6{margin-left:1.5rem}.mr-1{margin-right:.25rem}.ms{margin-inline-start:1rem}.mt--1px{margin-top:-1px}.mt-0\\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-1\\.5{margin-top:.375rem}.mt-2,.mt2{margin-top:.5rem}.mt-4,.mt4{margin-top:1rem}.mt-6,.mt6{margin-top:1.5rem}.mt-auto{margin-top:auto}.box-border{box-sizing:border-box}.inline{display:inline}.block{display:block}.inline-block{display:inline-block}.contents{display:contents}.dark .dark-hidden,.hidden,.light .light-hidden{display:none}.\\[\\&\\>svg\\]\\:h-full>svg,.h-full{height:100%}.\\[\\&\\>svg\\]\\:w-full>svg,.w-full{width:100%}.h-0\\.5{height:.125rem}.h-1{height:.25rem}.h-1\\.5{height:.375rem}.h-1\\/3{height:33.3333%}.h-10{height:2.5rem}.h-14{height:3.5rem}.h-16{height:4rem}.h-2{height:.5rem}.h-20px{height:20px}.h-24{height:6rem}.h-3{height:.75rem}.h-3\\.5{height:.875rem}.h-4{height:1rem}.h-4\\.5{height:1.125rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-7{height:1.75rem}.h-8{height:2rem}.h-9{height:2.25rem}.h-auto\\!{height:auto!important}.h-px{height:1px}.h-screen{height:100vh}.max-h-\\[60vh\\]{max-height:60vh}.max-h-24{max-height:6rem}.max-h-full{max-height:100%}.max-w-\\[90vw\\]{max-width:90vw}.max-w-\\[calc\\(100vw-2rem\\)\\]{max-width:calc(100vw - 2rem)}.max-w-100{max-width:25rem}.max-w-108{max-width:27rem}.max-w-200{max-width:50rem}.max-w-220px{max-width:220px}.max-w-64{max-width:16rem}.max-w-92{max-width:23rem}.max-w-96,.max-w-sm{max-width:24rem}.max-w-full{max-width:100%}.max-w-lg{max-width:32rem}.max-w-prose{max-width:65ch}.max-w-xs{max-width:20rem}.min-h-0{min-height:0}.min-h-16{min-height:4rem}.min-h-40{min-height:10rem}.min-h-5{min-height:1.25rem}.min-w-\\[--reka-combobox-trigger-width\\]{min-width:var(--reka-combobox-trigger-width)}.min-w-\\[--reka-select-trigger-width\\]{min-width:var(--reka-select-trigger-width)}.min-w-0{min-width:0}.min-w-28{min-width:7rem}.min-w-36{min-width:9rem}.min-w-40{min-width:10rem}.min-w-44{min-width:11rem}.min-w-5{min-width:1.25rem}.min-w-6{min-width:1.5rem}.w-\\[--reka-tabs-indicator-size\\]{width:var(--reka-tabs-indicator-size)}.w-\\[40px\\]{width:40px}.w-1\\.5{width:.375rem}.w-10{width:2.5rem}.w-11{width:2.75rem}.w-12{width:3rem}.w-16{width:4rem}.w-2{width:.5rem}.w-2\\/5{width:40%}.w-20px{width:20px}.w-24{width:6rem}.w-2px{width:2px}.w-3{width:.75rem}.w-3\\.5{width:.875rem}.w-3\\/5{width:60%}.w-4{width:1rem}.w-4\\.5{width:1.125rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-64{width:16rem}.w-7{width:1.75rem}.w-72{width:18rem}.w-8{width:2rem}.w-8\\.5{width:2.125rem}.w-80{width:20rem}.w-96{width:24rem}.w-auto\\!{width:auto!important}.w-fit{width:fit-content}.w-lg{width:32rem}.w-max{width:max-content}.w-px{width:1px}.w-screen{width:100vw}.flex{display:flex}.inline-flex{display:inline-flex}.flex-1{flex:1}.flex-auto{flex:auto}.flex-none{flex:none}.shrink-0{flex-shrink:0}.shrink-to-fit{flex-shrink:1}.flex-row{flex-direction:row}.flex-row-reverse{flex-direction:row-reverse}.flex-col{flex-direction:column}.flex-col-reverse{flex-direction:column-reverse}.flex-wrap{flex-wrap:wrap}.table{display:table}.border-collapse{border-collapse:collapse}.-translate-x-1\\/2,.translate-x--1\\/2{--un-hub-translate-x:-50%;transform:translateX(var(--un-hub-translate-x)) translateY(var(--un-hub-translate-y)) translateZ(var(--un-hub-translate-z)) rotate(var(--un-hub-rotate)) rotateX(var(--un-hub-rotate-x)) rotateY(var(--un-hub-rotate-y)) rotateZ(var(--un-hub-rotate-z)) skewX(var(--un-hub-skew-x)) skewY(var(--un-hub-skew-y)) scaleX(var(--un-hub-scale-x)) scaleY(var(--un-hub-scale-y)) scaleZ(var(--un-hub-scale-z))}.-translate-y-2{--un-hub-translate-y:-.5rem;transform:translateX(var(--un-hub-translate-x)) translateY(var(--un-hub-translate-y)) translateZ(var(--un-hub-translate-z)) rotate(var(--un-hub-rotate)) rotateX(var(--un-hub-rotate-x)) rotateY(var(--un-hub-rotate-y)) rotateZ(var(--un-hub-rotate-z)) skewX(var(--un-hub-skew-x)) skewY(var(--un-hub-skew-y)) scaleX(var(--un-hub-scale-x)) scaleY(var(--un-hub-scale-y)) scaleZ(var(--un-hub-scale-z))}.data-\\[state\\=checked\\]\\:translate-x-4[data-state=checked],.translate-x-4{--un-hub-translate-x:1rem;transform:translateX(var(--un-hub-translate-x)) translateY(var(--un-hub-translate-y)) translateZ(var(--un-hub-translate-z)) rotate(var(--un-hub-rotate)) rotateX(var(--un-hub-rotate-x)) rotateY(var(--un-hub-rotate-y)) rotateZ(var(--un-hub-rotate-z)) skewX(var(--un-hub-skew-x)) skewY(var(--un-hub-skew-y)) scaleX(var(--un-hub-scale-x)) scaleY(var(--un-hub-scale-y)) scaleZ(var(--un-hub-scale-z))}.translate-x-\\[--reka-tabs-indicator-position\\]{--un-hub-translate-x:var(--reka-tabs-indicator-position);transform:translateX(var(--un-hub-translate-x)) translateY(var(--un-hub-translate-y)) translateZ(var(--un-hub-translate-z)) rotate(var(--un-hub-rotate)) rotateX(var(--un-hub-rotate-x)) rotateY(var(--un-hub-rotate-y)) rotateZ(var(--un-hub-rotate-z)) skewX(var(--un-hub-skew-x)) skewY(var(--un-hub-skew-y)) scaleX(var(--un-hub-scale-x)) scaleY(var(--un-hub-scale-y)) scaleZ(var(--un-hub-scale-z))}.translate-x-0\\.5{--un-hub-translate-x:.125rem;transform:translateX(var(--un-hub-translate-x)) translateY(var(--un-hub-translate-y)) translateZ(var(--un-hub-translate-z)) rotate(var(--un-hub-rotate)) rotateX(var(--un-hub-rotate-x)) rotateY(var(--un-hub-rotate-y)) rotateZ(var(--un-hub-rotate-z)) skewX(var(--un-hub-skew-x)) skewY(var(--un-hub-skew-y)) scaleX(var(--un-hub-scale-x)) scaleY(var(--un-hub-scale-y)) scaleZ(var(--un-hub-scale-z))}.translate-x-1{--un-hub-translate-x:.25rem;transform:translateX(var(--un-hub-translate-x)) translateY(var(--un-hub-translate-y)) translateZ(var(--un-hub-translate-z)) rotate(var(--un-hub-rotate)) rotateX(var(--un-hub-rotate-x)) rotateY(var(--un-hub-rotate-y)) rotateZ(var(--un-hub-rotate-z)) skewX(var(--un-hub-skew-x)) skewY(var(--un-hub-skew-y)) scaleX(var(--un-hub-scale-x)) scaleY(var(--un-hub-scale-y)) scaleZ(var(--un-hub-scale-z))}.translate-x-5{--un-hub-translate-x:1.25rem;transform:translateX(var(--un-hub-translate-x)) translateY(var(--un-hub-translate-y)) translateZ(var(--un-hub-translate-z)) rotate(var(--un-hub-rotate)) rotateX(var(--un-hub-rotate-x)) rotateY(var(--un-hub-rotate-y)) rotateZ(var(--un-hub-rotate-z)) skewX(var(--un-hub-skew-x)) skewY(var(--un-hub-skew-y)) scaleX(var(--un-hub-scale-x)) scaleY(var(--un-hub-scale-y)) scaleZ(var(--un-hub-scale-z))}.translate-y--1\\/2{--un-hub-translate-y:-50%;transform:translateX(var(--un-hub-translate-x)) translateY(var(--un-hub-translate-y)) translateZ(var(--un-hub-translate-z)) rotate(var(--un-hub-rotate)) rotateX(var(--un-hub-rotate-x)) rotateY(var(--un-hub-rotate-y)) rotateZ(var(--un-hub-rotate-z)) skewX(var(--un-hub-skew-x)) skewY(var(--un-hub-skew-y)) scaleX(var(--un-hub-scale-x)) scaleY(var(--un-hub-scale-y)) scaleZ(var(--un-hub-scale-z))}.translate-y-0{--un-hub-translate-y:0;transform:translateX(var(--un-hub-translate-x)) translateY(var(--un-hub-translate-y)) translateZ(var(--un-hub-translate-z)) rotate(var(--un-hub-rotate)) rotateX(var(--un-hub-rotate-x)) rotateY(var(--un-hub-rotate-y)) rotateZ(var(--un-hub-rotate-z)) skewX(var(--un-hub-skew-x)) skewY(var(--un-hub-skew-y)) scaleX(var(--un-hub-scale-x)) scaleY(var(--un-hub-scale-y)) scaleZ(var(--un-hub-scale-z))}.translate-y-2{--un-hub-translate-y:.5rem;transform:translateX(var(--un-hub-translate-x)) translateY(var(--un-hub-translate-y)) translateZ(var(--un-hub-translate-z)) rotate(var(--un-hub-rotate)) rotateX(var(--un-hub-rotate-x)) rotateY(var(--un-hub-rotate-y)) rotateZ(var(--un-hub-rotate-z)) skewX(var(--un-hub-skew-x)) skewY(var(--un-hub-skew-y)) scaleX(var(--un-hub-scale-x)) scaleY(var(--un-hub-scale-y)) scaleZ(var(--un-hub-scale-z))}.-rotate-90{--un-hub-rotate-x:0;--un-hub-rotate-y:0;--un-hub-rotate-z:0;--un-hub-rotate:-90deg;transform:translateX(var(--un-hub-translate-x)) translateY(var(--un-hub-translate-y)) translateZ(var(--un-hub-translate-z)) rotate(var(--un-hub-rotate)) rotateX(var(--un-hub-rotate-x)) rotateY(var(--un-hub-rotate-y)) rotateZ(var(--un-hub-rotate-z)) skewX(var(--un-hub-skew-x)) skewY(var(--un-hub-skew-y)) scaleX(var(--un-hub-scale-x)) scaleY(var(--un-hub-scale-y)) scaleZ(var(--un-hub-scale-z))}.group[data-state=open] .group-data-\\[state\\=open\\]\\:rotate-90,.rotate-90{--un-hub-rotate-x:0;--un-hub-rotate-y:0;--un-hub-rotate-z:0;--un-hub-rotate:90deg;transform:translateX(var(--un-hub-translate-x)) translateY(var(--un-hub-translate-y)) translateZ(var(--un-hub-translate-z)) rotate(var(--un-hub-rotate)) rotateX(var(--un-hub-rotate-x)) rotateY(var(--un-hub-rotate-y)) rotateZ(var(--un-hub-rotate-z)) skewX(var(--un-hub-skew-x)) skewY(var(--un-hub-skew-y)) scaleX(var(--un-hub-scale-x)) scaleY(var(--un-hub-scale-y)) scaleZ(var(--un-hub-scale-z))}.rotate--45{--un-hub-rotate-x:0;--un-hub-rotate-y:0;--un-hub-rotate-z:0;--un-hub-rotate:-45deg;transform:translateX(var(--un-hub-translate-x)) translateY(var(--un-hub-translate-y)) translateZ(var(--un-hub-translate-z)) rotate(var(--un-hub-rotate)) rotateX(var(--un-hub-rotate-x)) rotateY(var(--un-hub-rotate-y)) rotateZ(var(--un-hub-rotate-z)) skewX(var(--un-hub-skew-x)) skewY(var(--un-hub-skew-y)) scaleX(var(--un-hub-scale-x)) scaleY(var(--un-hub-scale-y)) scaleZ(var(--un-hub-scale-z))}.rotate-0{--un-hub-rotate-x:0;--un-hub-rotate-y:0;--un-hub-rotate-z:0;--un-hub-rotate:0deg;transform:translateX(var(--un-hub-translate-x)) translateY(var(--un-hub-translate-y)) translateZ(var(--un-hub-translate-z)) rotate(var(--un-hub-rotate)) rotateX(var(--un-hub-rotate-x)) rotateY(var(--un-hub-rotate-y)) rotateZ(var(--un-hub-rotate-z)) skewX(var(--un-hub-skew-x)) skewY(var(--un-hub-skew-y)) scaleX(var(--un-hub-scale-x)) scaleY(var(--un-hub-scale-y)) scaleZ(var(--un-hub-scale-z))}.rotate-180{--un-hub-rotate-x:0;--un-hub-rotate-y:0;--un-hub-rotate-z:0;--un-hub-rotate:180deg;transform:translateX(var(--un-hub-translate-x)) translateY(var(--un-hub-translate-y)) translateZ(var(--un-hub-translate-z)) rotate(var(--un-hub-rotate)) rotateX(var(--un-hub-rotate-x)) rotateY(var(--un-hub-rotate-y)) rotateZ(var(--un-hub-rotate-z)) skewX(var(--un-hub-skew-x)) skewY(var(--un-hub-skew-y)) scaleX(var(--un-hub-scale-x)) scaleY(var(--un-hub-scale-y)) scaleZ(var(--un-hub-scale-z))}.rotate-270{--un-hub-rotate-x:0;--un-hub-rotate-y:0;--un-hub-rotate-z:0;--un-hub-rotate:270deg;transform:translateX(var(--un-hub-translate-x)) translateY(var(--un-hub-translate-y)) translateZ(var(--un-hub-translate-z)) rotate(var(--un-hub-rotate)) rotateX(var(--un-hub-rotate-x)) rotateY(var(--un-hub-rotate-y)) rotateZ(var(--un-hub-rotate-z)) skewX(var(--un-hub-skew-x)) skewY(var(--un-hub-skew-y)) scaleX(var(--un-hub-scale-x)) scaleY(var(--un-hub-scale-y)) scaleZ(var(--un-hub-scale-z))}.scale-100{--un-hub-scale-x:1;--un-hub-scale-y:1;transform:translateX(var(--un-hub-translate-x)) translateY(var(--un-hub-translate-y)) translateZ(var(--un-hub-translate-z)) rotate(var(--un-hub-rotate)) rotateX(var(--un-hub-rotate-x)) rotateY(var(--un-hub-rotate-y)) rotateZ(var(--un-hub-rotate-z)) skewX(var(--un-hub-skew-x)) skewY(var(--un-hub-skew-y)) scaleX(var(--un-hub-scale-x)) scaleY(var(--un-hub-scale-y)) scaleZ(var(--un-hub-scale-z))}.scale-120{--un-hub-scale-x:1.2;--un-hub-scale-y:1.2;transform:translateX(var(--un-hub-translate-x)) translateY(var(--un-hub-translate-y)) translateZ(var(--un-hub-translate-z)) rotate(var(--un-hub-rotate)) rotateX(var(--un-hub-rotate-x)) rotateY(var(--un-hub-rotate-y)) rotateZ(var(--un-hub-rotate-z)) skewX(var(--un-hub-skew-x)) skewY(var(--un-hub-skew-y)) scaleX(var(--un-hub-scale-x)) scaleY(var(--un-hub-scale-y)) scaleZ(var(--un-hub-scale-z))}.scale-98{--un-hub-scale-x:.98;--un-hub-scale-y:.98;transform:translateX(var(--un-hub-translate-x)) translateY(var(--un-hub-translate-y)) translateZ(var(--un-hub-translate-z)) rotate(var(--un-hub-rotate)) rotateX(var(--un-hub-rotate-x)) rotateY(var(--un-hub-rotate-y)) rotateZ(var(--un-hub-rotate-z)) skewX(var(--un-hub-skew-x)) skewY(var(--un-hub-skew-y)) scaleX(var(--un-hub-scale-x)) scaleY(var(--un-hub-scale-y)) scaleZ(var(--un-hub-scale-z))}.hover\\:scale-110:hover{--un-hub-scale-x:1.1;--un-hub-scale-y:1.1;transform:translateX(var(--un-hub-translate-x)) translateY(var(--un-hub-translate-y)) translateZ(var(--un-hub-translate-z)) rotate(var(--un-hub-rotate)) rotateX(var(--un-hub-rotate-x)) rotateY(var(--un-hub-rotate-y)) rotateZ(var(--un-hub-rotate-z)) skewX(var(--un-hub-skew-x)) skewY(var(--un-hub-skew-y)) scaleX(var(--un-hub-scale-x)) scaleY(var(--un-hub-scale-y)) scaleZ(var(--un-hub-scale-z))}.transform{transform:translateX(var(--un-hub-translate-x)) translateY(var(--un-hub-translate-y)) translateZ(var(--un-hub-translate-z)) rotate(var(--un-hub-rotate)) rotateX(var(--un-hub-rotate-x)) rotateY(var(--un-hub-rotate-y)) rotateZ(var(--un-hub-rotate-z)) skewX(var(--un-hub-skew-x)) skewY(var(--un-hub-skew-y)) scaleX(var(--un-hub-scale-x)) scaleY(var(--un-hub-scale-y)) scaleZ(var(--un-hub-scale-z))}@keyframes pulse{0%,to{opacity:1}50%{opacity:.5}}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.animate-pulse{animation:2s cubic-bezier(.4,0,.6,1) infinite pulse}.animate-spin{animation:1s linear infinite spin}.cursor-default{cursor:default}.cursor-pointer{cursor:pointer}.cursor-not-allowed{cursor:not-allowed}.cursor-grab{cursor:grab}.touch-none{touch-action:none}.select-none{-webkit-user-select:none;user-select:none}.resize{resize:both}.resize-none{resize:none}.items-start{align-items:flex-start}.items-center{align-items:center}.self-stretch{align-self:stretch}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-0{gap:0}.gap-0\\.5{gap:.125rem}.gap-1{gap:.25rem}.gap-1\\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.gap-x-2{column-gap:.5rem}.gap-y-0\\.5{row-gap:.125rem}.space-y-0\\.5>:not([hidden])~:not([hidden]){--un-hub-space-y-reverse:0;margin-top:calc(.125rem * calc(1 - var(--un-hub-space-y-reverse)));margin-bottom:calc(.125rem * var(--un-hub-space-y-reverse))}.divide-y>:not([hidden])~:not([hidden]){--un-hub-divide-y-reverse:0;border-top-width:calc(1px * calc(1 - var(--un-hub-divide-y-reverse)));border-bottom-width:calc(1px * var(--un-hub-divide-y-reverse))}.divide-\\#8882>:not([hidden])~:not([hidden]){--un-hub-divide-opacity:.13;border-color:rgb(136 136 136/var(--un-hub-divide-opacity))}.of-hidden,.overflow-hidden{overflow:hidden}.overflow-auto{overflow:auto}.overflow-clip{overflow:clip}.of-x-hidden{overflow-x:hidden}.of-y-auto{overflow-y:auto}.of-y-hidden{overflow-y:hidden}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.whitespace-pre-wrap{white-space:pre-wrap}.ws-nowrap{white-space:nowrap}.break-words{overflow-wrap:break-word}.b,.border{border-width:1px}.border-2{border-width:2px}.border-b{border-bottom-width:1px}.border-b-1\\.5{border-bottom-width:1.5px}.border-b-2{border-bottom-width:2px}.border-l{border-left-width:1px}.border-r{border-right-width:1px}.border-r-1\\.5{border-right-width:1.5px}.border-t{border-top-width:1px}.border-t-0{border-top-width:0}.first\\:border-l-0:first-child{border-left-width:0}.last\\:border-b-0:last-child{border-bottom-width:0}.border-amber-500\\/20{border-color:#f59e0b33}.border-amber\\/20{border-color:#fbbf2433}.border-blue-500\\/20{border-color:#3b82f633}.border-current{border-color:currentColor}.border-error\\!{--un-hub-border-opacity:1!important;border-color:rgb(246 61 104/var(--un-hub-border-opacity))!important}.border-green-500\\/20{border-color:#22c55e33}.border-primary-400\\/60{border-color:rgb(from var(--colors-primary-400,#75a67e) r g b / .6)}.border-primary\\/30{border-color:rgb(from var(--colors-primary-DEFAULT,#3a6a45) r g b / .3)}.border-red-500\\/20{border-color:#ef444433}.border-red-500\\/60{border-color:#ef444499}.border-red-500\\/70{border-color:#ef4444b3}.border-transparent{border-color:#0000}.dark .dark\\:border-primary-400\\/50{border-color:rgb(from var(--colors-primary-400,#75a67e) r g b / .5)}.dark .dark\\:data-\\[state\\=active\\]\\:border-primary-400[data-state=active]{--un-hub-border-opacity:1;border-color:rgb(from var(--colors-primary-400,#75a67e) r g b / var(--un-hub-border-opacity))}.data-\\[state\\=active\\]\\:border-primary-500[data-state=active],.data-\\[state\\=checked\\]\\:border-primary-500[data-state=checked]{--un-hub-border-opacity:1;border-color:rgb(from var(--colors-primary-500,#578861) r g b / var(--un-hub-border-opacity))}.focus-within\\:border-gray\\/15:focus-within{border-color:#9ca3af26}.hover\\:border-gray\\/10:hover{border-color:#9ca3af1a}.focus\\:border-primary-500:focus{--un-hub-border-opacity:1;border-color:rgb(from var(--colors-primary-500,#578861) r g b / var(--un-hub-border-opacity))}.focus\\:border-primary\\/40:focus{border-color:rgb(from var(--colors-primary-DEFAULT,#3a6a45) r g b / .4)}.border-t-transparent{border-top-color:#0000}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-xl{border-radius:.75rem}.rounded-r{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.bg-\\[\\#8881\\],.bg-\\#8881,.data-\\[state\\=visible\\]\\:bg-\\#8881[data-state=visible]{--un-hub-bg-opacity:.07;background-color:rgb(136 136 136/var(--un-hub-bg-opacity))}.bg-\\[\\#ddd\\]\\/40{background-color:#ddd6}.bg-\\#8882{--un-hub-bg-opacity:.13;background-color:rgb(136 136 136/var(--un-hub-bg-opacity))}.bg-\\#8885{--un-hub-bg-opacity:.33;background-color:rgb(136 136 136/var(--un-hub-bg-opacity))}.bg-amber{--un-hub-bg-opacity:1;background-color:rgb(251 191 36/var(--un-hub-bg-opacity))}.bg-amber-400\\/10,.bg-amber\\/10{background-color:#fbbf241a}.bg-black\\/30,.dark .dark\\:bg-black\\/30{background-color:#0000004d}.bg-blue{--un-hub-bg-opacity:1;background-color:rgb(96 165 250/var(--un-hub-bg-opacity))}.bg-blue-400\\/10,.bg-blue\\/10{background-color:#60a5fa1a}.bg-current{background-color:currentColor}.bg-error\\!{--un-hub-bg-opacity:1!important;background-color:rgb(246 61 104/var(--un-hub-bg-opacity))!important}.bg-gray{--un-hub-bg-opacity:1;background-color:rgb(156 163 175/var(--un-hub-bg-opacity))}.bg-gray-500{--un-hub-bg-opacity:1;background-color:rgb(107 114 128/var(--un-hub-bg-opacity))}.bg-gray-6{--un-hub-bg-opacity:1;background-color:rgb(75 85 99/var(--un-hub-bg-opacity))}.bg-gray\\/10{background-color:#9ca3af1a}.bg-gray\\/20{background-color:#9ca3af33}.bg-gray\\/3{background-color:#9ca3af08}.bg-gray\\/30{background-color:#9ca3af4d}.bg-gray\\/5{background-color:#9ca3af0d}.bg-green{--un-hub-bg-opacity:1;background-color:rgb(74 222 128/var(--un-hub-bg-opacity))}.bg-green-400\\/10{background-color:#4ade801a}.bg-orange\\/10{background-color:#fb923c1a}.bg-primary{--un-hub-bg-opacity:1;background-color:rgb(from var(--colors-primary-DEFAULT,#3a6a45) r g b / var(--un-hub-bg-opacity))}.bg-primary-500,.data-\\[state\\=checked\\]\\:bg-primary-500[data-state=checked]{--un-hub-bg-opacity:1;background-color:rgb(from var(--colors-primary-500,#578861) r g b / var(--un-hub-bg-opacity))}.bg-primary-500\\/20{background-color:rgb(from var(--colors-primary-500,#578861) r g b / .2)}.bg-primary\\/10{background-color:rgb(from var(--colors-primary-DEFAULT,#3a6a45) r g b / .1)}.bg-primary\\/15{background-color:rgb(from var(--colors-primary-DEFAULT,#3a6a45) r g b / .15)}.bg-primary\\/20{background-color:rgb(from var(--colors-primary-DEFAULT,#3a6a45) r g b / .2)}.bg-red{--un-hub-bg-opacity:1;background-color:rgb(248 113 113/var(--un-hub-bg-opacity))}.bg-red-400\\/10,.bg-red\\/10{background-color:#f871711a}.bg-red-500\\!{--un-hub-bg-opacity:1!important;background-color:rgb(239 68 68/var(--un-hub-bg-opacity))!important}.bg-transparent{background-color:#0000}.bg-white{--un-hub-bg-opacity:1;background-color:rgb(255 255 255/var(--un-hub-bg-opacity))}.bg-white\\/50{background-color:#ffffff80}.dark .dark\\:bg-black\\/40{background-color:#0006}.dark .dark\\:bg-black\\/45{background-color:#00000073}.data-\\[highlighted\\]\\:bg-red-500\\/10[data-highlighted]{background-color:#ef44441a}.hover\\:bg-\\[\\#8881\\]:hover{--un-hub-bg-opacity:.07;background-color:rgb(136 136 136/var(--un-hub-bg-opacity))}.hover\\:bg-\\[\\#8882\\]:hover{--un-hub-bg-opacity:.13;background-color:rgb(136 136 136/var(--un-hub-bg-opacity))}.hover\\:bg-\\#8888:hover{--un-hub-bg-opacity:.53;background-color:rgb(136 136 136/var(--un-hub-bg-opacity))}.hover\\:bg-gray\\/10:hover{background-color:#9ca3af1a}.hover\\:bg-gray\\/15:hover{background-color:#9ca3af26}.hover\\:bg-gray\\/20:hover{background-color:#9ca3af33}.hover\\:bg-gray\\/5:hover{background-color:#9ca3af0d}.hover\\:bg-orange\\/20:hover{background-color:#fb923c33}.hover\\:bg-primary\\/25:hover{background-color:rgb(from var(--colors-primary-DEFAULT,#3a6a45) r g b / .25)}.hover\\:bg-red-600\\!:hover{--un-hub-bg-opacity:1!important;background-color:rgb(220 38 38/var(--un-hub-bg-opacity))!important}.hover\\:bg-red\\/20:hover{background-color:#f8717133}.stroke-current{stroke:currentColor}.object-contain{object-fit:contain}.p-\\[0\\.25em\\]{padding:.25em}.p-\\[0\\.5em\\]{padding:.5em}.p-0\\.5{padding:.125rem}.p-1,.p1{padding:.25rem}.p-1\\.5,.p1\\.5{padding:.375rem}.p-2,.p2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p6{padding:1.5rem}.p8{padding:2rem}.px,.px-4,.px4{padding-left:1rem;padding-right:1rem}.px-0\\.5{padding-left:.125rem;padding-right:.125rem}.px-1,.px1{padding-left:.25rem;padding-right:.25rem}.px-1\\.5{padding-left:.375rem;padding-right:.375rem}.px-2,.px2{padding-left:.5rem;padding-right:.5rem}.px-2\\.5{padding-left:.625rem;padding-right:.625rem}.px-3,.px3{padding-left:.75rem;padding-right:.75rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-0{padding-top:0;padding-bottom:0}.py-0\\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1,.py1{padding-top:.25rem;padding-bottom:.25rem}.py-1\\.5,.py1\\.5{padding-top:.375rem;padding-bottom:.375rem}.py-10{padding-top:2.5rem;padding-bottom:2.5rem}.py-12{padding-top:3rem;padding-bottom:3rem}.py-2,.py2{padding-top:.5rem;padding-bottom:.5rem}.py-2\\!{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-2\\.5{padding-top:.625rem;padding-bottom:.625rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.py-8{padding-top:2rem;padding-bottom:2rem}.pb-1{padding-bottom:.25rem}.pb2\\.5{padding-bottom:.625rem}.pl-4{padding-left:1rem}.pl-5{padding-left:1.25rem}.pl-7{padding-left:1.75rem}.pl2\\.5{padding-left:.625rem}.pr-2{padding-right:.5rem}.pr1{padding-right:.25rem}.pt-\\[20vh\\]{padding-top:20vh}.pt-1\\.5{padding-top:.375rem}.pt-6{padding-top:1.5rem}.pt2{padding-top:.5rem}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.text-balance{text-wrap:balance}.text-\\[10px\\]{font-size:10px}.text-\\[15px\\]{font-size:15px}.text-0\\.6em{font-size:.6em}.text-2\\.75{font-size:.6875rem}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.dark .dark\\:text-amber-300{--un-hub-text-opacity:1;color:rgb(252 211 77/var(--un-hub-text-opacity))}.dark .dark\\:text-amber-400,.text-amber{--un-hub-text-opacity:1;color:rgb(251 191 36/var(--un-hub-text-opacity))}.dark .dark\\:text-blue-300{--un-hub-text-opacity:1;color:rgb(147 197 253/var(--un-hub-text-opacity))}.dark .dark\\:text-green-300{--un-hub-text-opacity:1;color:rgb(134 239 172/var(--un-hub-text-opacity))}.dark .dark\\:text-green-400,.text-green{--un-hub-text-opacity:1;color:rgb(74 222 128/var(--un-hub-text-opacity))}.dark .dark\\:text-primary-300{--un-hub-text-opacity:1;color:rgb(from var(--colors-primary-300,#93c69d) r g b / var(--un-hub-text-opacity))}.dark .dark\\:text-red-300{--un-hub-text-opacity:1;color:rgb(252 165 165/var(--un-hub-text-opacity))}.dark .dark\\:text-red-400,.text-red{--un-hub-text-opacity:1;color:rgb(248 113 113/var(--un-hub-text-opacity))}.text-amber-600{--un-hub-text-opacity:1;color:rgb(217 119 6/var(--un-hub-text-opacity))}.text-amber-700{--un-hub-text-opacity:1;color:rgb(180 83 9/var(--un-hub-text-opacity))}.text-blue{--un-hub-text-opacity:1;color:rgb(96 165 250/var(--un-hub-text-opacity))}.text-blue-700{--un-hub-text-opacity:1;color:rgb(29 78 216/var(--un-hub-text-opacity))}.text-gray{--un-hub-text-opacity:1;color:rgb(156 163 175/var(--un-hub-text-opacity))}.text-green-600{--un-hub-text-opacity:1;color:rgb(22 163 74/var(--un-hub-text-opacity))}.text-green-700{--un-hub-text-opacity:1;color:rgb(21 128 61/var(--un-hub-text-opacity))}.text-orange{--un-hub-text-opacity:1;color:rgb(251 146 60/var(--un-hub-text-opacity))}.text-primary,.text-primary\\/bg-active{--un-hub-text-opacity:1;color:rgb(from var(--colors-primary-DEFAULT,#3a6a45) r g b / var(--un-hub-text-opacity))}.text-primary-600{--un-hub-text-opacity:1;color:rgb(from var(--colors-primary-600,#40704b) r g b / var(--un-hub-text-opacity))}.text-red-500{--un-hub-text-opacity:1;color:rgb(239 68 68/var(--un-hub-text-opacity))}.text-red-600{--un-hub-text-opacity:1;color:rgb(220 38 38/var(--un-hub-text-opacity))}.text-red-700{--un-hub-text-opacity:1;color:rgb(185 28 28/var(--un-hub-text-opacity))}.text-white{--un-hub-text-opacity:1;color:rgb(255 255 255/var(--un-hub-text-opacity))}.font-bold{font-weight:700}.font-medium{font-weight:500}.leading-4{line-height:1rem}.leading-5{line-height:1.25rem}.leading-none{line-height:1}.leading-relaxed{line-height:1.625}.tracking-tight{letter-spacing:-.025em}.tracking-wide{letter-spacing:.025em}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.font-sans{font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji}.uppercase{text-transform:uppercase}.capitalize{text-transform:capitalize}.italic{font-style:italic}.tabular-nums{--un-hub-numeric-spacing:tabular-nums;font-variant-numeric:var(--un-hub-ordinal) var(--un-hub-slashed-zero) var(--un-hub-numeric-figure) var(--un-hub-numeric-spacing) var(--un-hub-numeric-fraction)}.line-through{text-decoration-line:line-through}.underline,.hover\\:underline:hover{text-decoration-line:underline}.tab{-moz-tab-size:4;-o-tab-size:4;tab-size:4}.caret-primary-500{--un-hub-caret-opacity:1;caret-color:rgb(from var(--colors-primary-500,#578861) r g b / var(--un-hub-caret-opacity))}.data-\\[disabled\\]\\:op50[data-disabled],.op50,.group:hover .group-hover\\:op50{opacity:.5}.data-\\[state\\=checked\\]\\:op100[data-state=checked],.op100,.opacity-100,.group:hover .group-hover\\:opacity-100{opacity:1}.op0,.opacity-0{opacity:0}.op25{opacity:.25}.op30{opacity:.3}.op40{opacity:.4}.op60{opacity:.6}.op60\\!{opacity:.6!important}.op70{opacity:.7}.op75{opacity:.75}.op80{opacity:.8}.hover\\:op100:hover{opacity:1}.hover\\:op60:hover{opacity:.6}.hover\\:op70:hover{opacity:.7}.hover\\:op80:hover{opacity:.8}.disabled\\:op40:disabled{opacity:.4}.disabled\\:op50:disabled{opacity:.5}.data-\\[state\\=active\\]\\:shadow-sm[data-state=active],.data-\\[state\\=on\\]\\:shadow-sm[data-state=on],.shadow-sm{--un-hub-shadow:var(--un-hub-shadow-inset) 0 1px 2px 0 var(--un-hub-shadow-color,#0000000d);box-shadow:var(--un-hub-ring-offset-shadow), var(--un-hub-ring-shadow), var(--un-hub-shadow)}.shadow{--un-hub-shadow:var(--un-hub-shadow-inset) 0 1px 3px 0 var(--un-hub-shadow-color,#0000001a),var(--un-hub-shadow-inset) 0 1px 2px -1px var(--un-hub-shadow-color,#0000001a);box-shadow:var(--un-hub-ring-offset-shadow), var(--un-hub-ring-shadow), var(--un-hub-shadow)}.shadow-2xl{--un-hub-shadow:var(--un-hub-shadow-inset) 0 25px 50px -12px var(--un-hub-shadow-color,#00000040);box-shadow:var(--un-hub-ring-offset-shadow), var(--un-hub-ring-shadow), var(--un-hub-shadow)}.shadow-lg{--un-hub-shadow:var(--un-hub-shadow-inset) 0 10px 15px -3px var(--un-hub-shadow-color,#0000001a),var(--un-hub-shadow-inset) 0 4px 6px -4px var(--un-hub-shadow-color,#0000001a);box-shadow:var(--un-hub-ring-offset-shadow), var(--un-hub-ring-shadow), var(--un-hub-shadow)}.shadow-xl{--un-hub-shadow:var(--un-hub-shadow-inset) 0 20px 25px -5px var(--un-hub-shadow-color,#0000001a),var(--un-hub-shadow-inset) 0 8px 10px -6px var(--un-hub-shadow-color,#0000001a);box-shadow:var(--un-hub-ring-offset-shadow), var(--un-hub-ring-shadow), var(--un-hub-shadow)}.outline{outline-style:solid}.outline-none{outline-offset:2px;outline:2px solid #0000}.ring-1\\.5{--un-hub-ring-width:1.5px;--un-hub-ring-offset-shadow:var(--un-hub-ring-inset) 0 0 0 var(--un-hub-ring-offset-width) var(--un-hub-ring-offset-color);--un-hub-ring-shadow:var(--un-hub-ring-inset) 0 0 0 calc(var(--un-hub-ring-width) + var(--un-hub-ring-offset-width)) var(--un-hub-ring-color);box-shadow:var(--un-hub-ring-offset-shadow), var(--un-hub-ring-shadow), var(--un-hub-shadow)}.focus-within\\:ring-2:focus-within{--un-hub-ring-width:2px;--un-hub-ring-offset-shadow:var(--un-hub-ring-inset) 0 0 0 var(--un-hub-ring-offset-width) var(--un-hub-ring-offset-color);--un-hub-ring-shadow:var(--un-hub-ring-inset) 0 0 0 calc(var(--un-hub-ring-width) + var(--un-hub-ring-offset-width)) var(--un-hub-ring-color);box-shadow:var(--un-hub-ring-offset-shadow), var(--un-hub-ring-shadow), var(--un-hub-shadow)}.focus\\:ring-1:focus{--un-hub-ring-width:1px;--un-hub-ring-offset-shadow:var(--un-hub-ring-inset) 0 0 0 var(--un-hub-ring-offset-width) var(--un-hub-ring-offset-color);--un-hub-ring-shadow:var(--un-hub-ring-inset) 0 0 0 calc(var(--un-hub-ring-width) + var(--un-hub-ring-offset-width)) var(--un-hub-ring-color);box-shadow:var(--un-hub-ring-offset-shadow), var(--un-hub-ring-shadow), var(--un-hub-shadow)}.focus\\:ring-2:focus{--un-hub-ring-width:2px;--un-hub-ring-offset-shadow:var(--un-hub-ring-inset) 0 0 0 var(--un-hub-ring-offset-width) var(--un-hub-ring-offset-color);--un-hub-ring-shadow:var(--un-hub-ring-inset) 0 0 0 calc(var(--un-hub-ring-width) + var(--un-hub-ring-offset-width)) var(--un-hub-ring-color);box-shadow:var(--un-hub-ring-offset-shadow), var(--un-hub-ring-shadow), var(--un-hub-shadow)}.focus\\:ring-3:focus{--un-hub-ring-width:3px;--un-hub-ring-offset-shadow:var(--un-hub-ring-inset) 0 0 0 var(--un-hub-ring-offset-width) var(--un-hub-ring-offset-color);--un-hub-ring-shadow:var(--un-hub-ring-inset) 0 0 0 calc(var(--un-hub-ring-width) + var(--un-hub-ring-offset-width)) var(--un-hub-ring-color);box-shadow:var(--un-hub-ring-offset-shadow), var(--un-hub-ring-shadow), var(--un-hub-shadow)}.focus-visible\\:ring-2:focus-visible{--un-hub-ring-width:2px;--un-hub-ring-offset-shadow:var(--un-hub-ring-inset) 0 0 0 var(--un-hub-ring-offset-width) var(--un-hub-ring-offset-color);--un-hub-ring-shadow:var(--un-hub-ring-inset) 0 0 0 calc(var(--un-hub-ring-width) + var(--un-hub-ring-offset-width)) var(--un-hub-ring-color);box-shadow:var(--un-hub-ring-offset-shadow), var(--un-hub-ring-shadow), var(--un-hub-shadow)}.focus-visible\\:ring-3:focus-visible{--un-hub-ring-width:3px;--un-hub-ring-offset-shadow:var(--un-hub-ring-inset) 0 0 0 var(--un-hub-ring-offset-width) var(--un-hub-ring-offset-color);--un-hub-ring-shadow:var(--un-hub-ring-inset) 0 0 0 calc(var(--un-hub-ring-width) + var(--un-hub-ring-offset-width)) var(--un-hub-ring-color);box-shadow:var(--un-hub-ring-offset-shadow), var(--un-hub-ring-shadow), var(--un-hub-shadow)}.ring-purple\\/50{--un-hub-ring-color:#c084fc80}.focus-within\\:ring-primary-500\\/40:focus-within{--un-hub-ring-color:rgb(from var(--colors-primary-500,#578861) r g b / .4)}.focus-within\\:ring-red-500\\/40:focus-within{--un-hub-ring-color:#ef444466}.focus\\:ring-primary-500\\/25:focus{--un-hub-ring-color:rgb(from var(--colors-primary-500,#578861) r g b / .25)}.focus\\:ring-primary-500\\/40:focus{--un-hub-ring-color:rgb(from var(--colors-primary-500,#578861) r g b / .4)}.focus\\:ring-primary\\/30:focus{--un-hub-ring-color:rgb(from var(--colors-primary-DEFAULT,#3a6a45) r g b / .3)}.focus\\:ring-red-500\\/40:focus{--un-hub-ring-color:#ef444466}.focus-visible\\:ring-primary-500\\/30:focus-visible{--un-hub-ring-color:rgb(from var(--colors-primary-500,#578861) r g b / .3)}.focus-visible\\:ring-primary-500\\/40:focus-visible{--un-hub-ring-color:rgb(from var(--colors-primary-500,#578861) r g b / .4)}.backdrop-blur-0{--un-hub-backdrop-blur:blur(0);backdrop-filter:var(--un-hub-backdrop-blur) var(--un-hub-backdrop-brightness) var(--un-hub-backdrop-contrast) var(--un-hub-backdrop-grayscale) var(--un-hub-backdrop-hue-rotate) var(--un-hub-backdrop-invert) var(--un-hub-backdrop-opacity) var(--un-hub-backdrop-saturate) var(--un-hub-backdrop-sepia)}.backdrop-blur-1{--un-hub-backdrop-blur:blur(1px);backdrop-filter:var(--un-hub-backdrop-blur) var(--un-hub-backdrop-brightness) var(--un-hub-backdrop-contrast) var(--un-hub-backdrop-grayscale) var(--un-hub-backdrop-hue-rotate) var(--un-hub-backdrop-invert) var(--un-hub-backdrop-opacity) var(--un-hub-backdrop-saturate) var(--un-hub-backdrop-sepia)}.backdrop-blur-sm{--un-hub-backdrop-blur:blur(4px);backdrop-filter:var(--un-hub-backdrop-blur) var(--un-hub-backdrop-brightness) var(--un-hub-backdrop-contrast) var(--un-hub-backdrop-grayscale) var(--un-hub-backdrop-hue-rotate) var(--un-hub-backdrop-invert) var(--un-hub-backdrop-opacity) var(--un-hub-backdrop-saturate) var(--un-hub-backdrop-sepia)}.blur-2xl{--un-hub-blur:blur(40px);filter:var(--un-hub-blur) var(--un-hub-brightness) var(--un-hub-contrast) var(--un-hub-drop-shadow) var(--un-hub-grayscale) var(--un-hub-hue-rotate) var(--un-hub-invert) var(--un-hub-saturate) var(--un-hub-sepia)}.invert{--un-hub-invert:invert(1);filter:var(--un-hub-blur) var(--un-hub-brightness) var(--un-hub-contrast) var(--un-hub-drop-shadow) var(--un-hub-grayscale) var(--un-hub-hue-rotate) var(--un-hub-invert) var(--un-hub-saturate) var(--un-hub-sepia)}.saturate-0,.disabled\\:saturate-0:disabled{--un-hub-saturate:saturate(0);filter:var(--un-hub-blur) var(--un-hub-brightness) var(--un-hub-contrast) var(--un-hub-drop-shadow) var(--un-hub-grayscale) var(--un-hub-hue-rotate) var(--un-hub-invert) var(--un-hub-saturate) var(--un-hub-sepia)}.filter{filter:var(--un-hub-blur) var(--un-hub-brightness) var(--un-hub-contrast) var(--un-hub-drop-shadow) var(--un-hub-grayscale) var(--un-hub-hue-rotate) var(--un-hub-invert) var(--un-hub-saturate) var(--un-hub-sepia)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-all{transition-property:all;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-opacity{transition-property:opacity;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-transform{transition-property:transform;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1)}.duration-150{transition-duration:.15s}.duration-200{transition-duration:.2s}.duration-250{transition-duration:.25s}.duration-300{transition-duration:.3s}.delay-200{transition-delay:.2s}.ease-in{transition-timing-function:cubic-bezier(.4,0,1,1)}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}@media (width>=640px){.sm\\:gap-2\\.5{gap:.625rem}.sm\\:p-6{padding:1.5rem}}*,:before,:after,::backdrop{--un-hub-rotate:0;--un-hub-rotate-x:0;--un-hub-rotate-y:0;--un-hub-rotate-z:0;--un-hub-scale-x:1;--un-hub-scale-y:1;--un-hub-scale-z:1;--un-hub-skew-x:0;--un-hub-skew-y:0;--un-hub-translate-x:0;--un-hub-translate-y:0;--un-hub-translate-z:0;--un-hub-pan-x: ;--un-hub-pan-y: ;--un-hub-pinch-zoom: ;--un-hub-scroll-snap-strictness:proximity;--un-hub-ordinal: ;--un-hub-slashed-zero: ;--un-hub-numeric-figure: ;--un-hub-numeric-spacing: ;--un-hub-numeric-fraction: ;--un-hub-border-spacing-x:0;--un-hub-border-spacing-y:0;--un-hub-ring-offset-shadow:0 0 #0000;--un-hub-ring-shadow:0 0 #0000;--un-hub-shadow-inset: ;--un-hub-shadow:0 0 #0000;--un-hub-ring-inset: ;--un-hub-ring-offset-width:0px;--un-hub-ring-offset-color:#fff;--un-hub-ring-width:0px;--un-hub-ring-color:#93c5fd80;--un-hub-blur: ;--un-hub-brightness: ;--un-hub-contrast: ;--un-hub-drop-shadow: ;--un-hub-grayscale: ;--un-hub-hue-rotate: ;--un-hub-invert: ;--un-hub-saturate: ;--un-hub-sepia: ;--un-hub-backdrop-blur: ;--un-hub-backdrop-brightness: ;--un-hub-backdrop-contrast: ;--un-hub-backdrop-grayscale: ;--un-hub-backdrop-hue-rotate: ;--un-hub-backdrop-invert: ;--un-hub-backdrop-opacity: ;--un-hub-backdrop-saturate: ;--un-hub-backdrop-sepia: }*,:before,:after{border-color:#8882}.border-base{--un-hub-border-opacity:.13;border-color:rgb(153 153 153/var(--un-hub-border-opacity))}.bg-active{--un-hub-bg-opacity:.19;background-color:rgb(153 153 153/var(--un-hub-bg-opacity))}.bg-base{--un-hub-bg-opacity:1;background-color:rgb(255 255 255/var(--un-hub-bg-opacity))}.dark .bg-base{--un-hub-bg-opacity:1;background-color:rgb(17 17 17/var(--un-hub-bg-opacity))}.bg-hover{--un-hub-bg-opacity:.13;background-color:rgb(153 153 153/var(--un-hub-bg-opacity))}.bg-secondary{--un-hub-bg-opacity:1;background-color:rgb(246 246 246/var(--un-hub-bg-opacity))}.dark .bg-secondary{--un-hub-bg-opacity:1;background-color:rgb(16 16 16/var(--un-hub-bg-opacity))}.color-active{--un-hub-text-opacity:1;color:rgb(from var(--colors-primary-600,#40704b) r g b / var(--un-hub-text-opacity))}.dark .color-active{--un-hub-text-opacity:1;color:rgb(from var(--colors-primary-300,#93c69d) r g b / var(--un-hub-text-opacity))}.color-base{--un-hub-text-opacity:1;color:rgb(38 38 38/var(--un-hub-text-opacity))}.dark .color-base{--un-hub-text-opacity:1;color:rgb(229 229 229/var(--un-hub-text-opacity))}.color-faint,.dark .color-faint{--un-hub-text-opacity:1;color:rgb(115 115 115/var(--un-hub-text-opacity))}.color-muted{--un-hub-text-opacity:1;color:rgb(82 82 82/var(--un-hub-text-opacity))}.dark .color-muted{--un-hub-text-opacity:1;color:rgb(163 163 163/var(--un-hub-text-opacity))}:root,:host,.devframes-accent-scope{--devframe-primary-DEFAULT:var(--devframe-primary);--devframe-primary-600:color-mix(in oklab, var(--devframe-primary), white 9%);--devframe-primary-500:color-mix(in oklab, var(--devframe-primary), white 18%);--devframe-primary-400:color-mix(in oklab, var(--devframe-primary), white 34%);--devframe-primary-300:color-mix(in oklab, var(--devframe-primary), white 54%);--colors-primary-DEFAULT:var(--devframe-primary-DEFAULT,#3a6a45);--colors-primary-600:var(--devframe-primary-600,#40704b);--colors-primary-500:var(--devframe-primary-500,#578861);--colors-primary-400:var(--devframe-primary-400,#75a67e);--colors-primary-300:var(--devframe-primary-300,#93c69d)}`;function lt(e){let t=e.ownerDocument?.defaultView;return t?.HTMLIFrameElement?e instanceof t.HTMLIFrameElement:typeof HTMLIFrameElement<`u`?e instanceof HTMLIFrameElement:`src`in e}function ut(e={}){let t=new Map,n=new Map,r=e.maxPanes??1/0,i,a,o=0,s,c=!1,l=0;function u(){let t=e.document??globalThis.document;if(!t)throw Error("[iframe-pane] No document available. Provide `options.document` or run in a DOM environment.");return t}function d(){return u().defaultView}function f(){if(e.container){let t=e.container,n=t.ownerDocument.defaultView?.getComputedStyle(t).position;return(!n||n===`static`)&&(t.style.position=`relative`),t}if(!i){let t=u();i=t.createElement(`div`),i.setAttribute(`data-iframe-panes`,``),Object.assign(i.style,{position:`fixed`,inset:`0`,pointerEvents:`none`}),e.zIndex!=null&&(i.style.zIndex=String(e.zIndex)),t.body.appendChild(i)}return i}function p(){return!a&&typeof ResizeObserver<`u`&&(a=new ResizeObserver(e=>{for(let t of e)n.get(t.target)?.forEach(e=>e.update())})),a}function m(){if(s!=null)return;let e=d();if(!e){h();return}s=e.requestAnimationFrame(()=>{s=void 0,h()})}function h(){for(let e of n.values())for(let t of e)t.update()}let g=()=>m();function _(){let e=d();e&&(n.size>0?(e.addEventListener(`scroll`,g,{capture:!0,passive:!0}),e.addEventListener(`resize`,g,{passive:!0})):(e.removeEventListener(`scroll`,g,{capture:!0}),e.removeEventListener(`resize`,g),s!=null&&(e.cancelAnimationFrame(s),s=void 0)))}function v(e,t){let r=n.get(t);r||(r=new Set,n.set(t,r),p()?.observe(t)),r.add(e),_()}function y(e,t){let r=n.get(t);r&&(r.delete(e),r.size===0&&(n.delete(t),a?.unobserve(t)),_())}function b(e){e.element.style.pointerEvents=o>0?`none`:e.stateStyle.pointerEvents}function x(e){if(t.size<=r)return;let n=[...t.values()].filter(t=>!t.isMounted&&t!==e).sort((e,t)=>e.lastActiveAt-t.lastActiveAt);for(let e of n){if(t.size<=r)break;e.dispose()}}class S{id;element;styleDefault;styleActive;styleHidden;stateKeys;target=null;isVisible=!1;isDisposed=!1;lastActiveAt=0;constructor(t,n){this.id=t,this.styleDefault={...e.styleDefault,...n.style},this.styleActive={opacity:`1`,pointerEvents:`auto`,...e.styleActive,...n.styleActive},this.styleHidden={opacity:`0.001`,pointerEvents:`none`,...e.styleHidden,...n.styleHidden},this.stateKeys=[...new Set([...Object.keys(this.styleActive),...Object.keys(this.styleHidden)])];let r=u(),i=this.element=n.element??r.createElement(n.tagName??`iframe`);if(i.setAttribute(`data-iframe-pane`,t),Object.assign(i.style,{display:`block`,position:`absolute`,left:`0px`,top:`0px`,border:`0`},this.styleDefault),n.attrs)for(let[e,t]of Object.entries(n.attrs))i.setAttribute(e,t);this.applyState(),n.onCreated?.(i),n.src!=null&<(i)&&(i.src=n.src),f().appendChild(i)}get iframe(){return lt(this.element)?this.element:void 0}get isMounted(){return this.target!=null}get stateStyle(){return this.isVisible?this.styleActive:this.styleHidden}applyState(){let e=this.stateStyle,t={};for(let n of this.stateKeys)n in e||(t[n]=this.styleDefault[n]??``);Object.assign(this.element.style,t,e),o>0&&(this.element.style.pointerEvents=`none`)}touch(){this.lastActiveAt=++l}mount(e){this.assertNotDisposed(),this.target!==e&&(this.target&&y(this,this.target),this.target=e,v(this,e)),this.show()}unmount(){this.target&&=(y(this,this.target),null),this.hide()}show(){this.assertNotDisposed(),this.touch(),this.isVisible=!0,this.applyState(),this.update()}hide(){this.isDisposed||(this.isVisible=!1,this.applyState())}update(){if(this.isDisposed||!this.target||!this.isVisible)return;let e=this.target.getBoundingClientRect(),t=f().getBoundingClientRect();Object.assign(this.element.style,{left:`${e.left-t.left}px`,top:`${e.top-t.top}px`,width:`${e.width}px`,height:`${e.height}px`})}dispose(){this.isDisposed||(this.unmount(),this.isDisposed=!0,this.element.remove(),t.delete(this.id),e.onPaneDisposed?.(this))}assertNotDisposed(){if(this.isDisposed)throw Error(`[iframe-pane] Pane "${this.id}" has been disposed`)}}return{list(){return[...t.values()]},get(e){return t.get(e)},has(e){return t.has(e)},ensure:((n,r={})=>{if(c)throw Error(`[iframe-pane] Manager has been disposed`);let i=t.get(n);return i?i.touch():(i=new S(n,r),i.touch(),t.set(n,i),e.onPaneCreated?.(i),x(i)),i}),get maxPanes(){return r},set maxPanes(e){r=e,x()},get isPointerLocked(){return o>0},lockPointerEvents(){o++,t.forEach(b);let e=!1;return()=>{e||(e=!0,o--,t.forEach(b))}},updateAll:h,get container(){return e.container??i},get isDisposed(){return c},dispose(){if(!c){c=!0;for(let e of[...t.values()])e.dispose();a?.disconnect(),a=void 0,_(),i?.remove(),i=void 0}}}}function dt(e){return e?e.type===`iframe`?e.frameId??e.id:e.id:null}function ft(e,t,n){let r=Ve(),i=Xe(e,e=>{e&&!r.value&&(r.value=Le(ut({container:e,document:e.ownerDocument})),i())},{immediate:!0,flush:`post`});if(n&&Xe(n,e=>{r.value?.list().forEach(t=>{t.id!==e&&t.hide()})},{flush:`post`}),t){Xe(()=>t,()=>r.value?.updateAll(),{deep:!0});let e;Xe(()=>t.isDragging||t.isResizing,t=>{t?e??=r.value?.lockPointerEvents():(e?.(),e=void 0)},{flush:`sync`}),Je(()=>e?.())}return Je(()=>r.value?.dispose()),r}function pt(e,t){let n=B(e.rpc.isTrusted),r=e.rpc.events.on(`rpc:is-trusted:updated`,e=>{n.value=e,t?.(e)});return Je(()=>r()),n}function mt(e){return Array.isArray?Array.isArray(e):Ct(e)===`[object Array]`}function ht(e){if(typeof e==`string`)return e;if(typeof e==`bigint`)return e.toString();let t=e+``;return t==`0`&&1/e==-1/0?`-0`:t}function gt(e){return e==null?``:ht(e)}function Y(e){return typeof e==`string`}function _t(e){return typeof e==`number`}function vt(e){return e===!0||e===!1||bt(e)&&Ct(e)==`[object Boolean]`}function yt(e){return typeof e==`object`}function bt(e){return yt(e)&&e!==null}function xt(e){return e!=null}function St(e){return!e.trim().length}function Ct(e){return e==null?e===void 0?`[object Undefined]`:`[object Null]`:Object.prototype.toString.call(e)}var wt=`Incorrect 'index' type`,Tt=`Invalid doc index: must be a non-negative integer within the bounds of the docs array`,Et=e=>`Invalid value for key ${e}`,Dt=e=>`Pattern length exceeds max of ${e}.`,Ot=e=>`Missing ${e} property in key`,kt=e=>`Property 'weight' in key '${e}' must be a positive integer`,At=`Fuse.match does not support useTokenSearch: token search requires corpus-level statistics (df, fieldCount) that a one-off string comparison does not have. Use new Fuse(...).search(...) instead.`,jt=Object.prototype.hasOwnProperty,Mt=class{constructor(e){this._keys=[],this._keyMap={};let t=0;e.forEach(e=>{let n=Nt(e);this._keys.push(n),this._keyMap[n.id]=n,t+=n.weight}),this._keys.forEach(e=>{e.weight/=t})}get(e){return this._keyMap[e]}keys(){return this._keys}toJSON(){return JSON.stringify(this._keys)}};function Nt(e){let t=null,n=null,r=null,i=1,a=null;if(Y(e)||mt(e))r=e,t=Pt(e),n=Ft(e);else{if(!jt.call(e,`name`))throw Error(Ot(`name`));let o=e.name;if(r=o,jt.call(e,`weight`)&&e.weight!==void 0&&(i=e.weight,i<=0))throw Error(kt(Ft(o)));t=Pt(o),n=Ft(o),a=e.getFn??null}return{path:t,id:n,weight:i,src:r,getFn:a}}function Pt(e){return mt(e)?e:e.split(`.`)}function Ft(e){return mt(e)?e.join(`.`):e}function It(e,t){let n=[],r=!1,i=(e,t,a,o)=>{if(xt(e)){if(!t[a])n.push(o===void 0?e:{v:e,i:o});else{let s=e[t[a]];if(!xt(s))return;if(a===t.length-1&&(Y(s)||_t(s)||vt(s)||typeof s==`bigint`))n.push(o===void 0?gt(s):{v:gt(s),i:o});else if(mt(s)){r=!0;for(let e=0,n=s.length;ee.score===t.score?e.idx=9&&e<=13||e===32||e===160}function Ht(e=1,t=3){let n=new Map,r=10**t;return{get(t){let i=0,a=!1;for(let e=0;e{this._keysMap[e.id]=t})}create(){if(this.isCreated||!this.docs.length)return;this.isCreated=!0;let e=this.docs.length;this.records=Array(e);let t=0;if(Y(this.docs[0]))for(let n=0;ne&&--this.records[t].i}removeAll(e){let t=new Set;for(let n of e)Number.isInteger(n)&&n>=0&&t.add(n);if(t.size===0)return;this.records=this.records.filter(e=>!t.has(e.i));let n=Array.from(t).sort((e,t)=>e-t);for(let e of this.records){let t=0,r=n.length;for(;t>>1;n[i]t),records:this.records}}};function Wt(e,t,{getFn:n=X.getFn,fieldNormWeight:r=X.fieldNormWeight}={}){let i=new Ut({getFn:n,fieldNormWeight:r});return i.setKeys(e.map(Nt)),i.setSources(t),i.create(),i}function Gt(e,{getFn:t=X.getFn,fieldNormWeight:n=X.fieldNormWeight}={}){let{keys:r,records:i}=e,a=new Ut({getFn:t,fieldNormWeight:n});return a.setKeys(r),a.setIndexRecords(i),a}function Kt(e=[],t=X.minMatchCharLength){let n=[],r=-1,i=-1,a=0;for(let o=e.length;a=t&&n.push([r,i]),r=-1)}return e[a-1]&&a-r>=t&&n.push([r,a-1]),n}function qt(e,t,n,{location:r=X.location,distance:i=X.distance,threshold:a=X.threshold,findAllMatches:o=X.findAllMatches,minMatchCharLength:s=X.minMatchCharLength,includeMatches:c=X.includeMatches,ignoreLocation:l=X.ignoreLocation}={}){if(t.length>32)throw Error(Dt(32));let u=t.length,d=e.length,f=Math.max(0,Math.min(r,d)),p=a,m=f,h=(e,t)=>{let n=e/u;if(l)return n;let r=Math.abs(f-t);return i?n+r/i:r?1:n},g=s>1||c,_=g?Array(d):[],v;for(;(v=e.indexOf(t,m))>-1;){let e=h(0,v);if(p=Math.min(e,p),m=v+u,g){let e=0;for(;e=a;--r){let i=r-1,o=n[e[i]];if(c[r]=(c[r+1]<<1|1)&o,t&&(c[r]|=(y[r+1]|y[r])<<1|1|y[r+1]),c[r]&C&&(b=h(t,i),b<=p)){if(p=b,m=i,x=t,m<=f)break;a=Math.max(1,2*f-m)}}if(h(t+1,f)>p)break;y=c}if(g&&m>=0){let t=Math.min(d-1,m+u-1+x);for(let r=m;r<=t;r+=1)n[e[r]]&&(_[r]=1)}let w={isMatch:m>=0,score:Math.max(.001,b)};if(g){let e=Kt(_,s);e.length?c&&(w.indices=e):w.isMatch=!1}return w}function Jt(e){let t={};for(let n=0,r=e.length;ne[0]-t[0]||e[1]-t[1]);let t=[e[0]];for(let n=1,r=e.length;ne.normalize(`NFD`).replace(/[\u0300-\u036F\u0483-\u0489\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u0610-\u061A\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7\u06E8\u06EA-\u06ED\u0711\u0730-\u074A\u07A6-\u07B0\u07EB-\u07F3\u07FD\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u082D\u0859-\u085B\u08D3-\u08E1\u08E3-\u0903\u093A-\u093C\u093E-\u094F\u0951-\u0957\u0962\u0963\u0981-\u0983\u09BC\u09BE-\u09C4\u09C7\u09C8\u09CB-\u09CD\u09D7\u09E2\u09E3\u09FE\u0A01-\u0A03\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A70\u0A71\u0A75\u0A81-\u0A83\u0ABC\u0ABE-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AE2\u0AE3\u0AFA-\u0AFF\u0B01-\u0B03\u0B3C\u0B3E-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B56\u0B57\u0B62\u0B63\u0B82\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD7\u0C00-\u0C04\u0C3E-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C62\u0C63\u0C81-\u0C83\u0CBC\u0CBE-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CE2\u0CE3\u0D00-\u0D03\u0D3B\u0D3C\u0D3E-\u0D44\u0D46-\u0D48\u0D4A-\u0D4D\u0D57\u0D62\u0D63\u0D82\u0D83\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DF2\u0DF3\u0E31\u0E34-\u0E3A\u0E47-\u0E4E\u0EB1\u0EB4-\u0EB9\u0EBB\u0EBC\u0EC8-\u0ECD\u0F18\u0F19\u0F35\u0F37\u0F39\u0F3E\u0F3F\u0F71-\u0F84\u0F86\u0F87\u0F8D-\u0F97\u0F99-\u0FBC\u0FC6\u102B-\u103E\u1056-\u1059\u105E-\u1060\u1062-\u1064\u1067-\u106D\u1071-\u1074\u1082-\u108D\u108F\u109A-\u109D\u135D-\u135F\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17B4-\u17D3\u17DD\u180B-\u180D\u1885\u1886\u18A9\u1920-\u192B\u1930-\u193B\u1A17-\u1A1B\u1A55-\u1A5E\u1A60-\u1A7C\u1A7F\u1AB0-\u1ABE\u1B00-\u1B04\u1B34-\u1B44\u1B6B-\u1B73\u1B80-\u1B82\u1BA1-\u1BAD\u1BE6-\u1BF3\u1C24-\u1C37\u1CD0-\u1CD2\u1CD4-\u1CE8\u1CED\u1CF2-\u1CF4\u1CF7-\u1CF9\u1DC0-\u1DF9\u1DFB-\u1DFF\u20D0-\u20F0\u2CEF-\u2CF1\u2D7F\u2DE0-\u2DFF\u302A-\u302F\u3099\u309A\uA66F-\uA672\uA674-\uA67D\uA69E\uA69F\uA6F0\uA6F1\uA802\uA806\uA80B\uA823-\uA827\uA880\uA881\uA8B4-\uA8C5\uA8E0-\uA8F1\uA8FF\uA926-\uA92D\uA947-\uA953\uA980-\uA983\uA9B3-\uA9C0\uA9E5\uAA29-\uAA36\uAA43\uAA4C\uAA4D\uAA7B-\uAA7D\uAAB0\uAAB2-\uAAB4\uAAB7\uAAB8\uAABE\uAABF\uAAC1\uAAEB-\uAAEF\uAAF5\uAAF6\uABE3-\uABEA\uABEC\uABED\uFB1E\uFE00-\uFE0F\uFE20-\uFE2F]/g,``).replace(Zt,e=>Xt[e]):e=>e,$t=class{constructor(e,{location:t=X.location,threshold:n=X.threshold,distance:r=X.distance,includeMatches:i=X.includeMatches,findAllMatches:a=X.findAllMatches,minMatchCharLength:o=X.minMatchCharLength,isCaseSensitive:s=X.isCaseSensitive,ignoreDiacritics:c=X.ignoreDiacritics,ignoreLocation:l=X.ignoreLocation}={}){if(this.options={location:t,threshold:n,distance:r,includeMatches:i,findAllMatches:a,minMatchCharLength:o,isCaseSensitive:s,ignoreDiacritics:c,ignoreLocation:l},e=s?e:e.toLowerCase(),e=c?Qt(e):e,this.pattern=e,this.chunks=[],!this.pattern.length)return;let u=(e,t)=>{this.chunks.push({pattern:e,alphabet:Jt(e),startIndex:t})},d=this.pattern.length;if(d>32){let e=0,t=d%32,n=d-t;for(;e{let{isMatch:m,score:h,indices:g}=qt(e,t,n,{location:i+p,distance:a,threshold:o,findAllMatches:s,minMatchCharLength:c,includeMatches:r,ignoreLocation:l});m&&(f=!0),d+=h,m&&g&&u.push(...g)});let p={isMatch:f,score:f?d/this.chunks.length:1};return f&&r&&(p.indices=Yt(u)),p}},en=new Set([`fuzzy`,`include`]);function tn(e){return e.startsWith(`inverse`)}var nn=[{type:`exact`,multiRegex:/^="(.*)"$/,singleRegex:/^=(.*)$/,create:e=>({type:`exact`,search(t){let n=t===e;return{isMatch:n,score:+!n,indices:[0,e.length-1]}}})},{type:`include`,multiRegex:/^'"(.*)"$/,singleRegex:/^'(.*)$/,create:e=>({type:`include`,search(t){let n=0,r,i=[],a=e.length;for(;(r=t.indexOf(e,n))>-1;)n=r+a,i.push([r,n-1]);let o=!!i.length;return{isMatch:o,score:+!o,indices:i}}})},{type:`prefix-exact`,multiRegex:/^\^"(.*)"$/,singleRegex:/^\^(.*)$/,create:e=>({type:`prefix-exact`,search(t){let n=t.startsWith(e);return{isMatch:n,score:+!n,indices:[0,e.length-1]}}})},{type:`inverse-prefix-exact`,multiRegex:/^!\^"(.*)"$/,singleRegex:/^!\^(.*)$/,create:e=>({type:`inverse-prefix-exact`,search(t){let n=!t.startsWith(e);return{isMatch:n,score:+!n,indices:[0,t.length-1]}}})},{type:`inverse-suffix-exact`,multiRegex:/^!"(.*)"\$$/,singleRegex:/^!(.*)\$$/,create:e=>({type:`inverse-suffix-exact`,search(t){let n=!t.endsWith(e);return{isMatch:n,score:+!n,indices:[0,t.length-1]}}})},{type:`suffix-exact`,multiRegex:/^"(.*)"\$$/,singleRegex:/^(.*)\$$/,create:e=>({type:`suffix-exact`,search(t){let n=t.endsWith(e);return{isMatch:n,score:+!n,indices:[t.length-e.length,t.length-1]}}})},{type:`inverse-exact`,multiRegex:/^!"(.*)"$/,singleRegex:/^!(.*)$/,create:e=>({type:`inverse-exact`,search(t){let n=t.indexOf(e)===-1;return{isMatch:n,score:+!n,indices:[0,t.length-1]}}})},{type:`fuzzy`,multiRegex:/^"(.*)"$/,singleRegex:/^(.*)$/,create:(e,t={})=>{let n=new $t(e,{location:t.location??X.location,threshold:t.threshold??X.threshold,distance:t.distance??X.distance,includeMatches:t.includeMatches??X.includeMatches,findAllMatches:t.findAllMatches??X.findAllMatches,minMatchCharLength:t.minMatchCharLength??X.minMatchCharLength,isCaseSensitive:t.isCaseSensitive??X.isCaseSensitive,ignoreDiacritics:t.ignoreDiacritics??X.ignoreDiacritics,ignoreLocation:t.ignoreLocation??X.ignoreLocation});return{type:`fuzzy`,search(e){return n.searchIn(e)}}}}],rn=nn.length,an=`\0`,on=`|`;function sn(e){let t=[],n=e.length,r=0;for(;r=n)break;let i=r;for(;i=n||e[t]===` `){i++;break}if(e[t]===`$`&&(t+1>=n||e[t+1]===` `)){i+=2;break}}i++}t.push(e.substring(r,i)),r=i}else{for(;i{let n=sn(e.replace(/\u0000/g,`|`).trim()).filter(e=>e&&!!e.trim()),r=[];for(let e=0,i=n.length;e!!(e[mn.AND]||e[mn.OR]),_n=e=>!!e[hn.PATH],vn=e=>!mt(e)&&yt(e)&&!gn(e),yn=e=>({[mn.AND]:Object.keys(e).map(t=>({[t]:e[t]}))});function bn(e,t,{auto:n=!0}={}){let r=e=>{if(Y(e)){let r={keyId:null,pattern:e};return n&&(r.searcher=pn(e,t)),r}let i=Object.keys(e),a=_n(e);if(!a&&i.length>1&&!gn(e))return r(yn(e));if(vn(e)){let r=a?e[hn.PATH]:i[0],o=a?e[hn.PATTERN]:e[r];if(!Y(o))throw Error(Et(r));let s={keyId:Ft(r),pattern:o};return n&&(s.searcher=pn(o,t)),s}let o={children:[],operator:i[0]};return i.forEach(t=>{let n=e[t];mt(n)&&n.forEach(e=>{o.children.push(r(e))})}),o};return gn(e)||(e=yn(e)),r(e)}function xn(e,{ignoreFieldNorm:t=X.ignoreFieldNorm}){let n=1;return e.forEach(({key:e,norm:r,score:i})=>{let a=e?e.weight:null;n*=(i===0&&a?2**-52:i)**+((a||1)*(t?1:r))}),n}function Sn(e,{ignoreFieldNorm:t=X.ignoreFieldNorm}){e.forEach(e=>{e.score=xn(e.matches,{ignoreFieldNorm:t})})}var Cn=class{constructor(e,t){this.limit=e,this.heap=[],this.comparator=t}get size(){return this.heap.length}insert(e){this.size0;){let n=e-1>>1;if(this.comparator(t[e],t[n])<=0)break;let r=t[e];t[e]=t[n],t[n]=r,e=n}}_sinkDown(e){let t=this.heap,n=t.length,r=e;do{e=r;let i=2*e+1,a=2*e+2;if(i0&&(r=i),a0&&(r=a),r!==e){let n=t[e];t[e]=t[r],t[r]=n}}while(r!==e)}};function wn(e){let t=[];return e.matches.forEach(e=>{if(!xt(e.indices)||!e.indices.length)return;let n={indices:e.indices,value:e.value};e.key&&(n.key=e.key.id),e.idx>-1&&(n.refIndex=e.idx),t.push(n)}),t}function Tn(e,t,{includeMatches:n=X.includeMatches,includeScore:r=X.includeScore}={}){return e.map(e=>{let{idx:i}=e,a={item:t[i],refIndex:i};return n&&(a.matches=wn(e)),r&&(a.score=e.score),a})}var En=/[\p{L}\p{M}\p{N}_]+/gu,Dn=new WeakSet;function On(e){Dn.has(e)||(Dn.add(e),console.warn(`[Fuse] tokenize regex ${e} lacks the global flag; only the first match per text will be returned. Add the 'g' flag.`))}function kn(e){if(typeof e==`function`){let t=!1;return n=>{let r=e(n);if(!t&&(t=!0,!Array.isArray(r)||r.some(e=>typeof e!=`string`)))throw Error(`[Fuse] tokenize function must return string[]; received ${Array.isArray(r)?`array containing non-strings`:typeof r}.`);return r}}return e instanceof RegExp?(e.global||On(e),t=>t.match(e)||[]):e=>e.match(En)||[]}function An({isCaseSensitive:e=!1,ignoreDiacritics:t=!1,tokenize:n}={}){let r=kn(n);return{tokenize(n){return e||(n=n.toLowerCase()),t&&(n=Qt(n)),r(n)}}}var jn=class{static condition(e,t){return t.useTokenSearch}constructor(e,t){this.options=t,this.analyzer=An({isCaseSensitive:t.isCaseSensitive,ignoreDiacritics:t.ignoreDiacritics,tokenize:t.tokenize});let n=this.analyzer.tokenize(e),{df:r,fieldCount:i}=t._invertedIndex;this.termSearchers=[],this.idfWeights=[];for(let e of n){this.termSearchers.push(new $t(e,{location:t.location,threshold:t.threshold,distance:t.distance,includeMatches:t.includeMatches,findAllMatches:t.findAllMatches,minMatchCharLength:t.minMatchCharLength,isCaseSensitive:t.isCaseSensitive,ignoreDiacritics:t.ignoreDiacritics,ignoreLocation:!0}));let n=r.get(e)||0,a=Math.log(1+(i-n+.5)/(n+.5));this.idfWeights.push(a)}this.combineAll=t.tokenMatch===`all`,this.numTerms=this.termSearchers.length,this.useMask=this.numTerms<=31}searchIn(e){if(!this.termSearchers.length)return{isMatch:!1,score:1};let t=[],n=0,r=0,i=0,a=0,o=this.combineAll&&!this.useMask?new Set:null;for(let s=0;s0?1-n/r:0,c={isMatch:!0,score:Math.max(.001,s)};return this.options.includeMatches&&t.length&&(c.indices=Yt(t)),this.combineAll&&(this.useMask?c.matchedMask=a:c.matchedTerms=o,c.termCount=this.numTerms),c}};function Mn(e,t,n,r){let i=r.tokenize(t);if(!i.length)return;e.fieldCount++,e.docFieldCount.set(n,(e.docFieldCount.get(n)||0)+1);let a=new Set(i),o=e.docTermFieldHits.get(n);o||(o=new Map,e.docTermFieldHits.set(n,o));for(let t of a)o.set(t,(o.get(t)||0)+1),e.df.set(t,(e.df.get(t)||0)+1)}function Nn(e,t,n,r){let{i,v:a,$:o}=t;if(a!==void 0){Mn(e,a,i,r);return}if(o)for(let t=0;te-t);for(let t of n)In(e,t);let r=e=>{let t=0,r=n.length;for(;t>>1;n[i]i?r(t):t,n);e.docFieldCount=a;let o=new Map;for(let[t,n]of e.docTermFieldHits)o.set(t>i?r(t):t,n);e.docTermFieldHits=o}var Rn=class{constructor(e,t,n){this.options={...X,...t},this.options.useExtendedSearch,this.options.useTokenSearch,this._keyStore=new Mt(this.options.keys),this._docs=e,this._myIndex=null,this._invertedIndex=null,this.setCollection(e,n),this._lastQuery=null,this._lastSearcher=null}_getSearcher(e){if(this._lastQuery===e)return this._lastSearcher;let t=pn(e,this._invertedIndex?{...this.options,_invertedIndex:this._invertedIndex}:this.options);return this._lastQuery=e,this._lastSearcher=t,t}setCollection(e,t){if(this._docs=e,t&&!(t instanceof Ut))throw Error(wt);if(this._myIndex=t||Wt(this.options.keys,this._docs,{getFn:this.options.getFn,fieldNormWeight:this.options.fieldNormWeight}),this.options.useTokenSearch){let e=An({isCaseSensitive:this.options.isCaseSensitive,ignoreDiacritics:this.options.ignoreDiacritics,tokenize:this.options.tokenize});this._invertedIndex=Pn(this._myIndex.records,this._myIndex.keys.length,e)}this._invalidateSearcherCache()}add(e){if(!xt(e))return;this._docs.push(e);let t=this._myIndex.add(e,this._docs.length-1);if(this._invertedIndex&&t){let e=An({isCaseSensitive:this.options.isCaseSensitive,ignoreDiacritics:this.options.ignoreDiacritics,tokenize:this.options.tokenize});Fn(this._invertedIndex,t,this._myIndex.keys.length,e)}this._invalidateSearcherCache()}remove(e=()=>!1){let t=[],n=[];for(let r=0,i=this._docs.length;r!e.has(n)),this._myIndex.removeAll(n),this._invalidateSearcherCache()}return t}removeAt(e){if(!Number.isInteger(e)||e<0||e>=this._docs.length)throw Error(Tt);this._invertedIndex&&Ln(this._invertedIndex,[e]);let t=this._docs.splice(e,1)[0];return this._myIndex.removeAt(e),this._invalidateSearcherCache(),t}_invalidateSearcherCache(){this._lastQuery=null,this._lastSearcher=null}getIndex(){return this._myIndex}_normalizedKeys(){return this._myIndex.keys.map(e=>this._keyStore.get(e.id)||e)}search(e,t){let{limit:n=-1}=t||{},{includeMatches:r,includeScore:i,shouldSort:a,sortFn:o,ignoreFieldNorm:s}=this.options;if(Y(e)&&!e.trim()){let e=this._docs.map((e,t)=>({item:e,refIndex:t}));return _t(n)&&n>-1&&(e=e.slice(0,n)),e}let c=a&&_t(n)&&n>0&&Y(e),l=o,u=(e,t)=>l(e,t)||e.idx-t.idx,d;if(c){let t=new Cn(n,u);Y(this._docs[0])?this._searchStringList(e,{heap:t,ignoreFieldNorm:s}):this._searchObjectList(e,{heap:t,ignoreFieldNorm:s}),d=t.extractSorted()}else d=Y(e)?Y(this._docs[0])?this._searchStringList(e):this._searchObjectList(e):this._searchLogical(e),Sn(d,{ignoreFieldNorm:s}),a&&d.sort(Y(e)?u:l),_t(n)&&n>-1&&(d=d.slice(0,n));return Tn(d,this._docs,{includeMatches:r,includeScore:i})}_searchStringList(e,{heap:t,ignoreFieldNorm:n}={}){let r=this._getSearcher(e),i=this.options.useTokenSearch&&this.options.tokenMatch===`all`,{records:a}=this._myIndex,o=t?null:[];return a.forEach(({v:e,i:a,n:s})=>{if(!xt(e))return;let c=r.searchIn(e);if(c.isMatch){let r={score:c.score,value:e,norm:s,indices:c.indices};i&&(r.matchedMask=c.matchedMask,r.matchedTerms=c.matchedTerms,r.termCount=c.termCount);let l=[r];if(!i||this._coversAllTokens(l)){let r={item:e,idx:a,matches:l};t?(r.score=xn(r.matches,{ignoreFieldNorm:n}),t.insert(r)):o.push(r)}}}),o}_searchLogical(e){let t=bn(e,this.options),n=this._normalizedKeys(),r=(e,t,i)=>{if(!(`children`in e)){let{keyId:r,searcher:a}=e,o;return r===null?(o=[],n.forEach((e,n)=>{o.push(...this._findMatches({key:e,value:t[n],searcher:a}))})):o=this._findMatches({key:this._keyStore.get(r),value:this._myIndex.getValueForItemAtKeyId(t,r),searcher:a}),o&&o.length?[{idx:i,item:t,matches:o}]:[]}let{children:a,operator:o}=e,s=[];for(let e=0,n=a.length;e{if(xt(e)){let i=r(t,e,n);i.length&&(a.has(n)||(a.set(n,{idx:n,item:e,matches:[]}),o.push(a.get(n))),i.forEach(({matches:e})=>{a.get(n).matches.push(...e)}))}}),o}_searchObjectList(e,{heap:t,ignoreFieldNorm:n}={}){let r=this._getSearcher(e),i=this.options.useTokenSearch&&this.options.tokenMatch===`all`,{records:a}=this._myIndex,o=this._normalizedKeys(),s=t?null:[];return a.forEach(({$:e,i:a})=>{if(!xt(e))return;let c=[],l=!1,u=!1;if(o.forEach((t,n)=>{let i=this._findMatches({key:t,value:e[n],searcher:r});i.length?(c.push(...i),i[0].hasInverse&&(u=!0)):l=!0}),!(u&&l)&&c.length&&(!i||this._coversAllTokens(c))){let r={idx:a,item:e,matches:c};t?(r.score=xn(r.matches,{ignoreFieldNorm:n}),t.insert(r)):s.push(r)}}),s}_findMatches({key:e,value:t,searcher:n}){if(!xt(t))return[];let r=[];if(mt(t))t.forEach(({v:t,i,n:a})=>{if(!xt(t))return;let o=n.searchIn(t);if(o.isMatch){let n={score:o.score,key:e,value:t,idx:i,norm:a,indices:o.indices,hasInverse:o.hasInverse};o.termCount!==void 0&&(n.matchedMask=o.matchedMask,n.matchedTerms=o.matchedTerms,n.termCount=o.termCount),r.push(n)}});else{let{v:i,n:a}=t,o=n.searchIn(i);if(o.isMatch){let t={score:o.score,key:e,value:i,norm:a,indices:o.indices,hasInverse:o.hasInverse};o.termCount!==void 0&&(t.matchedMask=o.matchedMask,t.matchedTerms=o.matchedTerms,t.termCount=o.termCount),r.push(t)}}return r}_coversAllTokens(e){let t=e.length?e[0].termCount:void 0;if(t===void 0)return!0;if(t<=31){let n=0;for(let t=0;tfn(e))};var zn=Rn,Bn={viewBox:`0 0 500 500`,fill:`none`,xmlns:`http://www.w3.org/2000/svg`,role:`img`,"aria-label":`Devframe logo`},Vn=[`fill`],Hn=J({__name:`DevframeLogo`,setup(e){let t=`devframe-logo-gradient-${ze()}`;return(e,n)=>(G(),j(`svg`,Bn,[P(`path`,{d:`M238.464 40H66V236.007H104.577C220.688 236.007 232.79 158.437 232.79 121.733C232.79 108.867 243.38 108.867 243.38 121.733C251.318 236.701 356.35 245.457 365.527 246.223L365.542 246.224C374.619 246.981 382.561 251.521 365.542 254.548C253.592 264.765 243.758 364.282 243.38 375.634C243.002 386.986 235.438 399.851 232.79 375.634C222.957 274.603 144.289 257.954 104.577 257.954H66V460.772H238.464C358.356 460.772 452.152 371.472 452.152 251.521C452.152 131.571 356.465 40 238.464 40Z`,fill:`url(#${t})`,stroke:`#86997F`,"stroke-opacity":`0.5`,"stroke-width":`1.33156`},null,8,Vn),P(`defs`,null,[P(`radialGradient`,{id:t,cx:`0`,cy:`0`,r:`1`,gradientTransform:`matrix(399.011 555.102 -568.423 408.982 -11.6221 -34.9218)`,gradientUnits:`userSpaceOnUse`},[...n[0]||=[P(`stop`,{"stop-color":`#ADC77F`},null,-1),P(`stop`,{offset:`0.71903`,"stop-color":`#517158`},null,-1),P(`stop`,{offset:`1`,"stop-color":`#486954`},null,-1)]])])]))}}),Un={class:`flex items-center gap-1.5 select-none`,"aria-hidden":`true`},Wn=[`src`,`alt`],Gn=[`src`,`alt`],Kn={class:`text-xl font-bold tracking-tight color-base`},qn=J({__name:`BrandWordmark`,setup(e){let t=qe(),n=tt(e=>e.wordmark),r=tt(e=>e.logo);return(e,i)=>(G(),j(`div`,Un,[U(n)?(G(),j(`img`,{key:0,src:U(n),alt:U(t).productName,class:`h-6 flex-none object-contain`,draggable:`false`},null,8,Wn)):(G(),j(H,{key:1},[U(r)?(G(),j(`img`,{key:0,src:U(r),alt:`${U(t).productName} logo`,class:`w-6 h-6 flex-none object-contain`,draggable:`false`},null,8,Gn)):(G(),F(Hn,{key:1,class:`w-6 h-6 flex-none`})),P(`span`,Kn,q(U(t).productName),1)],64))]))}}),Jn=typeof navigator<`u`&&/mac|iphone|ipad|ipod/i.test(navigator.platform||navigator.userAgent),Yn={mod:Jn?`meta`:`ctrl`,cmd:`meta`,command:`meta`,super:`meta`,win:`meta`,meta:`meta`,ctrl:`ctrl`,control:`ctrl`,alt:`alt`,option:`alt`,opt:`alt`,shift:`shift`},Xn={esc:`Escape`,escape:`Escape`,enter:`Enter`,return:`Enter`,space:` `,tab:`Tab`,up:`ArrowUp`,down:`ArrowDown`,left:`ArrowLeft`,right:`ArrowRight`},Zn=[`ctrl`,`alt`,`shift`,`meta`];function Qn(e){let t=e.trim().split(`+`).map(e=>e.trim()).filter(Boolean),n=new Set,r=``;for(let e of t){let t=e.toLowerCase(),i=Yn[t];i==null?r=Xn[t]??(e.length===1?e.toLowerCase():e):n.add(i)}return{modifiers:Zn.filter(e=>n.has(e)),key:r}}function $n(e){return e.trim().split(/\s+/).filter(Boolean).map(Qn)}var er={meta:`⌘`,ctrl:`⌃`,alt:`⌥`,shift:`⇧`},tr={meta:`Win`,ctrl:`Ctrl`,alt:`Alt`,shift:`Shift`},nr={Enter:`↵`,Escape:`Esc`,ArrowUp:`↑`,ArrowDown:`↓`,ArrowLeft:`←`,ArrowRight:`→`," ":`Space`};function rr(e){let t=e.modifiers.map(e=>(Jn?er[e]:tr[e])??e),n=nr[e.key]??(e.key.length===1?e.key.toUpperCase():e.key);return[...t,n]}function ir(e){return $n(e).flatMap(rr)}var ar={class:`inline-flex gap-0.5 items-center`},or={key:1,class:`text-micro color-muted leading-none font-mono px-1.5 border border-base rounded bg-sunken inline-flex h-5 min-w-5 items-center justify-center`},sr=J({__name:`DisplayKbd`,props:{keys:{}},setup(e){let t=e,n=N(()=>t.keys?ir(t.keys):[]);return(t,r)=>(G(),j(`span`,ar,[e.keys?(G(!0),j(H,{key:0},L(n.value,(e,t)=>(G(),j(`kbd`,{key:t,class:`text-micro color-muted leading-none font-mono px-1 border border-base rounded bg-sunken inline-flex h-5 min-w-5 items-center justify-center`},q(e),1))),128)):(G(),j(`kbd`,or,[Pe(t.$slots,`default`)]))]))}}),cr={class:`flex items-center justify-center`},lr=[`src`,`alt`],ur=J({__name:`BrandMark`,setup(e){let t=qe(),n=tt(e=>e.logo);return(e,r)=>(G(),j(`div`,cr,[U(n)?(G(),j(`img`,{key:0,src:U(n),alt:`${U(t).productName} logo`,class:`w-full h-full object-contain`,draggable:`false`},null,8,lr)):(G(),F(Hn,{key:1,class:`w-full h-full`}))]))}});function dr(e,t){(t==null||t>e.length)&&(t=e.length);for(var n=0,r=Array(t);n`u`?null:Z(BigInt.prototype.toString),Ur=typeof Symbol>`u`?null:Z(Symbol.prototype.toString),Wr=Z(Object.prototype.hasOwnProperty),Gr=Z(Object.prototype.toString),Kr=Z(RegExp.prototype.test),qr=Jr(TypeError);function Z(e){return function(t){t instanceof RegExp&&(t.lastIndex=0);var n=[...arguments].slice(1);return Er(e,t,n)}}function Jr(e){return function(){return Dr(e,[...arguments])}}function Q(e,t){let n=arguments.length>2&&arguments[2]!==void 0?arguments[2]:Pr;if(vr&&vr(e,null),!Nr(t))return e;let r=t.length;for(;r--;){let i=t[r];if(typeof i==`string`){let e=n(i);e!==i&&(yr(t)||(t[r]=e),i=e)}e[i]=!0}return e}function Yr(e){for(let t=0;t/g),pi=Cr(/\${[\w\W]*/g),mi=Cr(/^data-[\-\w.\u00B7-\uFFFF]+$/),hi=Cr(/^aria-[\-\w]+$/),gi=Cr(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp|matrix):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),_i=Cr(/^(?:\w+script|data):/i),vi=Cr(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),yi=Cr(/^html$/i),bi=Cr(/^[a-z][.\w]*(-[.\w]+)+$/i),xi=Cr(/<[/\w!]/g),Si=Cr(/<[/\w]/g),Ci=Cr(/<\/no(script|embed|frames)/i),wi=Cr(/\/>/i),Ti={element:1,attribute:2,text:3,cdataSection:4,entityReference:5,entityNode:6,processingInstruction:7,comment:8,document:9,documentType:10,documentFragment:11,notation:12},Ei=[`style`,`script`,`xmp`,`iframe`,`noembed`,`noframes`,`plaintext`,`noscript`],Di=Sr(Q({},Ei)),Oi=function(){let e={};return Or(Ei,t=>{e[t]=Cr(RegExp(`])`,`i`))}),Sr(e)}(),ki=function(){return typeof window>`u`?null:window},Ai=function(e,t){if(typeof e!=`object`||typeof e.createPolicy!=`function`)return null;let n=null,r=`data-tt-policy-suffix`;t&&t.hasAttribute(r)&&(n=t.getAttribute(r));let i=`dompurify`+(n?`#`+n:``);try{return e.createPolicy(i,{createHTML(e){return e},createScriptURL(e){return e}})}catch{return console.warn(`TrustedTypes policy `+i+` could not be created.`),null}},ji=function(){return{afterSanitizeAttributes:[],afterSanitizeElements:[],afterSanitizeShadowDOM:[],beforeSanitizeAttributes:[],beforeSanitizeElements:[],beforeSanitizeShadowDOM:[],uponSanitizeAttribute:[],uponSanitizeElement:[],uponSanitizeShadowNode:[]}},Mi=function(e,t,n,r){return Wr(e,t)&&Nr(e[t])?Q(r.base?Xr(r.base):{},e[t],r.transform):n},Ni=function(e,t,n){let r=Wr(e,t)?e[t]:void 0;return r&&typeof r==`object`?Xr(r):n()};function Pi(){let e=arguments.length>0&&arguments[0]!==void 0?arguments[0]:ki(),t=e=>Pi(e);if(t.version=`3.4.14`,t.removed=[],!e||!e.document||e.document.nodeType!==Ti.document||!e.Element)return t.isSupported=!1,t;let n=e.document,r=n,i=r.currentScript;e.DocumentFragment;let a=e.HTMLTemplateElement,o=e.Node,s=e.Element,c=e.NodeFilter;e.NamedNodeMap===void 0&&(e.NamedNodeMap||e.MozNamedAttrMap),e.HTMLFormElement;let l=e.DOMParser,u=e.trustedTypes,d=s.prototype,f=Qr(d,`cloneNode`),p=Qr(d,`remove`),m=Qr(d,`nextSibling`),h=Qr(d,`childNodes`),g=Qr(d,`parentNode`),_=Qr(d,`shadowRoot`),v=Qr(d,`attributes`),y=o&&o.prototype?Qr(o.prototype,`nodeType`):null,b=o&&o.prototype?Qr(o.prototype,`nodeName`):null,x=o&&o.prototype?Qr(o.prototype,`ownerDocument`):null,S=function(e){return y?y(e):e.nodeType},C=function(e){return b?b(e):e.nodeName};if(typeof a==`function`){let e=n.createElement(`template`);e.content&&e.content.ownerDocument&&(n=e.content.ownerDocument)}let w,T=``,E,ee=!1,te=0,D=function(){if(te>0)throw qr(`A configured TRUSTED_TYPES_POLICY callback (createHTML or createScriptURL) must not call DOMPurify.sanitize, as that causes infinite recursion. Do not pass a policy whose callbacks wrap DOMPurify as TRUSTED_TYPES_POLICY; see the "DOMPurify and Trusted Types" section of the README.`)},ne=function(e){D(),te++;try{return w.createHTML(e)}finally{te--}},re=function(e){D(),te++;try{return w.createScriptURL(e)}finally{te--}},ie=function(){return ee||=(E=Ai(u,i),!0),E},ae=n,oe=ae.implementation,se=ae.createNodeIterator,ce=ae.createDocumentFragment,le=ae.getElementsByTagName,ue=r.importNode,O=ji();t.isSupported=typeof _r==`function`&&typeof g==`function`&&oe&&oe.createHTMLDocument!==void 0;let de=di,fe=fi,pe=pi,me=mi,he=hi,ge=_i,_e=vi,ve=bi,ye=gi,k=null,be=Q({},[...ei,...ti,...ni,...ii,...oi]),A=null,xe=Q({},[...si,...ci,...li,...ui]),Se=Object.seal(wr(null,{tagNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},allowCustomizedBuiltInElements:{writable:!0,configurable:!1,enumerable:!0,value:!1}})),j=null,Ce=null,we=Object.seal(wr(null,{tagCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeCheck:{writable:!0,configurable:!1,enumerable:!0,value:null}})),M=!0,N=!0,P=!1,Te=!0,Ee=!1,F=!0,De=!1,I=!1,Oe=null,ke=null,Ae=!1,je=!1,Me=!1,L=!1,Ne=!0,Pe=!1,Fe=`user-content-`,R=!0,z=!1,Ie={},Le=null,Re=Q({},`annotation-xml.audio.colgroup.desc.foreignobject.head.iframe.math.mi.mn.mo.ms.mtext.noembed.noframes.noscript.plaintext.script.selectedcontent.style.svg.template.thead.title.video.xmp`.split(`.`)),B=null,ze=Q({},[`audio`,`video`,`img`,`source`,`image`,`track`]),Be=null,Ve=Q({},[`alt`,`class`,`for`,`id`,`label`,`name`,`pattern`,`placeholder`,`role`,`summary`,`title`,`value`,`style`,`xmlns`]),He=`http://www.w3.org/1998/Math/MathML`,Ue=`http://www.w3.org/2000/svg`,We=`http://www.w3.org/1999/xhtml`,V=We,Ge=!1,Ke=null,H=Q({},[He,Ue,We],Fr),qe=Sr([`mi`,`mo`,`mn`,`ms`,`mtext`]),Je=Q({},qe),Ye=Sr([`annotation-xml`]),U=Q({},Ye),Xe=Q({},[`title`,`style`,`font`,`a`,`script`]),Ze=null,Qe=[`application/xhtml+xml`,`text/html`],W=null,G=null,$e=n.createElement(`form`),et=function(e){return e instanceof RegExp||e instanceof Function},K=function(){let e=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{};if(G&&G===e)return;(!e||typeof e!=`object`)&&(e={}),e=Xr(e),Ze=Qe.indexOf(e.PARSER_MEDIA_TYPE)===-1?`text/html`:e.PARSER_MEDIA_TYPE,W=Ze===`application/xhtml+xml`?Fr:Pr,k=Mi(e,`ALLOWED_TAGS`,be,{transform:W}),A=Mi(e,`ALLOWED_ATTR`,xe,{transform:W}),Ke=Mi(e,`ALLOWED_NAMESPACES`,H,{transform:Fr}),Be=Mi(e,`ADD_URI_SAFE_ATTR`,Ve,{transform:W,base:Ve}),B=Mi(e,`ADD_DATA_URI_TAGS`,ze,{transform:W,base:ze}),Le=Mi(e,`FORBID_CONTENTS`,Re,{transform:W}),j=Mi(e,`FORBID_TAGS`,Xr({}),{transform:W}),Ce=Mi(e,`FORBID_ATTR`,Xr({}),{transform:W}),Ie=Wr(e,`USE_PROFILES`)?e.USE_PROFILES&&typeof e.USE_PROFILES==`object`?Xr(e.USE_PROFILES):e.USE_PROFILES:!1,M=e.ALLOW_ARIA_ATTR!==!1,N=e.ALLOW_DATA_ATTR!==!1,P=e.ALLOW_UNKNOWN_PROTOCOLS||!1,Te=e.ALLOW_SELF_CLOSE_IN_ATTR!==!1,Ee=e.SAFE_FOR_TEMPLATES||!1,F=e.SAFE_FOR_XML!==!1,De=e.WHOLE_DOCUMENT||!1,je=e.RETURN_DOM||!1,Me=e.RETURN_DOM_FRAGMENT||!1,L=e.RETURN_TRUSTED_TYPE||!1,Ae=e.FORCE_BODY||!1,Ne=e.SANITIZE_DOM!==!1,Pe=e.SANITIZE_NAMED_PROPS||!1,R=e.KEEP_CONTENT!==!1,z=e.IN_PLACE||!1,ye=$r(e.ALLOWED_URI_REGEXP)?e.ALLOWED_URI_REGEXP:gi,V=typeof e.NAMESPACE==`string`?e.NAMESPACE:We,Je=Ni(e,`MATHML_TEXT_INTEGRATION_POINTS`,()=>Q({},qe)),U=Ni(e,`HTML_INTEGRATION_POINTS`,()=>Q({},Ye));let t=Ni(e,`CUSTOM_ELEMENT_HANDLING`,()=>wr(null));if(Se=wr(null),Wr(t,`tagNameCheck`)&&et(t.tagNameCheck)&&(Se.tagNameCheck=t.tagNameCheck),Wr(t,`attributeNameCheck`)&&et(t.attributeNameCheck)&&(Se.attributeNameCheck=t.attributeNameCheck),Wr(t,`allowCustomizedBuiltInElements`)&&typeof t.allowCustomizedBuiltInElements==`boolean`&&(Se.allowCustomizedBuiltInElements=t.allowCustomizedBuiltInElements),Cr(Se),Ee&&(N=!1),Me&&(je=!0),Ie&&(k=Q({},oi),A=wr(null),Ie.html===!0&&(Q(k,ei),Q(A,si)),Ie.svg===!0&&(Q(k,ti),Q(A,ci),Q(A,ui)),Ie.svgFilters===!0&&(Q(k,ni),Q(A,ci),Q(A,ui)),Ie.mathMl===!0&&(Q(k,ii),Q(A,li),Q(A,ui))),we.tagCheck=null,we.attributeCheck=null,Wr(e,`ADD_TAGS`)&&(typeof e.ADD_TAGS==`function`?we.tagCheck=e.ADD_TAGS:Nr(e.ADD_TAGS)&&(k===be&&(k=Xr(k)),Q(k,e.ADD_TAGS,W))),Wr(e,`ADD_ATTR`)&&(typeof e.ADD_ATTR==`function`?we.attributeCheck=e.ADD_ATTR:Nr(e.ADD_ATTR)&&(A===xe&&(A=Xr(A)),Q(A,e.ADD_ATTR,W))),Wr(e,`ADD_FORBID_CONTENTS`)&&Nr(e.ADD_FORBID_CONTENTS)&&(Le===Re&&(Le=Xr(Le)),Q(Le,e.ADD_FORBID_CONTENTS,W)),R&&(k[`#text`]=!0),De&&Q(k,[`html`,`head`,`body`]),k.table&&(Q(k,[`tbody`]),delete j.tbody),e.TRUSTED_TYPES_POLICY){if(typeof e.TRUSTED_TYPES_POLICY.createHTML!=`function`)throw qr(`TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.`);if(typeof e.TRUSTED_TYPES_POLICY.createScriptURL!=`function`)throw qr(`TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.`);let t=w;w=e.TRUSTED_TYPES_POLICY;try{T=ne(``)}catch(e){throw w=t,e}}else e.TRUSTED_TYPES_POLICY===null?(w=void 0,T=``):(w===void 0&&(w=ie()),w&&typeof T==`string`&&(T=ne(``)));Sr&&Sr(e),G=e},tt=Q({},[...ti,...ni,...ri]),nt=Q({},[...ii,...ai]),rt=function(e,t,n){return t.namespaceURI===We?e===`svg`:t.namespaceURI===He?e===`svg`&&(n===`annotation-xml`||Je[n]):!!tt[e]},q=function(e,t,n){return t.namespaceURI===We?e===`math`:t.namespaceURI===Ue?e===`math`&&U[n]:!!nt[e]},it=function(e,t,n){return t.namespaceURI===Ue&&!U[n]||t.namespaceURI===He&&!Je[n]?!1:!nt[e]&&(Xe[e]||!tt[e])},at=function(e){let t=g(e);(!t||!t.tagName)&&(t={namespaceURI:V,tagName:`template`});let n=Pr(e.tagName),r=Pr(t.tagName);return Ke[e.namespaceURI]?e.namespaceURI===Ue?rt(n,t,r):e.namespaceURI===He?q(n,t,r):e.namespaceURI===We?it(n,t,r):!!(Ze===`application/xhtml+xml`&&Ke[e.namespaceURI]):!1},ot=function(e){jr(t.removed,{element:e});try{g(e).removeChild(e)}catch{if(p(e),!g(e))throw qr(`a node selected for removal could not be detached from its tree and cannot be safely returned; refusing to sanitize in place`)}},st=function(e,t,n){try{e.removeAttributeNode(t)}catch{try{e.removeAttribute(n)}catch{}}},J=function(e){ut(e);let t=h(e);if(t){let e=[];Or(t,t=>{jr(e,t)}),Or(e,e=>{try{p(e)}catch{}})}let n=v(e);if(n)for(let t=n.length-1;t>=0;--t){let r=n[t],i=r&&r.name;typeof i==`string`&&st(e,r,i)}},ct=function(e,n,r){if(!r)try{r=n.getAttributeNode(e)}catch{r=null}jr(t.removed,{attribute:r||null,from:n});try{r?n.removeAttributeNode(r):n.removeAttribute(e)}catch{try{n.removeAttribute(e)}catch{}}if(e===`is`){if(je||Me)try{ot(n)}catch{}else try{n.setAttribute(e,``)}catch{}}},lt=function(e){let t=v(e);if(t)for(let n=t.length-1;n>=0;--n){let r=t[n],i=r&&r.name;typeof i!=`string`||A[W(i)]||st(e,r,i)}},ut=function(e){let t=[e];for(;t.length>0;){let e=t.pop();S(e)===Ti.element&<(e);let n=h(e);if(n)for(let e=n.length-1;e>=0;--e)t.push(n[e])}},dt=function(e,t){return F?e===`patchsrc`||e===`for`&&t!==`label`&&t!==`output`:!1},ft=function(e){if(!F)return;let t=[e];for(;t.length>0;){let e=t.pop(),n=S(e);if(n===Ti.processingInstruction||n===Ti.comment&&Kr(Si,e.data)){try{p(e)}catch{}continue}if(n===Ti.element){let t=e,n=W(C(e));try{t.hasAttribute&&t.hasAttribute(`patchsrc`)&&t.removeAttribute(`patchsrc`),t.hasAttribute&&t.hasAttribute(`for`)&&dt(`for`,n)&&t.removeAttribute(`for`)}catch{}}let r=h(e);if(r)for(let e=r.length-1;e>=0;--e)t.push(r[e])}},pt=function(e){let t=null,r=null;if(Ae)e=``+e;else{let t=Ir(e,/^[\r\n\t ]+/);r=t&&t[0]}Ze===`application/xhtml+xml`&&V===We&&(e=``+e+``);let i=w?ne(e):e;if(V===We)try{t=new l().parseFromString(i,Ze)}catch{}if(!t||!t.documentElement){t=oe.createDocument(V,`template`,null);try{t.documentElement.innerHTML=Ge?T:i}catch{}}let a=t.body||t.documentElement;return e&&r&&a.insertBefore(n.createTextNode(r),a.childNodes[0]||null),V===We?le.call(t,De?`html`:`body`)[0]:De?t.documentElement:a},mt=function(e){let t=x?x(e):e.ownerDocument;return se.call(t||e,e,c.SHOW_ELEMENT|c.SHOW_COMMENT|c.SHOW_TEXT|c.SHOW_PROCESSING_INSTRUCTION|c.SHOW_CDATA_SECTION,null)},ht=function(e){return e=Lr(e,de,` `),e=Lr(e,fe,` `),e=Lr(e,pe,` `),e},gt=function(e){e.normalize();let t=x?x(e):e.ownerDocument,n=se.call(t||e,e,c.SHOW_TEXT|c.SHOW_COMMENT|c.SHOW_CDATA_SECTION|c.SHOW_PROCESSING_INSTRUCTION,null),r=n.nextNode();for(;r;)r.data=ht(r.data),r=n.nextNode();let i=e.querySelectorAll?.call(e,`template`);i&&Or(i,e=>{_t(e.content)&>(e.content)})},Y=function(e){let t=b?b(e):null;return typeof t!=`string`||W(t)!==`form`?!1:typeof e.nodeName!=`string`||typeof e.textContent!=`string`||typeof e.removeChild!=`function`||e.attributes!==v(e)||typeof e.removeAttribute!=`function`||typeof e.setAttribute!=`function`||typeof e.namespaceURI!=`string`||typeof e.insertBefore!=`function`||typeof e.hasChildNodes!=`function`||e.nodeType!==y(e)||e.childNodes!==h(e)},_t=function(e){if(!y||typeof e!=`object`||!e)return!1;try{return y(e)===Ti.documentFragment}catch{return!1}},vt=function(e){if(!y||typeof e!=`object`||!e)return!1;try{return typeof y(e)==`number`}catch{return!1}};function yt(e,n,r){e.length!==0&&Or(e,e=>{e.call(t,n,r,G)})}let bt=function(e,t){return!!(F&&e.hasChildNodes()&&!vt(e.firstElementChild)&&Kr(xi,e.textContent)&&Kr(xi,e.innerHTML)||F&&e.namespaceURI===We&&Di[t]&&(vt(e.firstElementChild)||typeof e.textContent==`string`&&Kr(Oi[t],e.textContent))||e.nodeType===Ti.processingInstruction||F&&e.nodeType===Ti.comment&&Kr(Si,e.data))},xt=function(e,t){return e instanceof RegExp?Kr(e,t):e instanceof Function&&!!e(t,...[...arguments].slice(2))},St=function(e,t,n){if(!j[t]&&Ot(t)&&xt(Se.tagNameCheck,t))return!1;if(R&&!Le[t]){let t=g(e),r=h(e);if(r&&t){let i=r.length;for(let a=i-1;a>=0;--a){let i=e===n?f(r[a],!0):r[a];t.insertBefore(i,m(e))}}}return ot(e),!0},Ct=function(e,t,n,r){return e.length===0?t:t===n||t===r?Xr(t):t},wt=function(e,t){return e===t||g(e)!==null?!1:(z&&ut(e),!0)},Tt=function(e,n){if(yt(O.beforeSanitizeElements,e,null),wt(e,n))return!0;if(Y(e))return ot(e),!0;let r=W(C(e));if(k=Ct(O.uponSanitizeElement,k,be,Oe),yt(O.uponSanitizeElement,e,{tagName:r,allowedTags:k}),wt(e,n))return!0;if(bt(e,r))return ot(e),!0;if(j[r]||!(we.tagCheck instanceof Function&&we.tagCheck(r))&&!k[r]){let t=St(e,r,n);return t===!1&&yt(O.afterSanitizeElements,e,null),t}if(S(e)===Ti.element&&!at(e)||(r===`noscript`||r===`noembed`||r===`noframes`)&&Kr(Ci,e.innerHTML))return ot(e),!0;if(Ee&&e.nodeType===Ti.text){let n=ht(e.textContent);e.textContent!==n&&(jr(t.removed,{element:e.cloneNode()}),e.textContent=n)}return yt(O.afterSanitizeElements,e,null),!1},Et=function(e,t,r){if(Ce[t]||dt(t,e)||Ne&&(t===`id`||t===`name`)&&(r in n||r in $e))return!1;let i=A[t]||we.attributeCheck instanceof Function&&we.attributeCheck(t,e);return N&&Kr(me,t)||M&&Kr(he,t)?!0:i?Be[t]||Kr(ye,Lr(r,_e,``))||(t===`src`||t===`xlink:href`||t===`href`)&&e!==`script`&&Rr(r,`data:`)===0&&B[e]||P&&!Kr(ge,Lr(r,_e,``))?!0:!r:Ot(e)&&xt(Se.tagNameCheck,e)&&xt(Se.attributeNameCheck,t,e)||t===`is`&&Se.allowCustomizedBuiltInElements&&xt(Se.tagNameCheck,r)},Dt=Q({},[`annotation-xml`,`color-profile`,`font-face`,`font-face-format`,`font-face-name`,`font-face-src`,`font-face-uri`,`missing-glyph`]),Ot=function(e){return!Dt[Pr(e)]&&Kr(ve,e)},kt=function(e,t,n,r){if(w&&typeof u==`object`&&typeof u.getAttributeType==`function`&&!n)switch(u.getAttributeType(e,t)){case`TrustedHTML`:return ne(r);case`TrustedScriptURL`:return re(r)}return r},At=function(e,n,r,i){try{r?e.setAttributeNS(r,n,i):e.setAttribute(n,i),Y(e)?ot(e):Ar(t.removed)}catch{ct(n,e)}},jt=function(e){yt(O.beforeSanitizeAttributes,e,null);let t=e.attributes;if(!t||Y(e))return;A=Ct(O.uponSanitizeAttribute,A,xe,ke);let n={attrName:``,attrValue:``,keepAttr:!0,allowedAttributes:A,forceKeepAttr:void 0},r=t.length,i=W(e.nodeName);for(;r--;){let a=t[r],o=a.name,s=a.namespaceURI,c=a.value,l=W(o),u=c,d=o===`value`?u:zr(u);if(n.attrName=l,n.attrValue=d,n.keepAttr=!0,n.forceKeepAttr=void 0,yt(O.uponSanitizeAttribute,e,n),d=n.attrValue,Pe&&(l===`id`||l===`name`)&&Rr(d,Fe)!==0&&(ct(o,e,a),d=Fe+d),F&&Kr(/((--!?|])>)|<\/(style|script|title|xmp|textarea|noscript|iframe|noembed|noframes)/i,d)){ct(o,e,a);continue}if(l===`attributename`&&Ir(d,`href`)){ct(o,e,a);continue}if(!n.forceKeepAttr){if(!n.keepAttr){ct(o,e,a);continue}if(!Te&&Kr(wi,d)){ct(o,e,a);continue}if(Ee&&(d=ht(d)),!Et(i,l,d)){ct(o,e,a);continue}d=kt(i,l,s,d),d!==u&&At(e,o,s,d)}}yt(O.afterSanitizeAttributes,e,null)},Mt=function(e){let t=null,n=mt(e);for(yt(O.beforeSanitizeShadowDOM,e,null);t=n.nextNode();)if(yt(O.uponSanitizeShadowNode,t,null),Tt(t,e),jt(t),_t(t.content)&&Mt(t.content),S(t)===Ti.element){let e=_(t);_t(e)&&(Nt(e),Mt(e))}yt(O.afterSanitizeShadowDOM,e,null)},Nt=function(e){let t=[{node:e,shadow:null}];for(;t.length>0;){let e=t.pop();if(e.shadow){Mt(e.shadow);continue}let n=e.node,r=S(n)===Ti.element,i=h(n);if(i)for(let e=i.length-1;e>=0;--e)t.push({node:i[e],shadow:null});if(r){let e=b?b(n):null;if(typeof e==`string`&&W(e)===`template`){let e=n.content;_t(e)&&t.push({node:e,shadow:null})}}if(r){let e=_(n);_t(e)&&t.push({node:null,shadow:e},{node:e,shadow:null})}}};return t.sanitize=function(e){let n=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{},i=null,a=null,o=null,s=null;if(Ge=!e,Ge&&(e=``),typeof e!=`string`&&!vt(e)&&(e=Zr(e),typeof e!=`string`))throw qr(`dirty is not a string, aborting`);if(!t.isSupported)return e;I?(k=Oe,A=ke):K(n),(O.uponSanitizeElement.length>0||O.uponSanitizeAttribute.length>0)&&(k=Xr(k)),O.uponSanitizeAttribute.length>0&&(A=Xr(A)),t.removed=[];let c=z&&typeof e!=`string`&&vt(e);if(c){ft(e);let t=C(e);if(typeof t==`string`){let n=W(t);if(!k[n]||j[n])throw J(e),qr(`root node is forbidden and cannot be sanitized in-place`)}if(Y(e))throw J(e),qr(`root node is clobbered and cannot be sanitized in-place`);try{Nt(e)}catch(t){throw J(e),t}}else if(vt(e))i=pt(``),a=i.ownerDocument.importNode(e,!0),a.nodeType===Ti.element&&a.nodeName===`BODY`||a.nodeName===`HTML`?i=a:i.appendChild(a),Nt(a);else{if(!je&&!Ee&&!De&&e.indexOf(`<`)===-1)return w&&L?ne(e):e;if(i=pt(e),!i)return je?null:L?T:``}i&&Ae&&ot(i.firstChild);let l=c?e:i;try{let e=mt(l);for(;o=e.nextNode();)Tt(o,l),jt(o),_t(o.content)&&Mt(o.content)}catch(n){throw c&&(J(e),Or(t.removed,e=>{e.element&&ut(e.element)})),n}if(c)return Or(t.removed,e=>{e.element&&ut(e.element)}),Ee&>(e),e;if(je){if(Ee&>(i),Me)for(s=ce.call(i.ownerDocument);i.firstChild;)s.appendChild(i.firstChild);else s=i;return(A.shadowroot||A.shadowrootmode)&&(s=ue.call(r,s,!0)),s}let u=De?i.outerHTML:i.innerHTML;return De&&k[`!doctype`]&&i.ownerDocument&&i.ownerDocument.doctype&&i.ownerDocument.doctype.name&&Kr(yi,i.ownerDocument.doctype.name)&&(u=` +-`+u),Ee&&(u=ht(u)),w&&L?ne(u):u},t.setConfig=function(){let e=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{};K(e),I=!0,Oe=k,ke=A},t.clearConfig=function(){G=null,I=!1,Oe=null,ke=null,w=E,T=``},t.isValidAttribute=function(e,t,n){G||K({});let r=W(e),i=W(t);return Et(r,i,n)},t.addHook=function(e,t){typeof t==`function`&&Wr(O,e)&&jr(O[e],t)},t.removeHook=function(e,t){if(Wr(O,e)){if(t!==void 0){let n=kr(O[e],t);return n===-1?void 0:Mr(O[e],n,1)[0]}return Ar(O[e])}},t.removeHooks=function(e){Wr(O,e)&&(O[e]=[])},t.removeAllHooks=function(){O=ji()},t}var Fi=Pi(),Ii=new Map,Li=Fi();async function Ri(e,t){let n=`${e}:${t}`;if(Ii.has(n))return Ii.get(n);let r=i().then(e=>(Ii.set(n,e),e)).catch(e=>{throw Ii.delete(n),e});return Ii.set(n,r),r;async function i(){let n=`https://api.iconify.design/${e}/${t}.svg?color=currentColor&width=100%`,r=await fetch(n,{signal:AbortSignal.timeout(1e4)}).then(e=>e.text());return Li.sanitize(r)}}var zi=[`innerHTML`],Bi=[`src`],Vi=J({__name:`IconifyIcon`,props:{icon:{}},setup(e){let t=e,n=N(()=>t.icon.includes(`/`)||t.icon.startsWith(`data:`)||t.icon.startsWith(`builtin:`)),r=N(()=>{if(n.value)return;let e=t.icon.match(/^(?:i-)?([\w-]+):([\w-]+)$/);if(e)return{collection:e[1],icon:e[2]}}),i=B(void 0);return $e(async()=>{if(!r.value){i.value=void 0;return}try{i.value=await Ri(r.value.collection,r.value.icon)}catch{i.value=void 0}}),(t,n)=>r.value?(G(),j(`div`,{key:0,innerHTML:i.value},null,8,zi)):(G(),j(`img`,{key:1,src:e.icon,class:`w-full h-full m-auto`,draggable:`false`},null,8,Bi))}}),Hi={key:1,class:`flex items-center justify-center`},Ui=J({__name:`DockIcon`,props:{icon:{},title:{}},setup(e){let t=e,n=N(()=>typeof t.icon==`string`?{dark:t.icon,light:t.icon}:t.icon);return(t,r)=>n.value.light===`builtin:devframes`?(G(),F(ur,{key:0})):(G(),j(`div`,Hi,[n.value.light===n.value.dark?(G(),F(Vi,{key:0,icon:n.value.light,title:e.title},null,8,[`icon`,`title`])):(G(),j(H,{key:1},[I(Vi,{class:`dark-hidden`,icon:n.value.light,title:e.title},null,8,[`icon`,`title`]),I(Vi,{class:`light-hidden`,icon:n.value.dark,title:e.title},null,8,[`icon`,`title`])],64))]))}}),Wi=[`id`],Gi={class:`flex items-center gap-2 flex-1 of-hidden min-w-0`},Ki={class:`flex-1 min-w-0`},qi={class:`flex items-center gap-1.5`},Ji={class:`truncate`},Yi={key:0,class:`op50`},Xi={key:0,class:`text-[10px] px-1 py-0 rounded bg-blue/10 text-blue shrink-0 leading-4`},Zi={key:0,class:`truncate text-xs op40 mt-0.5`},Qi={class:`flex items-center gap-1.5 flex-none`},$i={key:0,class:`i-ph-spinner-gap-duotone w-3.5 h-3.5 animate-spin op50`},ea={key:1,class:`i-ph-caret-right w-3 h-3 op40`},ta=J({__name:`CommandPaletteItem`,props:{entry:{},parentTitle:{},showParentTitle:{type:Boolean},selected:{type:Boolean},loading:{type:Boolean},keybindings:{}},emits:[`select`,`activate`],setup(e){return(t,n)=>(G(),j(`button`,{id:`cmd-${e.entry.id}`,class:`w-full text-left`,onClick:n[0]||=e=>t.$emit(`activate`),onMouseover:n[1]||=e=>t.$emit(`select`)},[P(`div`,{class:V([`flex items-center gap-2 justify-between rounded-md px-2.5 py-1.5 text-sm transition-colors`,e.selected?`bg-primary/10 text-primary`:`op80 hover:op100`])},[P(`div`,Gi,[e.entry.icon?(G(),F(Ui,{key:0,icon:e.entry.icon,class:`w-4 h-4 flex-none op70`},null,8,[`icon`])):A(``,!0),P(`div`,Ki,[P(`div`,qi,[P(`span`,Ji,[e.parentTitle&&e.showParentTitle?(G(),j(`span`,Yi,q(e.parentTitle)+` › `,1)):A(``,!0),M(` `+q(e.entry.title),1)]),e.entry.source===`server`?(G(),j(`span`,Xi,`server`)):A(``,!0)]),e.selected&&e.entry.description?(G(),j(`div`,Zi,q(e.entry.description),1)):A(``,!0)])]),P(`div`,Qi,[(G(!0),j(H,null,L(e.keybindings,(e,t)=>(G(),F(sr,{key:t,keys:e.key},null,8,[`keys`]))),128)),e.loading?(G(),j(`span`,$i)):e.entry.children?.length?(G(),j(`span`,ea)):A(``,!0)])],2)],40,Wi))}}),na={class:`absolute inset-0 flex items-start justify-center relative pt-[20vh] pointer-events-none`},ra={class:`w-full w-lg bg-glass:80 color-base border border-base rounded-lg shadow-xl pointer-events-auto of-hidden flex flex-col max-h-[60vh]`},ia={class:`border-b border-base flex items-center px-3`},aa=[`onClick`],oa={key:0,class:`py-8 flex flex-col items-center justify-center gap-2 op50 text-sm`},sa={key:0},ca={class:`text-primary op100`},la={key:1},ua={class:`border-t border-base flex items-center justify-between gap-4 px-3 py-1.5 text-[10px] op50`},da={class:`flex items-center gap-1.5`},fa=J({__name:`CommandPalette`,props:{context:{}},setup(e){let t=e,n=N(()=>t.context.commands),i=N({get:()=>n.value.paletteOpen,set:e=>{n.value.paletteOpen=e}}),a=B(``),o=B(0),s=K(`searchInput`),c=K(`listContainer`),l=B(!1),u=B([]),d=N(()=>x(n.value.paletteCommands)),f=N(()=>u.value.length>0?u.value.at(-1).items.map(e=>({entry:e,searchTitle:e.title})):d.value),p=B(),m=N(()=>p.value?p.value.map(e=>({entry:e,searchTitle:e.title})):f.value),_=N(()=>new zn(m.value,{keys:[`searchTitle`,`entry.description`,`entry.id`],distance:50,threshold:.4})),v=N(()=>a.value?_.value.search(a.value).map(e=>e.item):m.value);Xe(a,()=>{o.value=0});function y(e){a.value=``,o.value=0,p.value=void 0;let t=h(n.value.paletteCommands,e);u.value=t,e!=null&&!t.some(t=>t.id===e)&&(n.value.paletteScopeId=null)}Xe(i,e=>{e?(y(n.value.paletteScopeId),requestAnimationFrame(()=>{l.value=!0}),Ae(()=>s.value?.focus())):(l.value=!1,n.value.paletteScopeId=null)}),Xe(()=>n.value.paletteScopeId,e=>{i.value&&y(e)}),Xe(()=>n.value.paletteCommands,e=>{if(!i.value||u.value.length===0)return;let t=n.value.paletteScopeId,r=t!=null&&u.value.some(e=>e.id===t),a=fe(e,u.value,t);u.value=a,o.value=Math.min(o.value,Math.max(v.value.length-1,0)),r&&t!=null&&!a.some(e=>e.id===t)&&(n.value.paletteScopeId=null)});function b(e){let t=v.value.length;t!==0&&(o.value=(o.value+e+t)%t,S())}function S(){let e=v.value[o.value];e&&((c.value?.getRootNode())?.getElementById(`cmd-${e.entry.id}`))?.scrollIntoView({block:`nearest`})}let C=B(null);async function w(e){let r=m.value.find(t=>t.entry.id===e.entry.id)?.entry;if(r){if(g(r,t.context.docks.entries)===`drill`){n.value.paletteScopeId=r.id;return}if(r.source===`client`&&r.action){try{let e=await r.action();if(Array.isArray(e)){p.value=e,a.value=``,o.value=0;return}}catch(e){console.error(`[@devframes/hub-ui] Command "${r.id}" failed:`,e)}if(pe(r,t.context.docks.entries,n.value.paletteOpen,n.value.paletteScopeId))return;T();return}if(r.source===`server`){C.value=r.id;try{await n.value.execute(r.id)}catch(e){console.error(`[@devframes/hub-ui] Command "${r.id}" failed:`,e)}finally{C.value=null}T();return}T()}}function T(){l.value=!1,setTimeout(()=>{i.value=!1,a.value=``,u.value=[],p.value=void 0},150)}function E(){if(p.value){p.value=void 0,a.value=``,o.value=0;return}if(u.value.length>0){u.value.pop(),n.value.paletteScopeId=r(u.value),a.value=``,o.value=0;return}T()}function ee(e){u.value.splice(e),n.value.paletteScopeId=r(u.value),a.value=``,o.value=0}function te(e){e.key===`Backspace`&&!a.value&&(u.value.length>0||p.value)&&(e.preventDefault(),E())}function D(e){if(i.value)switch(e.key){case`ArrowDown`:case`ArrowUp`:e.preventDefault(),b(e.key===`ArrowDown`?1:-1);break;case`Enter`:{let t=v.value[o.value];t&&(e.preventDefault(),w(t));break}case`Escape`:e.preventDefault(),u.value.length>0||p.value?E():T()}}function ne(e){return n.value.getKeybindings(e)}return(e,t)=>i.value?(G(),j(`div`,{key:0,class:`devframes-command-palette fixed inset-0 z-command-palette`,onKeydown:D},[P(`div`,{class:V([`absolute inset-0 bg-white/50 dark:bg-black/30 transition-opacity duration-150`,l.value?`opacity-100 backdrop-blur-1`:`opacity-0 backdrop-blur-0`]),onClick:T},null,2),P(`div`,na,[P(`div`,{class:V([`flex flex-col transition-all duration-150`,l.value?`opacity-100 scale-100 translate-y-0`:`opacity-0 scale-98 -translate-y-2`])},[I(qn,{class:`absolute top--32px left-5px pointer-events-none`}),P(`div`,ra,[P(`header`,ia,[u.value.length>0?(G(!0),j(H,{key:0},L(u.value,(e,n)=>(G(),j(`button`,{key:n,class:`text-xs op60 hover:op80 mr-1 flex items-center gap-0.5`,onClick:e=>ee(n)},[M(q(e.title)+` `,1),t[1]||=P(`span`,{class:`op40`},`›`,-1)],8,aa))),128)):A(``,!0),Ze(P(`input`,{ref_key:`searchInput`,ref:s,"onUpdate:modelValue":t[0]||=e=>a.value=e,class:`flex-1 bg-transparent py-3 outline-none text-sm color-base`,placeholder:`Type a command...`,onKeydown:te},null,544),[[ke,a.value]])]),P(`div`,{ref_key:`listContainer`,ref:c,class:`flex-1 of-y-auto p-1.5`},[(G(!0),j(H,null,L(v.value,(e,t)=>(G(),F(ta,{key:e.entry.id,entry:e.entry,"parent-title":e.parentTitle,"show-parent-title":!u.value.length,selected:o.value===t,loading:C.value===e.entry.id,keybindings:ne(e.entry.id),onSelect:e=>o.value=t,onActivate:t=>w(e)},null,8,[`entry`,`parent-title`,`show-parent-title`,`selected`,`loading`,`keybindings`,`onSelect`,`onActivate`]))),128)),v.value.length?A(``,!0):(G(),j(`div`,oa,[t[4]||=P(`div`,{class:`i-ph-magnifying-glass-duotone w-6 h-6`},null,-1),a.value?(G(),j(`div`,sa,[t[2]||=M(` No results for "`,-1),P(`strong`,ca,q(a.value),1),t[3]||=M(`" `,-1)])):(G(),j(`div`,la,` No commands available `))]))],512),P(`footer`,ua,[t[6]||=P(`div`,{class:`flex items-center gap-1.5`},[P(`kbd`,{class:`px-1 py-0.5 rounded border border-base bg-base font-mono`},`↓↑`),P(`span`,null,`navigate`)],-1),P(`div`,da,[t[5]||=P(`kbd`,{class:`px-1 py-0.5 rounded border border-base bg-base font-mono`},`esc`,-1),P(`span`,null,q(u.value.length>0||p.value?`back`:`close`),1)]),t[7]||=P(`div`,{class:`flex items-center gap-1.5`},[P(`kbd`,{class:`px-1 py-0.5 rounded border border-base bg-base font-mono`},`↵`),P(`span`,null,`select`)],-1)])])],2)])],32)):A(``,!0)}}),pa=J({__name:`FeedbackSpinner`,props:{size:{default:`1em`}},setup(e){let t=e,n=N(()=>typeof t.size==`number`?`${t.size}px`:t.size);return(e,t)=>(G(),j(`span`,{class:`border-2 border-current border-t-transparent rounded-full inline-block animate-spin`,style:R({width:n.value,height:n.value}),role:`status`,"aria-label":`Loading`},null,4))}}),ma=J({__name:`ActionButton`,props:{as:{},to:{},href:{},icon:{},variant:{default:`action`},size:{default:`md`},loading:{type:Boolean},disabled:{type:Boolean}},setup(e){let t=e,n=N(()=>t.as?t.as:t.href!=null||t.to!=null?`a`:`button`),r=N(()=>n.value===`a`),i=N(()=>n.value===`button`),a=N(()=>t.size===`sm`),o=N(()=>t.variant===`primary`?a.value?`btn-primary text-sm`:`btn-primary`:t.variant===`text`?a.value?`btn-text text-sm`:`btn-text`:a.value?`btn-action-sm`:`btn-action`),s=N(()=>t.disabled||t.loading);return(t,a)=>(G(),F(he(n.value),{class:V([o.value,{"pointer-events-none op-mute":s.value&&!i.value}]),href:r.value?e.href??e.to:void 0,to:e.as&&e.to!=null?e.to:void 0,disabled:i.value?s.value:void 0,"aria-disabled":s.value||void 0,"aria-busy":e.loading||void 0},{default:z(()=>[e.loading?(G(),F(pa,{key:0,size:`1em`})):e.icon?(G(),j(`span`,{key:1,class:V(e.icon),"aria-hidden":`true`},null,2)):A(``,!0),Pe(t.$slots,`default`)]),_:3},8,[`class`,`href`,`to`,`disabled`,`aria-disabled`,`aria-busy`]))}}),ha=Ye({singleton:!0});function ga(){return ha.start}var _a=[`aria-labelledby`,`onKeydown`],va=[`onClick`],ya={class:`relative w-full max-w-96 bg-base border border-base rounded-lg shadow-xl p-5`},ba={key:0,id:`devframes-confirm-title`,class:`text-sm font-medium leading-5`},xa={class:`flex items-center justify-end gap-2 mt-6`},Sa=J({__name:`Confirm`,setup(e){let t=K(`confirmButton`);Ge(()=>{(t.value?.$el)?.focus({preventScroll:!0})});function n(e,t){e(t)}return(e,r)=>(G(),F(U(ha),null,{default:z(({resolve:e,args:[r]})=>[P(`div`,{class:`devframes-confirm fixed inset-0 z-dock-confirm flex items-center justify-center p-4 color-base`,role:`dialog`,"aria-modal":`true`,"aria-labelledby":r.title?`devframes-confirm-title`:void 0,"aria-describedby":`devframes-confirm-message`,onKeydown:ye(Te(t=>n(e,!1),[`prevent`,`stop`]),[`esc`])},[P(`div`,{class:`absolute inset-0 bg-black/30 dark:bg-black/45 backdrop-blur-1 cursor-default`,"aria-hidden":`true`,onClick:t=>n(e,!1)},null,8,va),P(`div`,ya,[r.title?(G(),j(`h3`,ba,q(r.title),1)):A(``,!0),P(`p`,{id:`devframes-confirm-message`,class:V([`text-xs op60 leading-5`,r.title?`mt-1.5`:``])},q(r.message),3),P(`div`,xa,[I(ma,{variant:`text`,size:`sm`,onClick:t=>n(e,!1)},{default:z(()=>[M(q(r.cancelText??`Cancel`),1)]),_:2},1032,[`onClick`]),I(ma,{ref_key:`confirmButton`,ref:t,variant:`primary`,size:`sm`,onClick:t=>n(e,!0)},{default:z(()=>[M(q(r.confirmText??`OK`),1)]),_:2},1032,[`onClick`])])])],40,_a)]),_:1}))}}),Ca=nt([]),wa=new Map;function Ta(){return Ca}function Ea(e){let t=Ca.find(t=>t.id===e.id);if(t){t.entry=e;let n=wa.get(e.id);n&&(clearTimeout(n),wa.delete(e.id));let r=e.autoDismiss??5e3;r!==!1&&wa.set(e.id,setTimeout(Da,r,e.id));return}let n={id:e.id,entry:e};Ca.push(n);let r=e.autoDismiss??5e3;r!==!1&&wa.set(e.id,setTimeout(Da,r,e.id))}function Da(e){let t=Ca.findIndex(t=>t.id===e);t!==-1&&Ca.splice(t,1);let n=wa.get(e);n&&(clearTimeout(n),wa.delete(e))}function Oa(e,t,n){return e.notify?t?JSON.stringify(e)!==JSON.stringify(t):!n||e.status===`loading`:!1}var ka;function Aa(e){if(ka)return ka;let t=ka=nt({entries:[],unreadCount:0,pendingSelectId:null}),n=new Map,r=!0,i=null;async function a(){let a=await e.rpc.call(`devframes:plugin:messages:list`,i),o=0;a.full&&n.clear();for(let e of a.removedIds)n.delete(e);for(let e of a.entries){let t=n.get(e.id);t||o++,Oa(e,t,r)&&Ea(e),n.set(e.id,e)}t.entries=Array.from(n.values()),t.unreadCount+=o,i=a.version,r=!1}let o=()=>a().catch(()=>{});return e.rpc.client.register({name:`devframe:messages:updated`,type:`action`,handler:()=>{e.rpc.isTrusted&&o()}}),e.rpc.ensureTrusted().then(()=>o()),t}function ja(e){ka&&(ka.pendingSelectId=e)}var Ma={info:{icon:`i-ph:info-duotone`,color:`text-blue`,bg:`bg-blue`,label:`Info`},warn:{icon:`i-ph:warning-duotone`,color:`text-amber`,bg:`bg-amber`,label:`Warning`},error:{icon:`i-ph:x-circle-duotone`,color:`text-red`,bg:`bg-red`,label:`Error`},success:{icon:`i-ph:check-circle-duotone`,color:`text-green`,bg:`bg-green`,label:`Success`},debug:{icon:`i-ph:bug-duotone`,color:`text-gray`,bg:`bg-gray`,label:`Debug`}};function Na(e,t=1){let n=0;for(let t=0;t({color:Na(t.label),backgroundColor:Na(t.label,.1)}));return(t,r)=>(G(),j(`span`,{class:`text-xs px-1 rounded`,style:R(n.value)},q(e.label),5))}}),Fa={class:`flex items-start gap-2 relative`},Ia={key:0,class:`flex-none mt-0.5 border-2 border-current border-t-transparent rounded-full animate-spin op50 w-4 h-4`},La={class:`flex-1 min-w-0 space-y-0.5`},Ra={class:`flex items-center gap-2`},za=[`title`],Ba={key:0,class:`text-xs op80 whitespace-pre-wrap`},Va={key:1,class:`flex flex-wrap items-center gap-x-2 gap-y-0.5`},Ha=J({__name:`MessageItem`,props:{entry:{},compact:{type:Boolean}},setup(e){let t=e,n=rt(()=>t.entry.timestamp);return(t,r)=>(G(),j(`div`,Fa,[P(`div`,{class:V([`w-2px flex-none absolute left-0 top-4px bottom-4px rounded-r`,[U(Ma)[e.entry.level]?.bg||`bg-gray`]])},null,2),e.entry.status===`loading`?(G(),j(`div`,Ia)):(G(),j(`div`,{key:1,class:V([`flex-none mt-0.5 w-4 h-4`,[U(Ma)[e.entry.level]?.icon,U(Ma)[e.entry.level]?.color]])},null,2)),P(`div`,La,[P(`div`,Ra,[P(`div`,{class:V([`flex-1 min-w-0 truncate text-sm font-medium`,[e.entry.status===`loading`?`op60`:``]])},q(e.entry.message),3),e.compact?A(``,!0):(G(),j(`span`,{key:0,class:`text-xs op40 flex-none`,title:new Date(e.entry.timestamp).toLocaleString()},q(U(n)),9,za)),Pe(t.$slots,`actions`)]),e.entry.description?(G(),j(`div`,Ba,q(e.entry.description),1)):A(``,!0),e.compact?A(``,!0):(G(),j(`div`,Va,[e.entry.category?(G(),F(Pa,{key:0,label:e.entry.category},null,8,[`label`])):A(``,!0),(G(!0),j(H,null,L(e.entry.labels,e=>(G(),F(Pa,{key:e,label:e},null,8,[`label`]))),128))]))])]))}}),Ua={key:0,class:`fixed bottom-4 right-4 z-dock-toast flex flex-col gap-2 pointer-events-auto w-72`},Wa=[`onClick`],Ga=[`onClick`],Ka=[`onClick`],qa=J({__name:`ToastOverlay`,props:{context:{}},setup(e){let t=e;t.context&&Aa(t.context);let n=Ta();function r(e){Da(e),ja(e),t.context?.docks.switchEntry(y)}function i(e){e.kind===`activate`?t.context?.docks.switchEntry(e.activate.dockId):e.kind===`command`&&t.context?.commands.execute(e.command.id,...e.command.params??[])}return(e,t)=>U(n).length>0?(G(),j(`div`,Ua,[I(at,{"enter-active-class":`transition-all duration-300 ease-out`,"leave-active-class":`transition-all duration-200 ease-in`,"enter-from-class":`opacity-0 translate-x-4`,"leave-to-class":`opacity-0 translate-x-4`},{default:z(()=>[(G(!0),j(H,null,L(U(n),e=>(G(),j(`div`,{key:e.id,class:`bg-toast-glass border color-base border-base shadow-xl cursor-pointer transition-colors rounded`,onClick:t=>r(e.id)},[I(Ha,{entry:e.entry,compact:``,class:`px-3 py-2.5`},{actions:z(()=>[(G(!0),j(H,null,L(e.entry.actions,e=>(G(),j(`button`,{key:e.id,class:`flex-none text-xs px-1.5 py-0.5 rounded border border-base op70 hover:op100 hover:bg-active transition`,onClick:Te(t=>i(e),[`stop`])},q(e.label),9,Ga))),128)),P(`button`,{class:`flex-none op30 hover:op100 p-0.5 rounded hover:bg-active transition`,onClick:Te(t=>U(Da)(e.id),[`stop`])},[...t[0]||=[P(`div`,{class:`i-ph-x w-3 h-3`},null,-1)]],8,Ka)]),_:2},1032,[`entry`])],8,Wa))),128))]),_:1})])):A(``,!0)}}),Ja=100,Ya=10,Xa=8;function Za(e,t,n){return Math.min(Math.max(e,t),Math.max(n,t))}function Qa({rect:e,anchorRight:t,anchorBottom:n,vw:r,vh:i}){return e.leftr-Ja?`left`:e.topi-Ja?`top`:`bottom`}function $a(e,t){let{rect:n,anchorRight:r,anchorBottom:i,vw:a,vh:o,gap:s,panelWidth:c,panelHeight:l}=t;return e===`bottom`&&i+s+l>o-Xa&&n.top-s-l>=Xa?`top`:e===`top`&&n.top-s-la-Xa&&n.left-s-c>=Xa?`left`:e===`left`&&n.left-s-ce[t]!==`none`)||/paint|layout|strict|content/.test(e.contain))&&(t=n)}return t?.parentElement??void 0}var io=J({name:`FloatingPopover`,props:{item:{type:Object,required:!1},dismissOnClickOutside:{type:Boolean,default:!0},panelClass:{type:[String,Array],required:!1},ignore:{type:Array,required:!1}},emits:[`dismiss`],setup(e,{emit:t}){let n=K(`panel`),r=B(e.item?.el),i=B(0),a=B();function o(e){a.value=e?ro(e):void 0}let s=nt({width:0,height:0}),c=B(!1);function l(){if(!e.item||!n.value)return;let{width:t,height:r}=n.value.getBoundingClientRect();(Math.abs(t-s.width)>.5||Math.abs(r-s.height)>.5)&&(s.width=t,s.height=r),Ae(()=>{c.value=!0})}Me(()=>{o(e.item?.el),l()}),we(l),Fe(window,`resize`,()=>{r.value&&i.value++}),Fe(window,`scroll`,()=>{r.value&&i.value++},{capture:!0,passive:!0});let u=st(()=>{e.item?.el??(r.value=void 0,s.width=0,s.height=0,c.value=!1)},800);e.dismissOnClickOutside&&We(n,()=>{t(`dismiss`)},{ignore:e.ignore}),Xe(()=>e.item,e=>{e?(r.value===e.el?i.value++:r.value=e.el,o(e.el)):u()});let d,f={},p=e=>a.value?je(Ke,{to:a.value},[e]):e;return()=>{if(i.value,!r.value)return null;let t=c.value?`transition-all duration-300`:`transition-opacity duration-300`;if(!e.item)return p(je(`div`,{ref:`panel`,class:[`fixed z-floating-tooltip text-xs ${t} w-max bg-glass:80 color-base border border-base rounded px2 p1`,`op0 pointer-events-none`,e.panelClass],style:f},d));let{style:n}=to({rect:r.value.getBoundingClientRect(),viewportWidth:window.innerWidth,viewportHeight:window.innerHeight,panelWidth:s.width,panelHeight:s.height,gap:e.item.gap,placement:e.item.placement});f=n;let a=(typeof e.item?.content==`string`?je(`span`,e.item?.content):e.item?.content())??d;return d=a,p(je(`div`,{ref:`panel`,class:[`fixed z-floating-tooltip text-xs ${t} w-max bg-glass:80 color-base border border-base rounded px2 p1`,e.item?`op100`:`op0 pointer-events-none`,e.panelClass],style:n},a))}}}),ao=J({__name:`FloatingElements`,setup(e){let t=de(),n=E(),r=T(),i=ne(),a=ae(),o=b();return Fe(window,`keydown`,e=>{e.key===`Escape`&&a.value&&le(null)}),(e,s)=>(G(),j(H,null,[I(U(io),{item:U(n),onDismiss:s[0]||=()=>U(D)(null)},null,8,[`item`]),I(U(io),{item:U(r),onDismiss:s[1]||=()=>U(O)(null)},null,8,[`item`]),I(U(io),{item:U(i),onDismiss:s[2]||=()=>U(S)(null)},null,8,[`item`]),I(U(io),{item:U(a),onDismiss:s[3]||=()=>U(le)(null)},null,8,[`item`]),I(U(io),{item:U(o),onDismiss:s[4]||=()=>U(C)(null)},null,8,[`item`]),I(U(io),{item:U(t)},null,8,[`item`])],64))}}),oo=[`aria-label`],so=[`value`,`autofocus`,`aria-label`,`aria-invalid`,`disabled`,`onInput`,`onKeydown`,`onPaste`],co=J({__name:`OtpInput`,props:{modelValue:{},length:{default:6},disabled:{type:Boolean,default:!1},invalid:{type:Boolean,default:!1},autofocus:{type:Boolean,default:!0},label:{default:`One-time authorization code`}},emits:[`update:modelValue`,`complete`],setup(e,{expose:t,emit:n}){let r=e,i=n,a=B([]),o=N(()=>Array.from({length:r.length},(e,t)=>r.modelValue[t]??``));function s(e,t){e&&(a.value[t]=e)}function c(e){let t=Math.max(0,Math.min(r.length-1,e));Ae(()=>{let e=a.value[t];e?.focus(),e?.select()})}function l(e){let t=e.replace(/\D/g,``).slice(0,r.length);t!==r.modelValue&&i(`update:modelValue`,t),t.length===r.length&&i(`complete`,t)}function u(e,t){let n=o.value.slice(),i=e;for(let e of t){if(i>=r.length)break;n[i]=e,i++}return l(n.join(``)),i}function d(e,t){let n=t.target.value.replace(/\D/g,``);if(!n){let t=o.value.slice();t[e]=``,l(t.join(``));return}c(u(e,n))}function f(e,t){switch(t.key){case`Backspace`:{t.preventDefault();let n=o.value.slice();n[e]?(n[e]=``,l(n.join(``))):e>0&&(n[e-1]=``,l(n.join(``)),c(e-1));break}case`Delete`:{t.preventDefault();let n=o.value.slice();n[e]=``,l(n.join(``));break}case`ArrowLeft`:t.preventDefault(),c(e-1);break;case`ArrowRight`:t.preventDefault(),c(e+1);break;case`Home`:t.preventDefault(),c(0);break;case`End`:t.preventDefault(),c(r.length-1)}}function p(e,t){t.preventDefault();let n=(t.clipboardData?.getData(`text`)??``).replace(/\D/g,``);n&&c(u(e,n.slice(0,r.length-e)))}function m(e){e.target.select()}function h(e){return r.invalid?`border-red-500/70 text-red-500`:o.value[e]?`border-primary-400/60 dark:border-primary-400/50`:`border-base`}return t({focus(){let e=o.value.findIndex(e=>!e);c(e===-1?r.length-1:e)}}),(t,n)=>(G(),j(`div`,{role:`group`,"aria-label":e.label,class:V([`flex items-center justify-center gap-2 sm:gap-2.5`,e.invalid?`devframes-shake`:``])},[(G(!0),j(H,null,L(o.value,(t,n)=>(G(),j(`input`,{key:n,ref_for:!0,ref:e=>s(e,n),value:t,type:`text`,inputmode:`numeric`,autocomplete:`one-time-code`,"data-1p-ignore":``,"data-lpignore":`true`,"data-bwignore":`true`,"data-form-type":`other`,autofocus:e.autofocus&&n===0,"aria-label":`Digit ${n+1} of ${e.length}`,"aria-invalid":e.invalid||void 0,disabled:e.disabled,maxlength:`1`,class:V([`w-11 h-14 text-center text-2xl font-mono tabular-nums font-medium rounded-xl border bg-base color-base caret-primary-500 outline-none transition-all duration-150 focus:border-primary-500 focus:ring-3 focus:ring-primary-500/25 disabled:op40 disabled:pointer-events-none`,h(n)]),onInput:e=>d(n,e),onKeydown:e=>f(n,e),onPaste:e=>p(n,e),onFocus:m},null,42,so))),128))],10,oo))}}),lo={class:`w-full h-full flex items-center justify-center p8 color-base overflow-auto`},uo={class:`w-full max-w-108 flex flex-col items-center text-center`},fo={class:`relative flex items-center justify-center`},po={class:`text-2xl font-bold tracking-tight`},mo={class:`mt2 text-sm op-fade leading-relaxed max-w-92`},ho={class:`mt6 w-full rounded-lg border border-base bg-base/60 shadow-sm p6 flex flex-col items-center gap-4`},go=6,_o=J({__name:`ViewBuiltinClientAuthNotice`,props:{context:{}},setup(e){let t=e,n=qe(),r=B(``),i=B(``),a=B(!1),o=K(`otp`),s=!1;Xe(r,()=>{if(s){s=!1;return}i.value&&=``});async function c(){let e=r.value;if(!(e.length(G(),j(`div`,lo,[P(`div`,uo,[P(`div`,fo,[t[1]||=P(`div`,{class:`absolute w-24 h-24 rounded-full bg-primary-500/20 blur-2xl`,"aria-hidden":`true`},null,-1),I(ur,{class:`relative w-16 h-16`})]),P(`h1`,po,` Authorize `+q(U(n).productName),1),P(`p`,mo,q(U(n).productName)+` can access your server, read your filesystem, and run commands. Confirm it's you before continuing. `,1),P(`div`,ho,[t[2]||=P(`div`,{class:`flex flex-col items-center gap-1`},[P(`p`,{class:`text-sm op-fade`},[M(` Find the `),P(`span`,{class:`font-mono text-primary-600 dark:text-primary-300`},`6-digit code`),M(` printed in your terminal. `)])],-1),P(`form`,{class:`flex flex-col items-center gap-4 w-full`,autocomplete:`off`,onSubmit:Te(c,[`prevent`])},[I(co,{ref_key:`otp`,ref:o,modelValue:r.value,"onUpdate:modelValue":t[0]||=e=>r.value=e,length:go,invalid:!!i.value,disabled:a.value,label:`Enter your one-time authorization code`,onComplete:c},null,8,[`modelValue`,`invalid`,`disabled`]),I(ma,{type:`submit`,variant:`primary`,icon:`i-ph-shield-check-duotone`,class:`w-full justify-center text-base py-2! disabled:saturate-0`,loading:a.value,disabled:r.value.length[M(q(a.value?`Authorizing`:`Authorize`),1)]),_:1},8,[`loading`,`disabled`]),i.value||a.value?(G(),j(`p`,{key:0,class:V([`text-sm min-h-5 transition-colors`,i.value?`text-red-500`:`op-mute`]),role:`alert`,"aria-live":`assertive`},[i.value?(G(),j(H,{key:0},[M(q(i.value),1)],64)):a.value?(G(),j(H,{key:1},[M(` Authorizing... `)],64)):A(``,!0)],2)):A(``,!0)],32)]),t[3]||=P(`p`,{class:`mt4 text-xs op-mute max-w-92 leading-relaxed`},` You can also use the magic link to authorize automatically. `,-1)])]))}}),vo={class:`flex flex-col gap-6`},yo={class:`flex items-center gap-3 cursor-pointer group`},bo=J({__name:`SettingsAdvanced`,props:{context:{}},setup(e){let t=e,n=oe(t.context),r=t.context.docks.settings,i=qe(),a=ga();async function o(){await a({title:`Reset All Settings`,message:`Reset all settings to defaults? This includes appearance, docks, and shortcuts.`})&&r.mutate(()=>be())}async function s(){await a({title:`Reset Keyboard Shortcuts`,message:`Reset all keyboard shortcuts to defaults?`})&&r.mutate(e=>{e.commandShortcuts={}})}async function c(){await a({title:`Reset Dock Settings`,message:`Reset dock visibility, order, and pinning to defaults?`})&&r.mutate(e=>{let t=be();e.docksHidden=t.docksHidden,e.docksCategoriesHidden=t.docksCategoriesHidden,e.docksCustomOrder=t.docksCustomOrder,e.docksPinned=t.docksPinned})}async function l(){await a({title:`Revoke Authorization`,message:`Revoke this browser's access to ${i.value.productName}? You'll need to authorize again with a new code to reconnect.`})&&await t.context.rpc.call(`devframe:auth:revoke`)}return(e,t)=>(G(),j(`div`,vo,[P(`label`,yo,[P(`button`,{class:V([`w-10 h-6 rounded-full transition-colors relative shrink-0`,U(n).showDevframeInspector?`bg-primary`:`bg-gray/30`]),onClick:t[0]||=e=>U(r).mutate(e=>{e.showDevframeInspector=!U(n).showDevframeInspector})},[P(`div`,{class:V([`absolute top-1 w-4 h-4 rounded-full bg-white shadow transition-transform`,U(n).showDevframeInspector?`translate-x-5`:`translate-x-1`])},null,2)],2),t[1]||=P(`div`,{class:`flex flex-col`},[P(`span`,{class:`text-sm`},`Show Devframe Inspector`),P(`span`,{class:`text-xs op50`},`Reveal the experimental Devframe Inspector dock, the devtools for the devtools`)],-1)]),t[10]||=P(`div`,{class:`border-t border-base`},null,-1),P(`div`,{class:`flex items-start gap-4`},[t[3]||=P(`div`,{class:`flex-1`},[P(`div`,{class:`text-sm`},` Reset Keyboard Shortcuts `),P(`div`,{class:`text-xs op50 mt-0.5`},` Remove all custom shortcut overrides and restore default keybindings `)],-1),P(`button`,{class:`px-4 py-2 rounded bg-orange/10 text-orange hover:bg-orange/20 transition-colors flex items-center gap-2 text-sm shrink-0`,onClick:s},[...t[2]||=[P(`div`,{class:`i-ph-keyboard-duotone w-4 h-4`},null,-1),M(` Reset Shortcuts `,-1)]])]),P(`div`,{class:`flex items-start gap-4`},[t[5]||=P(`div`,{class:`flex-1`},[P(`div`,{class:`text-sm`},` Reset Dock Settings `),P(`div`,{class:`text-xs op50 mt-0.5`},` Restore default dock visibility, order, and pinning `)],-1),P(`button`,{class:`px-4 py-2 rounded bg-orange/10 text-orange hover:bg-orange/20 transition-colors flex items-center gap-2 text-sm shrink-0`,onClick:c},[...t[4]||=[P(`div`,{class:`i-ph-layout-duotone w-4 h-4`},null,-1),M(` Reset Docks `,-1)]])]),P(`div`,{class:`border-t border-base pt-6`},[P(`div`,{class:`flex items-start gap-4`},[t[7]||=P(`div`,{class:`flex-1`},[P(`div`,{class:`text-sm`},` Reset All Settings `),P(`div`,{class:`text-xs op50 mt-0.5`},` Reset everything to defaults including appearance, docks, and shortcuts `)],-1),P(`button`,{class:`px-4 py-2 rounded bg-red/10 text-red hover:bg-red/20 transition-colors flex items-center gap-2 text-sm shrink-0`,onClick:o},[...t[6]||=[P(`div`,{class:`i-ph-arrow-counter-clockwise w-4 h-4`},null,-1),M(` Reset All `,-1)]])])]),P(`div`,{class:`border-t border-base pt-6`},[P(`div`,{class:`flex items-start gap-4`},[t[9]||=P(`div`,{class:`flex-1`},[P(`div`,{class:`text-sm`},` Revoke Authorization `),P(`div`,{class:`text-xs op50 mt-0.5`},` De-authorize this browser and revoke its access token; you'll re-authorize with a new code `)],-1),P(`button`,{class:`px-4 py-2 rounded bg-red/10 text-red hover:bg-red/20 transition-colors flex items-center gap-2 text-sm shrink-0`,onClick:l},[...t[8]||=[P(`div`,{class:`i-ph-sign-out-duotone w-4 h-4`},null,-1),M(` Revoke Access `,-1)]])])])]))}}),xo={class:`flex flex-col gap-4`},So={class:`flex flex-col gap-2`},Co={class:`flex flex-col`},wo={class:`text-xs op50`},To={class:`flex items-center gap-1 bg-gray/10 rounded-lg p1 w-fit`},Eo=[`onClick`],Do={key:0,class:`flex flex-col gap-2`},Oo={class:`flex flex-col`},ko={class:`text-xs op50`},Ao={class:`flex items-center gap-1 bg-gray/10 rounded-lg p1 w-fit`},jo=[`onClick`],Mo={class:`flex items-center gap-3 cursor-pointer group`},No={class:`flex items-center gap-3 cursor-pointer group`},Po={class:`flex flex-col`},Fo={class:`text-xs op50`},Io={class:`flex items-center gap-3 cursor-pointer group`},Lo=J({__name:`SettingsAppearance`,props:{context:{}},setup(e){let t=e,n=oe(t.context),r=t.context.docks.settings,i=qe(),a=t.context.panel.store,o=t.context.clientType===`embedded`,s=ie(),c=N(()=>{let e=[{value:`float`,label:`Float`,icon:`i-ph-cards-three-duotone`},{value:`edge`,label:`Edge`,icon:`i-ph-square-half-bottom-duotone`}];return ue()&&e.push({value:`popup`,label:`Popup`,icon:`i-ph-arrow-square-out-duotone`}),e}),l=N(()=>a.mode),u=[{value:`auto`,label:`Auto`,icon:`i-ph-laptop-duotone`},{value:`light`,label:`Light`,icon:`i-ph-sun-duotone`},{value:`dark`,label:`Dark`,icon:`i-ph-moon-duotone`}];function d(e){e===`popup`?se(t.context):a.mode=e}return(e,t)=>(G(),j(`div`,xo,[P(`div`,So,[P(`div`,Co,[t[3]||=P(`span`,{class:`text-sm`},`Color mode`,-1),P(`span`,wo,`Theme for `+q(U(i).productName)+` and its inner panels`,1)]),P(`div`,To,[(G(),j(H,null,L(u,e=>P(`button`,{key:e.value,class:V([`flex items-center gap-1.5 px3 py1.5 rounded-md text-sm transition-all`,U(Re)===e.value?`bg-base shadow text-primary font-medium`:`op60 hover:op100 hover:bg-gray/10`]),onClick:t=>U(Qe)(e.value)},[P(`div`,{class:V([e.icon,`w-4 h-4`])},null,2),M(` `+q(e.label),1)],10,Eo)),64))])]),o&&!U(s)?(G(),j(`div`,Do,[P(`div`,Oo,[t[4]||=P(`span`,{class:`text-sm`},`Dock mode`,-1),P(`span`,ko,`How the `+q(U(i).productName)+` panel is displayed`,1)]),P(`div`,Ao,[(G(!0),j(H,null,L(c.value,e=>(G(),j(`button`,{key:e.value,class:V([`flex items-center gap-1.5 px3 py1.5 rounded-md text-sm transition-all`,l.value===e.value?`bg-base shadow text-primary font-medium`:`op60 hover:op100 hover:bg-gray/10`]),onClick:t=>d(e.value)},[P(`div`,{class:V([e.icon,`w-4 h-4`])},null,2),M(` `+q(e.label),1)],10,jo))),128))])])):A(``,!0),P(`label`,Mo,[P(`button`,{class:V([`w-10 h-6 rounded-full transition-colors relative shrink-0`,U(n).showIframeAddressBar?`bg-primary`:`bg-gray/30`]),onClick:t[0]||=e=>U(r).mutate(e=>{e.showIframeAddressBar=!U(n).showIframeAddressBar})},[P(`div`,{class:V([`absolute top-1 w-4 h-4 rounded-full bg-white shadow transition-transform`,U(n).showIframeAddressBar?`translate-x-5`:`translate-x-1`])},null,2)],2),t[5]||=P(`div`,{class:`flex flex-col`},[P(`span`,{class:`text-sm`},`Always show iframe address bars`),P(`span`,{class:`text-xs op50`},`Show the address bar for every iframe dock.`)],-1)]),P(`label`,No,[P(`button`,{class:V([`w-10 h-6 rounded-full transition-colors relative shrink-0`,U(n).closeOnOutsideClick?`bg-primary`:`bg-gray/30`]),onClick:t[1]||=e=>U(r).mutate(e=>{e.closeOnOutsideClick=!U(n).closeOnOutsideClick})},[P(`div`,{class:V([`absolute top-1 w-4 h-4 rounded-full bg-white shadow transition-transform`,U(n).closeOnOutsideClick?`translate-x-5`:`translate-x-1`])},null,2)],2),P(`div`,Po,[t[6]||=P(`span`,{class:`text-sm`},`Close panel on outside click`,-1),P(`span`,Fo,`Close the `+q(U(i).productName)+` panel when clicking outside of it (embedded mode only)`,1)])]),P(`label`,Io,[P(`button`,{class:V([`w-10 h-6 rounded-full transition-colors relative shrink-0`,U(n).autoCollapseEdgeToolbar?`bg-primary`:`bg-gray/30`]),onClick:t[2]||=e=>U(r).mutate(e=>{e.autoCollapseEdgeToolbar=!U(n).autoCollapseEdgeToolbar})},[P(`div`,{class:V([`absolute top-1 w-4 h-4 rounded-full bg-white shadow transition-transform`,U(n).autoCollapseEdgeToolbar?`translate-x-5`:`translate-x-1`])},null,2)],2),t[7]||=P(`div`,{class:`flex flex-col`},[P(`span`,{class:`text-sm`},`Auto-collapse edge toolbar`),P(`span`,{class:`text-xs op50`},`Shrink the edge-docked toolbar to a small handle when idle and closed (edge mode only)`)],-1)])]))}}),Ro={ref:`sortContainer`,class:`flex flex-col gap-4`},zo={class:`flex items-center gap-2 px-4 py-3 bg-gray/5 cursor-pointer select-none border-b border-base`},Bo=[`disabled`,`onClick`],Vo={class:`font-medium capitalize`},Ho={class:`text-xs op40`},Uo=[`onClick`],Wo=[`data-dock-id`,`data-container`],Go=[`disabled`,`title`,`onClick`],Ko={key:0,class:`i-ph-check-bold text-xs`},qo={class:`flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity`},Jo=[`onClick`],Yo=[`onClick`],Xo=[`title`,`onClick`],Zo={class:`bg-gray/3 flex flex-col w-full flex-auto`},Qo={key:0,class:`px-2 py-2 text-xs op40 italic border-b border-base border-t-0`},$o={key:0,class:`flex items-center gap-2 px-2 py-1.5 bg-gray/5 border-b border-base border-t-0`},es={class:`text-xs uppercase tracking-wide op50`},ts=[`onClick`],ns=[`data-dock-id`,`data-container`],rs=[`title`,`onClick`],is={key:0,class:`i-ph-check-bold text-xs`},as={class:`flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity`},os=[`onClick`],ss=[`onClick`],cs=[`title`,`onClick`],ls=`:::`,us=4,ds=J({__name:`SettingsDocks`,props:{context:{}},setup(e){let t=e,r=oe(t.context),i=t.context.docks.settings,a=N(()=>s(t.context.docks.entries,r.value,{includeHidden:!0,collapseGroups:!0}));function o(e){return n(t.context.docks.entries,e,r.value,{includeHidden:!0})}function c(e){return v(t.context.docks.entries,e,r.value,{includeHidden:!0})}let l=e=>`cat:${e}`,u=(e,t)=>`grp:${e}${ls}${t}`;function d(e){let t=e.slice(4),n=t.indexOf(ls);return{groupId:t.slice(0,n),subcategory:t.slice(n+3)}}function f(e){if(e.startsWith(`grp:`)){let{groupId:t,subcategory:n}=d(e);return c(t).find(([e])=>e===n)?.[1]??[]}let t=e.slice(4);return a.value.find(([e])=>e===t)?.[1]??[]}function p(e){let n={...r.value,docksCustomOrder:{}};if(e.startsWith(`grp:`)){let{groupId:r,subcategory:i}=d(e);return v(t.context.docks.entries,r,n,{includeHidden:!0}).find(([e])=>e===i)?.[1]??[]}let i=e.slice(4);return s(t.context.docks.entries,n,{includeHidden:!0,collapseGroups:!0}).find(([e])=>e===i)?.[1]??[]}let h=K(`sortContainer`),g=new Map,y=B(null),b=B(null),x=B(null),S=B(!1),C=0;function w(e){let t=e.target;for(;t&&t!==h.value;){if(t.dataset.dockId)return{dockId:t.dataset.dockId,container:t.dataset.container??null};t=t.parentElement}return{dockId:null,container:null}}function T(e){for(;e&&e!==h.value;){if(e.tagName===`BUTTON`||e.tagName===`A`||e.tagName===`INPUT`)return!0;e=e.parentElement}return!1}function E(e,t){let n=p(e).map(e=>e.id),r=t.length===n.length&&t.every((e,t)=>e.id===n[t]);i.mutate(e=>{t.forEach((t,n)=>{r?delete e.docksCustomOrder[t.id]:e.docksCustomOrder[t.id]=n})})}He(h,{draggingElement:()=>h.value?.ownerDocument?.defaultView??window,onStart(e,t){if(t.button!==0||T(t.target))return!1;let{dockId:n,container:r}=w(t);if(!n||!r)return!1;y.value=n,b.value=r,S.value=!1,C=t.clientY},onMove(e,t){if(!y.value)return;if(!S.value){if(Math.abs(t.clientY-C)=a.top&&t.clientY<=a.bottom){n=e;break}}x.value=n},onEnd(){if(y.value&&S.value&&x.value&&b.value){let e=[...f(b.value)],t=e.findIndex(e=>e.id===y.value),n=e.findIndex(e=>e.id===x.value);t!==-1&&n!==-1&&(e.splice(n,0,e.splice(t,1)[0]),E(b.value,e))}y.value=null,b.value=null,x.value=null,S.value=!1}});function ee(e,t,n){e?g.set(t,{el:e,container:n}):g.delete(t)}function te(e,t){if(e===`~settings`)return;let n=r.value.docksHidden.includes(e);t??n?i.mutate(t=>{t.docksHidden=t.docksHidden.filter(t=>t!==e)}):i.mutate(t=>{t.docksHidden=[...t.docksHidden,e]})}function D(e,t){if(!_(e))return;let n=r.value.docksCategoriesHidden.includes(e);t??n?i.mutate(t=>{t.docksCategoriesHidden=t.docksCategoriesHidden.filter(t=>t!==e)}):i.mutate(t=>{t.docksCategoriesHidden=[...t.docksCategoriesHidden,e]})}function ne(e){r.value.docksPinned.includes(e)?i.mutate(t=>{t.docksPinned=t.docksPinned.filter(t=>t!==e)}):i.mutate(t=>{t.docksPinned=[...t.docksPinned,e]})}function re(e,t,n){let r=[...f(e)],i=r.findIndex(e=>e.id===t),a=i+n;a<0||a>=r.length||(r.splice(a,0,r.splice(i,1)[0]),E(e,r))}function ie(e){let t=f(e),n=t.map(e=>e.id);if(e.startsWith(`cat:`))for(let e of t)e.type===`group`&&n.push(...o(e.id).map(e=>e.id));return n}function ae(e){let t=f(e).map(e=>e.id),n=p(e).map(e=>e.id);return t.length!==n.length||t.some((e,t)=>e!==n[t])?!0:e.startsWith(`cat:`)?f(e).some(e=>e.type===`group`&&c(e.id).some(([t])=>ae(u(e.id,t)))):!1}function se(e){let t=ie(e);i.mutate(e=>{t.forEach(t=>{delete e.docksCustomOrder[t]})})}return(e,t)=>(G(),j(H,null,[t[11]||=P(`p`,{class:`text-sm op50 mb-4`},` Manage visibility and order of dock entries. Hidden entries will not appear in the dock bar. `,-1),P(`div`,Ro,[(G(!0),j(H,null,L(a.value,([e,n])=>(G(),j(`div`,{key:e,class:V([`border border-base rounded-lg overflow-hidden transition-opacity`,U(r).docksCategoriesHidden.includes(e)?`op40`:``])},[P(`div`,zo,[P(`button`,{class:V([`w-5 h-5 flex items-center justify-center rounded transition-colors`,[U(_)(e)?U(r).docksCategoriesHidden.includes(e)?`bg-gray/20`:`bg-primary/20 text-primary`:`bg-gray/20 cursor-not-allowed op50`]]),disabled:!U(_)(e),onClick:t=>D(e)},[P(`div`,{class:V([`transition-transform`,U(r).docksCategoriesHidden.includes(e)?`i-ph-eye-slash text-sm op50`:`i-ph-check-bold text-xs`])},null,2)],10,Bo),P(`span`,Vo,q(U(m)(e)),1),P(`span`,Ho,`(`+q(n.length)+`)`,1),t[1]||=P(`span`,{class:`flex-auto`},null,-1),ae(l(e))?(G(),j(`button`,{key:0,class:`w-6 h-6 flex items-center justify-center rounded hover:bg-gray/20 transition-colors`,title:`Reset custom order`,onClick:t=>se(l(e))},[...t[0]||=[P(`div`,{class:`i-ph-arrows-counter-clockwise-duotone text-sm op60`},null,-1)]],8,Uo)):A(``,!0)]),P(`div`,null,[(G(!0),j(H,null,L(n,(i,a)=>(G(),j(H,{key:i.id},[P(`div`,{ref_for:!0,ref:t=>ee(t,i.id,l(e)),"data-dock-id":i.id,"data-container":l(e),class:V([`flex items-center gap-3 px-2 py-2.5 hover:bg-gray/5 transition-all group border-b border-base border-t-0`,[U(r).docksHidden.includes(i.id)?`op40`:``,S.value&&y.value===i.id?`op30 bg-gray/10`:``,x.value===i.id?`ring-1.5 ring-purple/50 rounded`:``,S.value?`select-none`:``]])},[P(`div`,{class:`i-ph-dots-six-vertical w-4 h-4 shrink-0 op25 group-hover:op50 transition-opacity cursor-grab`,style:R(S.value&&y.value===i.id?`cursor: grabbing`:``)},null,4),P(`button`,{class:V([`w-6 h-6 flex items-center justify-center rounded border border-transparent hover:border-base transition-colors shrink-0`,i.id===`~settings`?`cursor-not-allowed op50`:U(r).docksHidden.includes(i.id)?`op50`:``]),disabled:i.id===`~settings`,title:i.id===`~settings`?`Always visible`:U(r).docksHidden.includes(i.id)?`Show`:`Hide`,onClick:e=>te(i.id)},[P(`div`,{class:V([`w-4 h-4 rounded flex items-center justify-center transition-colors`,i.id===`~settings`||U(r).docksHidden.includes(i.id)?`bg-gray/30`:`bg-primary/20 text-primary`])},[U(r).docksHidden.includes(i.id)?A(``,!0):(G(),j(`div`,Ko))],2)],10,Go),I(Ui,{icon:i.icon,title:i.title,class:V([`w-5 h-5 shrink-0`,U(r).docksHidden.includes(i.id)?`saturate-0`:``])},null,8,[`icon`,`title`,`class`]),P(`span`,{class:V([`truncate`,U(r).docksHidden.includes(i.id)?`line-through op60`:``])},q(i.title),3),i.type===`action`?(G(),F(Pa,{key:0,label:`Action`,class:`flex-none text-xs`})):i.type===`group`?(G(),F(Pa,{key:1,label:`Group`,class:`flex-none text-xs`})):A(``,!0),t[4]||=P(`div`,{class:`flex flex-auto`},null,-1),P(`div`,qo,[a>0?(G(),j(`button`,{key:0,class:`w-6 h-6 flex items-center justify-center rounded hover:bg-gray/20 transition-colors`,title:`Move up (higher priority)`,onClick:t=>re(l(e),i.id,-1)},[...t[2]||=[P(`div`,{class:`i-ph-caret-up text-sm op60`},null,-1)]],8,Jo)):A(``,!0),are(l(e),i.id,1)},[...t[3]||=[P(`div`,{class:`i-ph-caret-down text-sm op60`},null,-1)]],8,Yo)):A(``,!0)]),P(`button`,{class:V([`w-7 h-7 flex items-center justify-center rounded hover:bg-gray/20 transition-colors shrink-0`,U(r).docksPinned.includes(i.id)?`text-amber`:`op40 hover:op70`]),title:U(r).docksPinned.includes(i.id)?`Unpin`:`Pin`,onClick:e=>ne(i.id)},[P(`div`,{class:V([U(r).docksPinned.includes(i.id)?`i-ph-push-pin-fill rotate--45`:`i-ph-push-pin`,`color-base`])},null,2)],10,Xo)],10,Wo),i.type===`group`?(G(),j(`div`,{key:0,class:V([`flex`,U(r).docksHidden.includes(i.id)?`op50`:``])},[t[10]||=P(`div`,{class:`border-base border-r border-b w-6 flex-none`},null,-1),P(`div`,Zo,[o(i.id).length===0?(G(),j(`div`,Qo,` No tools in this group yet `)):A(``,!0),(G(!0),j(H,null,L(c(i.id),([e,n])=>(G(),j(H,{key:e},[c(i.id).length>1?(G(),j(`div`,$o,[P(`span`,es,q(U(m)(e)),1),t[6]||=P(`span`,{class:`flex-auto`},null,-1),ae(u(i.id,e))?(G(),j(`button`,{key:0,class:`w-5 h-5 flex items-center justify-center rounded hover:bg-gray/20 transition-colors`,title:`Reset custom order`,onClick:t=>se(u(i.id,e))},[...t[5]||=[P(`div`,{class:`i-ph-arrows-counter-clockwise-duotone text-xs op60`},null,-1)]],8,ts)):A(``,!0)])):A(``,!0),(G(!0),j(H,null,L(n,(a,o)=>(G(),j(`div`,{key:a.id,ref_for:!0,ref:t=>ee(t,a.id,u(i.id,e)),"data-dock-id":a.id,"data-container":u(i.id,e),class:V([`flex items-center gap-3 px-2 py-2 hover:bg-gray/5 transition-all group border-b border-base border-t-0`,[U(r).docksHidden.includes(a.id)?`op40`:``,S.value&&y.value===a.id?`op30 bg-gray/10`:``,x.value===a.id?`ring-1.5 ring-purple/50 rounded`:``,S.value?`select-none`:``]])},[P(`div`,{class:`i-ph-dots-six-vertical w-4 h-4 shrink-0 op25 group-hover:op50 transition-opacity cursor-grab`,style:R(S.value&&y.value===a.id?`cursor: grabbing`:``)},null,4),P(`button`,{class:V([`w-6 h-6 flex items-center justify-center rounded border border-transparent hover:border-base transition-colors shrink-0`,U(r).docksHidden.includes(a.id)?`op50`:``]),title:U(r).docksHidden.includes(a.id)?`Show`:`Hide`,onClick:e=>te(a.id)},[P(`div`,{class:V([`w-4 h-4 rounded flex items-center justify-center transition-colors`,U(r).docksHidden.includes(a.id)?`bg-gray/30`:`bg-primary/20 text-primary`])},[U(r).docksHidden.includes(a.id)?A(``,!0):(G(),j(`div`,is))],2)],10,rs),I(Ui,{icon:a.icon,title:a.title,class:V([`w-5 h-5 shrink-0`,U(r).docksHidden.includes(a.id)?`saturate-0`:``])},null,8,[`icon`,`title`,`class`]),P(`span`,{class:V([`truncate text-sm`,U(r).docksHidden.includes(a.id)?`line-through op60`:``])},q(a.title),3),a.type===`action`?(G(),F(Pa,{key:0,label:`Action`,class:`flex-none text-xs`})):A(``,!0),t[9]||=P(`div`,{class:`flex flex-auto`},null,-1),P(`div`,as,[o>0?(G(),j(`button`,{key:0,class:`w-6 h-6 flex items-center justify-center rounded hover:bg-gray/20 transition-colors`,title:`Move up (higher priority)`,onClick:t=>re(u(i.id,e),a.id,-1)},[...t[7]||=[P(`div`,{class:`i-ph-caret-up text-sm op60`},null,-1)]],8,os)):A(``,!0),ore(u(i.id,e),a.id,1)},[...t[8]||=[P(`div`,{class:`i-ph-caret-down text-sm op60`},null,-1)]],8,ss)):A(``,!0)]),P(`button`,{class:V([`w-7 h-7 flex items-center justify-center rounded hover:bg-gray/20 transition-colors shrink-0`,U(r).docksPinned.includes(a.id)?`text-amber`:`op40 hover:op70`]),title:U(r).docksPinned.includes(a.id)?`Unpin`:`Pin`,onClick:e=>ne(a.id)},[P(`div`,{class:V([U(r).docksPinned.includes(a.id)?`i-ph-push-pin-fill rotate--45`:`i-ph-push-pin`,`color-base`])},null,2)],10,cs)],10,ns))),128))],64))),128))])],2)):A(``,!0)],64))),128))])],2))),128))],512)],64))}}),fs={class:`mb-3`},ps={class:`border border-base rounded-lg overflow-hidden`},ms={class:`flex-1 min-w-0`},hs={class:`flex items-center gap-1.5`},gs={class:`truncate text-sm`},_s={key:0,class:`text-[10px] px-1.5 py-0.5 rounded bg-blue/10 text-blue shrink-0`},vs={key:0,class:`text-xs op40 truncate mt-0.5`},ys={key:2,class:`flex items-center gap-1.5 shrink-0`},bs=[`onClick`],xs=[`onClick`],Ss=[`onClick`],Cs=[`onClick`],ws={key:0,class:`py-6 text-center text-sm op40`},Ts={class:`relative bg-base border border-base rounded-lg shadow-xl w-96 p-5`},Es={class:`mb-4`},Ds=[`value`],Os={class:`flex items-center gap-2 mb-4`},ks={key:0,class:`mb-4 flex flex-col gap-1.5`},As={class:`flex items-center justify-end gap-2`},js=[`disabled`],Ms=J({__name:`SettingsShortcuts`,props:{context:{}},setup(e){let n=e,r=n.context.commands,a=oe(n.context),o=N(()=>a.value.commandShortcuts??{}),s=B(``),c=N(()=>d(r.commands,{...n.context.when.context,dockOpen:!0,paletteOpen:!1})),l=N(()=>{let e=[];return f(c.value,(t,n)=>{let r=n.at(-1)?.title;e.push({command:t,...r?{parentTitle:r}:{},depth:n.length})}),e}),p=N(()=>{if(!s.value)return l.value;let e=s.value.toLowerCase();return l.value.filter(t=>t.command.title.toLowerCase().includes(e)||t.command.id.toLowerCase().includes(e)||t.command.description?.toLowerCase().includes(e))});function m(e){return r.getKeybindings(e)}function h(e){return e.depth>0?{marginLeft:`${e.depth*1.5}rem`}:{}}function g(e){return e.source===`server`||!!e.action}function _(e){return ee(o.value[e],v(e))}function v(e){return te(r.commands,e)?.keybindings??[]}function y(e){r.settings.mutate(t=>{v(e).length>0?t.commandShortcuts[e]=[]:delete t.commandShortcuts[e]})}function b(e){r.settings.mutate(t=>{delete t.commandShortcuts[e]})}let x=B(!1),S=B(null),C=B(!1),w=B(!1),T=B(!1),E=B(!1),D=B(``);function ne(e){let t=m(e);if(S.value=e,t.length>0){let e=t[0].key.split(`+`);C.value=e.includes(`Mod`),w.value=e.includes(`Ctrl`),T.value=e.includes(`Alt`),E.value=e.includes(`Shift`),D.value=e.filter(e=>![`Mod`,`Ctrl`,`Alt`,`Shift`,`Meta`].includes(e)).join(`+`)}else C.value=!1,w.value=!1,T.value=!1,E.value=!1,D.value=``;x.value=!0}function re(){x.value=!1,S.value=null}let ie=N(()=>{let e=[];return C.value&&e.push(`Mod`),w.value&&e.push(`Ctrl`),T.value&&e.push(`Alt`),E.value&&e.push(`Shift`),D.value&&e.push(D.value),e.join(`+`)}),ae=N(()=>{let e=[],n=ie.value;if(!n)return e;let r=C.value||w.value||T.value;!r&&D.value&&e.push(`Single key without modifiers may interfere with typing`),!r&&E.value&&D.value.length===1&&e.push(`Shift + letter may interfere with typing`);let a=i[n];if(a){let r=t(n).join(`+`);e.push(`Conflicts with browser shortcut: ${a} (${r})`)}if(S.value){for(let t of l.value)if(t.command.id!==S.value&&m(t.command.id).some(e=>e.key===n)){e.push(`Conflicts with "${t.command.title}"`);break}}return e}),se=N(()=>D.value.length>0);function ce(e){if(e.preventDefault(),e.stopPropagation(),[`Control`,`Meta`,`Alt`,`Shift`].includes(e.key))return;C.value=u?e.metaKey:e.ctrlKey,u&&e.ctrlKey&&(w.value=!0),T.value=e.altKey,E.value=e.shiftKey;let t=e.key;t.length===1&&(t=t.toUpperCase()),D.value=t}function le(){if(!S.value||!se.value)return;let e=[{key:ie.value}];ee(e,v(S.value))?r.settings.mutate(t=>{t.commandShortcuts[S.value]=e}):r.settings.mutate(e=>{delete e.commandShortcuts[S.value]}),re()}let ue=B(null);return Xe(x,async e=>{if(!e)return;await Ae();let t=ue.value?.ownerDocument??document,n=t.defaultView??window,r=e=>{if(e.key===`Escape`){let e=t.activeElement;(!e||!e.classList.contains(`shortcut-key-input`))&&(re(),n.removeEventListener(`keydown`,r))}};n.addEventListener(`keydown`,r)}),(e,n)=>(G(),j(H,null,[n[10]||=P(`p`,{class:`text-sm op50 mb-4`},` Customize keyboard shortcuts for commands. `,-1),P(`div`,fs,[Ze(P(`input`,{"onUpdate:modelValue":n[0]||=e=>s.value=e,class:`w-full bg-gray/10 rounded-lg px-3 py-2 text-sm outline-none focus:ring-1 focus:ring-primary/30`,placeholder:`Search commands...`},null,512),[[ke,s.value]])]),P(`div`,ps,[(G(!0),j(H,null,L(p.value,e=>(G(),j(`div`,{key:e.command.id,class:`flex items-center gap-3 px-4 py-2.5 hover:bg-gray/5 transition-colors border-b border-base last:border-b-0`},[e.command.icon?(G(),F(Ui,{key:0,icon:e.command.icon,class:`w-4 h-4 shrink-0 op60`,style:R(h(e))},null,8,[`icon`,`style`])):(G(),j(`div`,{key:1,style:R(h(e)),class:`w-4 h-4 shrink-0`},null,4)),P(`div`,ms,[P(`div`,hs,[P(`span`,gs,q(e.command.title),1),e.command.source===`server`?(G(),j(`span`,_s,`server`)):A(``,!0)]),e.command.description?(G(),j(`div`,vs,q(e.command.description),1)):A(``,!0)]),g(e.command)?(G(),j(`div`,ys,[m(e.command.id).length>0?(G(!0),j(H,{key:0},L(m(e.command.id),(t,n)=>(G(),j(`button`,{key:n,class:`flex items-center gap-0.5 hover:bg-gray/10 rounded px-1 py-0.5 transition-colors`,title:`Click to edit`,onClick:t=>ne(e.command.id)},[I(sr,{keys:t.key},null,8,[`keys`])],8,bs))),128)):(G(),j(`button`,{key:1,class:`text-xs op30 hover:op60 px-2 py-1 rounded hover:bg-gray/10 transition-colors`,onClick:t=>ne(e.command.id)},` Add shortcut `,8,xs)),m(e.command.id).length>0?(G(),j(`button`,{key:2,class:`w-5 h-5 flex items-center justify-center rounded hover:bg-gray/20 transition-colors op30 hover:op70`,title:`Clear shortcut`,onClick:t=>y(e.command.id)},[...n[5]||=[P(`div`,{class:`i-ph-x text-xs`},null,-1)]],8,Ss)):A(``,!0),_(e.command.id)?(G(),j(`button`,{key:3,class:`w-5 h-5 flex items-center justify-center rounded hover:bg-gray/20 transition-colors op30 hover:op70`,title:`Reset to default`,onClick:t=>b(e.command.id)},[...n[6]||=[P(`div`,{class:`i-ph-arrow-counter-clockwise text-xs`},null,-1)]],8,Cs)):A(``,!0)])):A(``,!0)]))),128)),p.value.length===0?(G(),j(`div`,ws,` No commands found `)):A(``,!0)]),x.value?(G(),j(`div`,{key:0,ref_key:`editorModal`,ref:ue,class:`fixed inset-0 z-command-palette flex items-center justify-center`},[P(`div`,{class:`absolute inset-0 bg-black/30`,onClick:re}),P(`div`,Ts,[n[9]||=P(`h3`,{class:`text-sm font-medium mb-4`},` Edit Shortcut `,-1),P(`div`,Es,[n[7]||=P(`label`,{class:`text-xs op50 mb-1.5 block`},`Press a key combination or toggle modifiers below`,-1),P(`input`,{class:`shortcut-key-input w-full bg-gray/10 border border-base rounded-lg px-3 py-2.5 text-center text-sm font-mono outline-none focus:border-primary/40`,value:ie.value?U(t)(ie.value).join(` + `):``,placeholder:`Press keys...`,readonly:``,onKeydown:ce},null,40,Ds)]),P(`div`,Os,[P(`button`,{class:V([`px-3 py-1.5 rounded-md text-xs font-mono border transition-colors`,C.value?`bg-primary/15 border-primary/30 text-primary`:`border-base op50 hover:op80`]),onClick:n[1]||=e=>C.value=!C.value},q(U(u)?`⌘ Cmd`:`Ctrl`),3),U(u)?(G(),j(`button`,{key:0,class:V([`px-3 py-1.5 rounded-md text-xs font-mono border transition-colors`,w.value?`bg-primary/15 border-primary/30 text-primary`:`border-base op50 hover:op80`]),onClick:n[2]||=e=>w.value=!w.value},` \\u2303 Ctrl `,2)):A(``,!0),P(`button`,{class:V([`px-3 py-1.5 rounded-md text-xs font-mono border transition-colors`,T.value?`bg-primary/15 border-primary/30 text-primary`:`border-base op50 hover:op80`]),onClick:n[3]||=e=>T.value=!T.value},q(U(u)?`⌥ Alt`:`Alt`),3),P(`button`,{class:V([`px-3 py-1.5 rounded-md text-xs font-mono border transition-colors`,E.value?`bg-primary/15 border-primary/30 text-primary`:`border-base op50 hover:op80`]),onClick:n[4]||=e=>E.value=!E.value},q(U(u)?`⇧ Shift`:`Shift`),3)]),ae.value.length>0?(G(),j(`div`,ks,[(G(!0),j(H,null,L(ae.value,(e,t)=>(G(),j(`div`,{key:t,class:`flex items-center gap-1.5 text-xs text-amber`},[n[8]||=P(`div`,{class:`i-ph-warning-duotone w-3.5 h-3.5 shrink-0`},null,-1),M(` `+q(e),1)]))),128))])):A(``,!0),P(`div`,As,[P(`button`,{class:`px-3 py-1.5 rounded text-xs op60 hover:op100 hover:bg-gray/10 transition-colors`,onClick:re},` Cancel `),P(`button`,{class:V([`px-3 py-1.5 rounded text-xs transition-colors`,se.value?`bg-primary/15 text-primary hover:bg-primary/25`:`op30 cursor-not-allowed`]),disabled:!se.value,onClick:le},` OK `,10,js)])])],512)):A(``,!0)],64))}}),Ns={class:`h-full w-full overflow-hidden flex flex-col`},Ps={class:`flex items-center border-b border-base px-6 shrink-0`},Fs=[`onClick`],Is={key:0,class:`absolute bottom-0 left-2 right-2 h-0.5 bg-primary rounded-full`},Ls={class:`flex-1 overflow-auto p8`,style:{"scrollbar-gutter":`stable`}},Rs={class:`max-w-200 mx-auto`},zs=J({__name:`ViewBuiltinSettings`,props:{context:{},entry:{}},setup(e){let t=[{id:`appearance`,label:`Appearance`,icon:`i-ph-paint-brush-duotone`},{id:`docks`,label:`Docks`,icon:`i-ph-layout-duotone`},{id:`shortcuts`,label:`Shortcuts`,icon:`i-ph-keyboard-duotone`},{id:`advanced`,label:`Advanced`,icon:`i-ph-wrench-duotone`}],n=B(`appearance`);return(r,i)=>(G(),j(`div`,Ns,[P(`div`,Ps,[(G(),j(H,null,L(t,e=>P(`button`,{key:e.id,class:V([`flex items-center gap-1.5 px-4 py-3 text-sm transition-colors relative`,n.value===e.id?`op100 text-primary`:`op50 hover:op80`]),onClick:t=>n.value=e.id},[P(`div`,{class:V([e.icon,`w-4 h-4`])},null,2),M(` `+q(e.label)+` `,1),n.value===e.id?(G(),j(`div`,Is)):A(``,!0)],10,Fs)),64))]),P(`div`,Ls,[P(`div`,Rs,[n.value===`appearance`?(G(),F(Lo,{key:0,context:e.context},null,8,[`context`])):A(``,!0),n.value===`shortcuts`?(G(),F(Ms,{key:1,context:e.context},null,8,[`context`])):A(``,!0),n.value===`docks`?(G(),F(ds,{key:2,context:e.context},null,8,[`context`])):A(``,!0),n.value===`advanced`?(G(),F(bo,{key:3,context:e.context},null,8,[`context`])):A(``,!0)])])]))}}),Bs=J({__name:`ViewCustomRenderer`,props:{context:{},entry:{},panes:{},divStyle:{}},setup(e){let t=e,n=K(`viewFrame`);return Me(()=>{let e=t.panes.ensure(t.entry.id,{tagName:`div`}),r=e.element;r.style.boxShadow=`none`,r.style.outline=`none`,Object.assign(r.style,t.divStyle);let i=t.context.docks.getStateById(t.entry.id);i&&(i.domElements.panel=r),e.mount(n.value)}),xe(()=>{t.panes.get(t.entry.id)?.unmount()}),(e,t)=>(G(),j(`div`,{ref_key:`viewFrame`,ref:n,class:`devframes-view-custom-renderer w-full h-full flex items-center justify-center`},null,512))}}),Vs={class:`devframes-view-dock-renderer relative h-full w-full`},Hs={key:0,class:`absolute inset-0 flex flex-col items-center justify-center gap-2 p6 text-center`},Us={class:`text-sm color-muted`},Ws={key:1,class:`absolute inset-0 flex flex-col items-center justify-center gap-2 p6 text-center`},Gs={class:`text-sm color-muted`},Ks={key:0,class:`max-w-100 text-xs color-faint`},qs=J({__name:`ViewDockRenderer`,props:{context:{},entry:{}},setup(e){let t=e,n=K(`container`),r=B(`loading`),i=B(null),a,o=0;async function s(){let e=++o;if(a?.(),a=void 0,!t.context.renderers.has(t.entry.type)){r.value=`missing-renderer`;return}r.value=`loading`,i.value=null;let s=await t.context.renderers.mount(t.entry,n.value);if(e!==o){s.status===`mounted`&&s.dispose();return}s.status===`mounted`?(a=s.dispose,r.value=`mounted`):s.status===`missing-renderer`?r.value=`missing-renderer`:(r.value=`load-error`,i.value=String(s.error?.message??s.error))}return Me(()=>{s()}),Xe(()=>t.entry.id,()=>{s()}),xe(()=>{o++,a?.(),a=void 0}),(t,a)=>(G(),j(`div`,Vs,[Ze(P(`div`,{ref_key:`container`,ref:n,class:V([`h-full w-full`,U(Be)?`dark`:`light`])},null,2),[[k,r.value===`mounted`||r.value===`loading`]]),r.value===`missing-renderer`?(G(),j(`div`,Hs,[a[1]||=P(`div`,{class:`i-ph:puzzle-piece-duotone text-3xl color-faint`},null,-1),P(`div`,Us,` No renderer for “`+q(e.entry.type)+`” in the current environment `,1),a[2]||=P(`div`,{class:`text-xs color-faint`},` The host has not registered a renderer for this dock type. `,-1)])):r.value===`load-error`?(G(),j(`div`,Ws,[a[4]||=P(`div`,{class:`i-ph:warning-duotone text-3xl color-faint`},null,-1),P(`div`,Gs,` The renderer for “`+q(e.entry.type)+`” failed to load `,1),i.value?(G(),j(`div`,Ks,q(i.value),1)):A(``,!0),I(ma,{class:`mt-2`,onClick:a[0]||=e=>s()},{default:z(()=>[...a[3]||=[M(` Retry `,-1)]]),_:1})])):A(``,!0)]))}}),Js=[`top`,`right`,`bottom`,`left`],Ys=[`start`,`end`],Xs=Js.reduce((e,t)=>e.concat(t,t+`-`+Ys[0],t+`-`+Ys[1]),[]),Zs=Math.min,Qs=Math.max,$s={left:`right`,right:`left`,bottom:`top`,top:`bottom`};function ec(e,t,n){return Qs(e,Zs(t,n))}function tc(e,t){return typeof e==`function`?e(t):e}function nc(e){return e.split(`-`)[0]}function rc(e){return e.split(`-`)[1]}function ic(e){return e===`x`?`y`:`x`}function ac(e){return e===`y`?`height`:`width`}function oc(e){let t=e[0];return t===`t`||t===`b`?`y`:`x`}function sc(e){return ic(oc(e))}function cc(e,t,n){n===void 0&&(n=!1);let r=rc(e),i=sc(e),a=ac(i),o=i===`x`?r===(n?`end`:`start`)?`right`:`left`:r===`start`?`bottom`:`top`;return t.reference[a]>t.floating[a]&&(o=_c(o)),[o,_c(o)]}function lc(e){let t=_c(e);return[uc(e),t,uc(t)]}function uc(e){return e.includes(`start`)?e.replace(`start`,`end`):e.replace(`end`,`start`)}var dc=[`left`,`right`],fc=[`right`,`left`],pc=[`top`,`bottom`],mc=[`bottom`,`top`];function hc(e,t,n){switch(e){case`top`:case`bottom`:return n?t?fc:dc:t?dc:fc;case`left`:case`right`:return t?pc:mc;default:return[]}}function gc(e,t,n,r){let i=rc(e),a=hc(nc(e),n===`start`,r);return i&&(a=a.map(e=>e+`-`+i),t&&(a=a.concat(a.map(uc)))),a}function _c(e){let t=nc(e);return $s[t]+e.slice(t.length)}function vc(e){return{top:e.top??0,right:e.right??0,bottom:e.bottom??0,left:e.left??0}}function yc(e){return typeof e==`number`?{top:e,right:e,bottom:e,left:e}:vc(e)}function bc(e){let{x:t,y:n,width:r,height:i}=e;return{width:r,height:i,top:n,left:t,right:t+r,bottom:n+i,x:t,y:n}}function xc(e,t,n){let{reference:r,floating:i}=e,a=oc(t),o=sc(t),s=ac(o),c=nc(t),l=a===`y`,u=r.x+r.width/2-i.width/2,d=r.y+r.height/2-i.height/2,f=r[s]/2-i[s]/2,p;switch(c){case`top`:p={x:u,y:r.y-i.height};break;case`bottom`:p={x:u,y:r.y+r.height};break;case`right`:p={x:r.x+r.width,y:d};break;case`left`:p={x:r.x-i.width,y:d};break;default:p={x:r.x,y:r.y}}let m=rc(t);return m&&(p[o]+=f*(m===`end`?1:-1)*(n&&l?-1:1)),p}async function Sc(e,t){t===void 0&&(t={});let{x:n,y:r,platform:i,rects:a,elements:o,strategy:s}=e,{boundary:c=`clippingAncestors`,rootBoundary:l=`viewport`,elementContext:u=`floating`,altBoundary:d=!1,padding:f=0}=tc(t,e),p=yc(f),m=o[d?u===`floating`?`reference`:`floating`:u],h=bc(await i.getClippingRect({element:await(i.isElement==null?void 0:i.isElement(m))??!0?m:m.contextElement||await(i.getDocumentElement==null?void 0:i.getDocumentElement(o.floating)),boundary:c,rootBoundary:l,strategy:s})),g=u===`floating`?{x:n,y:r,width:a.floating.width,height:a.floating.height}:a.reference,_=await(i.getOffsetParent==null?void 0:i.getOffsetParent(o.floating)),v=await(i.isElement==null?void 0:i.isElement(_))&&await(i.getScale==null?void 0:i.getScale(_))||{x:1,y:1},y=bc(i.convertOffsetParentRelativeRectToViewportRelativeRect?await i.convertOffsetParentRelativeRectToViewportRelativeRect({elements:o,rect:g,offsetParent:_,strategy:s}):g);return{top:(h.top-y.top+p.top)/v.y,bottom:(y.bottom-h.bottom+p.bottom)/v.y,left:(h.left-y.left+p.left)/v.x,right:(y.right-h.right+p.right)/v.x}}var Cc=50,wc=async(e,t,n)=>{let{placement:r=`bottom`,strategy:i=`absolute`,middleware:a=[],platform:o}=n,s=o.detectOverflow?o:{...o,detectOverflow:Sc},c=await(o.isRTL==null?void 0:o.isRTL(t)),l=await o.getElementRects({reference:e,floating:t,strategy:i}),{x:u,y:d}=xc(l,r,c),f=r,p=0,m={};for(let n=0;n({name:`arrow`,options:e,async fn(t){let{x:n,y:r,placement:i,rects:a,platform:o,elements:s,middlewareData:c}=t,{element:l,padding:u=0}=tc(e,t)||{};if(l==null)return{};let d=yc(u),f={x:n,y:r},p=sc(i),m=ac(p),h=await o.getDimensions(l),g=p===`y`,_=g?`top`:`left`,v=g?`bottom`:`right`,y=g?`clientHeight`:`clientWidth`,b=a.reference[m]+a.reference[p]-f[p]-a.floating[m],x=f[p]-a.reference[p],S=await(o.getOffsetParent==null?void 0:o.getOffsetParent(l)),C=S?S[y]:0;(!C||!await(o.isElement==null?void 0:o.isElement(S)))&&(C=s.floating[y]||a.floating[m]);let w=b/2-x/2,T=C/2-h[m]/2-1,E=Zs(d[_],T),ee=Zs(d[v],T),te=C-h[m]-ee,D=C/2-h[m]/2+w,ne=ec(E,D,te),re=!c.arrow&&rc(i)!=null&&D!==ne&&a.reference[m]/2-(Drc(t)===e),...n.filter(t=>rc(t)!==e)]:n.filter(e=>nc(e)===e)).filter(n=>!e||rc(n)===e||(t?uc(n)!==n:!1))}var Dc=function(e){return e===void 0&&(e={}),{name:`autoPlacement`,options:e,async fn(t){let{rects:n,middlewareData:r,placement:i,platform:a,elements:o}=t,{crossAxis:s=!1,alignment:c,allowedPlacements:l=Xs,autoAlignment:u=!0,...d}=tc(e,t),f=c!==void 0||l===Xs?Ec(c||null,u,l):l,p=r.autoPlacement?.index||0,m=f[p];if(m==null)return{};if(i!==m)return{reset:{placement:f[0]}};let h=await a.detectOverflow(t,d),g=cc(m,n,await(a.isRTL==null?void 0:a.isRTL(o.floating))),_=[h[nc(m)],h[g[0]],h[g[1]]],v=[...r.autoPlacement?.overflows||[],{placement:m,overflows:_}],y=f[p+1];if(y)return{data:{index:p+1,overflows:v},reset:{placement:y}};let b=v.map(e=>{let t=rc(e.placement);return[e.placement,t&&s?e.overflows.slice(0,2).reduce((e,t)=>e+t,0):e.overflows[0],e.overflows]}).sort((e,t)=>e[1]-t[1]),x=b.filter(e=>e[2].slice(0,rc(e[0])?2:3).every(e=>e<=0))[0]?.[0]||b[0][0];return x===i?{}:{data:{index:p+1,overflows:v},reset:{placement:x}}}}},Oc=function(e){return e===void 0&&(e={}),{name:`flip`,options:e,async fn(t){var n;let{placement:r,middlewareData:i,rects:a,initialPlacement:o,platform:s,elements:c}=t,{mainAxis:l=!0,crossAxis:u=!0,fallbackPlacements:d,fallbackStrategy:f=`bestFit`,fallbackAxisSideDirection:p=`none`,flipAlignment:m=!0,...h}=tc(e,t);if((n=i.arrow)!=null&&n.alignmentOffset)return{};let g=nc(r),_=oc(o),v=nc(o)===o,y=await(s.isRTL==null?void 0:s.isRTL(c.floating)),b=d||(v||!m?[_c(o)]:lc(o)),x=p!==`none`;!d&&x&&b.push(...gc(o,m,p,y));let S=[o,...b],C=await s.detectOverflow(t,h),w=[],T=i.flip?.overflows||[];if(l&&w.push(C[g]),u){let e=cc(r,a,y);w.push(C[e[0]],C[e[1]])}if(T=[...T,{placement:r,overflows:w}],!w.every(e=>e<=0)){let e=(i.flip?.index||0)+1,t=S[e];if(t&&(u!==`alignment`||_===oc(t)||T.every(e=>oc(e.placement)!==_||e.overflows[0]>0)))return{data:{index:e,overflows:T},reset:{placement:t}};let n=T.filter(e=>e.overflows[0]<=0).sort((e,t)=>e.overflows[1]-t.overflows[1])[0]?.placement;if(!n)switch(f){case`bestFit`:{let e=T.filter(e=>{if(x){let t=oc(e.placement);return t===_||t===`y`}return!0}).map(e=>[e.placement,e.overflows.filter(e=>e>0).reduce((e,t)=>e+t,0)]).sort((e,t)=>e[1]-t[1])[0]?.[0];e&&(n=e);break}case`initialPlacement`:n=o}if(r!==n)return{reset:{placement:n}}}return{}}}},kc=new Set([`left`,`top`]);async function Ac(e,t){let{placement:n,platform:r,elements:i}=e,a=await(r.isRTL==null?void 0:r.isRTL(i.floating)),o=nc(n),s=rc(n),c=oc(n)===`y`,l=kc.has(o)?-1:1,u=a&&c?-1:1,d=tc(t,e),{mainAxis:f,crossAxis:p,alignmentAxis:m}=typeof d==`number`?{mainAxis:d,crossAxis:0,alignmentAxis:null}:{mainAxis:d.mainAxis||0,crossAxis:d.crossAxis||0,alignmentAxis:d.alignmentAxis};return s&&typeof m==`number`&&(p=s===`end`?m*-1:m),c?{x:p*u,y:f*l}:{x:f*l,y:p*u}}var jc=function(e){return e===void 0&&(e=0),{name:`offset`,options:e,async fn(t){var n;let{x:r,y:i,placement:a,middlewareData:o}=t,s=await Ac(t,e);return a===o.offset?.placement&&(n=o.arrow)!=null&&n.alignmentOffset?{}:{x:r+s.x,y:i+s.y,data:{...s,placement:a}}}}},Mc=function(e){return e===void 0&&(e={}),{name:`shift`,options:e,async fn(t){let{x:n,y:r,placement:i,platform:a}=t,{mainAxis:o=!0,crossAxis:s=!1,limiter:c={fn:e=>{let{x:t,y:n}=e;return{x:t,y:n}}},...l}=tc(e,t),u={x:n,y:r},d=await a.detectOverflow(t,l),f=oc(i),p=ic(f),m=u[p],h=u[f],g=(e,t)=>ec(t+d[e===`y`?`top`:`left`],t,t-d[e===`y`?`bottom`:`right`]);o&&(m=g(p,m)),s&&(h=g(f,h));let _=c.fn({...t,[p]:m,[f]:h});return{..._,data:{x:_.x-n,y:_.y-r,enabled:{[p]:o,[f]:s}}}}}},Nc=function(e){return e===void 0&&(e={}),{name:`size`,options:e,async fn(t){let{placement:n,rects:r,platform:i,elements:a}=t,{apply:o=()=>{},...s}=tc(e,t),c=await i.detectOverflow(t,s),l=nc(n),u=rc(n),d=oc(n)===`y`,{width:f,height:p}=r.floating,m,h;l===`top`||l===`bottom`?(m=l,h=u===(await(i.isRTL==null?void 0:i.isRTL(a.floating))?`start`:`end`)?`left`:`right`):(h=l,m=u===`end`?`top`:`bottom`);let g=p-c.top-c.bottom,_=f-c.left-c.right,v=Zs(p-c[m],g),y=Zs(f-c[h],_),b=t.middlewareData.shift,x=!b,S=v,C=y;b!=null&&b.enabled.x&&(C=_),b!=null&&b.enabled.y&&(S=g),x&&!u&&(d?C=f-2*Qs(c.left,c.right):S=p-2*Qs(c.top,c.bottom)),await o({...t,availableWidth:C,availableHeight:S});let w=await i.getDimensions(a.floating);return f!==w.width||p!==w.height?{reset:{rects:!0}}:{}}}};function Pc(e){return e.ownerDocument?.defaultView||window}function Fc(e){return Pc(e).getComputedStyle(e)}var Ic=Math.min,Lc=Math.max,Rc=Math.round;function zc(e){let t=Fc(e),n=parseFloat(t.width),r=parseFloat(t.height),i=e.offsetWidth,a=e.offsetHeight,o=Rc(n)!==i||Rc(r)!==a;return o&&(n=i,r=a),{width:n,height:r,fallback:o}}function Bc(e){return Gc(e)?(e.nodeName||``).toLowerCase():``}var Vc;function Hc(){if(Vc)return Vc;let e=navigator.userAgentData;return e&&Array.isArray(e.brands)?(Vc=e.brands.map((e=>e.brand+`/`+e.version)).join(` `),Vc):navigator.userAgent}function Uc(e){return e instanceof Pc(e).HTMLElement}function Wc(e){return e instanceof Pc(e).Element}function Gc(e){return e instanceof Pc(e).Node}function Kc(e){return typeof ShadowRoot>`u`?!1:e instanceof Pc(e).ShadowRoot||e instanceof ShadowRoot}function qc(e){let{overflow:t,overflowX:n,overflowY:r,display:i}=Fc(e);return/auto|scroll|overlay|hidden|clip/.test(t+r+n)&&![`inline`,`contents`].includes(i)}function Jc(e){return[`table`,`td`,`th`].includes(Bc(e))}function Yc(e){let t=/firefox/i.test(Hc()),n=Fc(e),r=n.backdropFilter||n.WebkitBackdropFilter;return n.transform!==`none`||n.perspective!==`none`||!!r&&r!==`none`||t&&n.willChange===`filter`||t&&!!n.filter&&n.filter!==`none`||[`transform`,`perspective`].some((e=>n.willChange.includes(e)))||[`paint`,`layout`,`strict`,`content`].some((e=>{let t=n.contain;return t!=null&&t.includes(e)}))}function Xc(){return!/^((?!chrome|android).)*safari/i.test(Hc())}function Zc(e){return[`html`,`body`,`#document`].includes(Bc(e))}function Qc(e){return Wc(e)?e:e.contextElement}var $c={x:1,y:1};function el(e){let t=Qc(e);if(!Uc(t))return $c;let n=t.getBoundingClientRect(),{width:r,height:i,fallback:a}=zc(t),o=(a?Rc(n.width):n.width)/r,s=(a?Rc(n.height):n.height)/i;return o&&Number.isFinite(o)||(o=1),s&&Number.isFinite(s)||(s=1),{x:o,y:s}}function tl(e,t,n,r){t===void 0&&(t=!1),n===void 0&&(n=!1);let i=e.getBoundingClientRect(),a=Qc(e),o=$c;t&&(r?Wc(r)&&(o=el(r)):o=el(e));let s=a?Pc(a):window,c=!Xc()&&n,l=(i.left+(c&&s.visualViewport?.offsetLeft||0))/o.x,u=(i.top+(c&&s.visualViewport?.offsetTop||0))/o.y,d=i.width/o.x,f=i.height/o.y;if(a){let e=Pc(a),t=r&&Wc(r)?Pc(r):r,n=e.frameElement;for(;n&&r&&t!==e;){let e=el(n),t=n.getBoundingClientRect(),r=getComputedStyle(n);t.x+=(n.clientLeft+parseFloat(r.paddingLeft))*e.x,t.y+=(n.clientTop+parseFloat(r.paddingTop))*e.y,l*=e.x,u*=e.y,d*=e.x,f*=e.y,l+=t.x,u+=t.y,n=Pc(n).frameElement}}return{width:d,height:f,top:u,right:l+d,bottom:u+f,left:l,x:l,y:u}}function nl(e){return((Gc(e)?e.ownerDocument:e.document)||window.document).documentElement}function rl(e){return Wc(e)?{scrollLeft:e.scrollLeft,scrollTop:e.scrollTop}:{scrollLeft:e.pageXOffset,scrollTop:e.pageYOffset}}function il(e){return tl(nl(e)).left+rl(e).scrollLeft}function al(e){if(Bc(e)===`html`)return e;let t=e.assignedSlot||e.parentNode||Kc(e)&&e.host||nl(e);return Kc(t)?t.host:t}function ol(e){let t=al(e);return Zc(t)?t.ownerDocument.body:Uc(t)&&qc(t)?t:ol(t)}function sl(e,t){t===void 0&&(t=[]);let n=ol(e),r=n===e.ownerDocument?.body,i=Pc(n);return r?t.concat(i,i.visualViewport||[],qc(n)?n:[]):t.concat(n,sl(n))}function cl(e,t,n){return t===`viewport`?bc(function(e,t){let n=Pc(e),r=nl(e),i=n.visualViewport,a=r.clientWidth,o=r.clientHeight,s=0,c=0;if(i){a=i.width,o=i.height;let e=Xc();(e||!e&&t===`fixed`)&&(s=i.offsetLeft,c=i.offsetTop)}return{width:a,height:o,x:s,y:c}}(e,n)):Wc(t)?bc(function(e,t){let n=tl(e,!0,t===`fixed`),r=n.top+e.clientTop,i=n.left+e.clientLeft,a=Uc(e)?el(e):{x:1,y:1};return{width:e.clientWidth*a.x,height:e.clientHeight*a.y,x:i*a.x,y:r*a.y}}(t,n)):bc(function(e){let t=nl(e),n=rl(e),r=e.ownerDocument.body,i=Lc(t.scrollWidth,t.clientWidth,r.scrollWidth,r.clientWidth),a=Lc(t.scrollHeight,t.clientHeight,r.scrollHeight,r.clientHeight),o=-n.scrollLeft+il(e),s=-n.scrollTop;return Fc(r).direction===`rtl`&&(o+=Lc(t.clientWidth,r.clientWidth)-i),{width:i,height:a,x:o,y:s}}(nl(e)))}function ll(e){return Uc(e)&&Fc(e).position!==`fixed`?e.offsetParent:null}function ul(e){let t=Pc(e),n=ll(e);for(;n&&Jc(n)&&Fc(n).position===`static`;)n=ll(n);return n&&(Bc(n)===`html`||Bc(n)===`body`&&Fc(n).position===`static`&&!Yc(n))?t:n||function(e){let t=al(e);for(;Uc(t)&&!Zc(t);){if(Yc(t))return t;t=al(t)}return null}(e)||t}function dl(e,t,n){let r=Uc(t),i=nl(t),a=tl(e,!0,n===`fixed`,t),o={scrollLeft:0,scrollTop:0},s={x:0,y:0};if(r||!r&&n!==`fixed`){if((Bc(t)!==`body`||qc(i))&&(o=rl(t)),Uc(t)){let e=tl(t,!0);s.x=e.x+t.clientLeft,s.y=e.y+t.clientTop}else i&&(s.x=il(i))}return{x:a.left+o.scrollLeft-s.x,y:a.top+o.scrollTop-s.y,width:a.width,height:a.height}}var fl={getClippingRect:function(e){let{element:t,boundary:n,rootBoundary:r,strategy:i}=e,a=[...n===`clippingAncestors`?function(e,t){let n=t.get(e);if(n)return n;let r=sl(e).filter((e=>Wc(e)&&Bc(e)!==`body`)),i=null,a=Fc(e).position===`fixed`,o=a?al(e):e;for(;Wc(o)&&!Zc(o);){let e=Fc(o),t=Yc(o);(a?t||i:t||e.position!==`static`||!i||![`absolute`,`fixed`].includes(i.position))?i=e:r=r.filter((e=>e!==o)),o=al(o)}return t.set(e,r),r}(t,this._c):[].concat(n),r],o=a[0],s=a.reduce(((e,n)=>{let r=cl(t,n,i);return e.top=Lc(r.top,e.top),e.right=Ic(r.right,e.right),e.bottom=Ic(r.bottom,e.bottom),e.left=Lc(r.left,e.left),e}),cl(t,o,i));return{width:s.right-s.left,height:s.bottom-s.top,x:s.left,y:s.top}},convertOffsetParentRelativeRectToViewportRelativeRect:function(e){let{rect:t,offsetParent:n,strategy:r}=e,i=Uc(n),a=nl(n);if(n===a)return t;let o={scrollLeft:0,scrollTop:0},s={x:1,y:1},c={x:0,y:0};if((i||!i&&r!==`fixed`)&&((Bc(n)!==`body`||qc(a))&&(o=rl(n)),Uc(n))){let e=tl(n);s=el(n),c.x=e.x+n.clientLeft,c.y=e.y+n.clientTop}return{width:t.width*s.x,height:t.height*s.y,x:t.x*s.x-o.scrollLeft*s.x+c.x,y:t.y*s.y-o.scrollTop*s.y+c.y}},isElement:Wc,getDimensions:function(e){return Uc(e)?zc(e):e.getBoundingClientRect()},getOffsetParent:ul,getDocumentElement:nl,getScale:el,async getElementRects(e){let{reference:t,floating:n,strategy:r}=e,i=this.getOffsetParent||ul,a=this.getDimensions;return{reference:dl(t,await i(n),r),floating:{x:0,y:0,...await a(n)}}},getClientRects:e=>Array.from(e.getClientRects()),isRTL:e=>Fc(e).direction===`rtl`},pl=(e,t,n)=>{let r=new Map,i={platform:fl,...n},a={...i.platform,_c:r};return wc(e,t,{...i,platform:a})},ml={disabled:!1,distance:5,skidding:0,container:`body`,boundary:void 0,instantMove:!1,disposeTimeout:150,popperTriggers:[],strategy:`absolute`,preventOverflow:!0,flip:!0,shift:!0,overflowPadding:0,arrowPadding:0,arrowOverflow:!0,autoHideOnMousedown:!1,themes:{tooltip:{placement:`top`,triggers:[`hover`,`focus`,`touch`],hideTriggers:e=>[...e,`click`],delay:{show:200,hide:0},handleResize:!1,html:!1,loadingContent:`...`},dropdown:{placement:`bottom`,triggers:[`click`],delay:0,handleResize:!0,autoHide:!0},menu:{$extend:`dropdown`,triggers:[`hover`,`focus`],popperTriggers:[`hover`],delay:{show:0,hide:400}}}};function hl(e,t){let n=ml.themes[e]||{},r;do r=n[t],typeof r>`u`?n.$extend?n=ml.themes[n.$extend]||{}:(n=null,r=ml[t]):n=null;while(n);return r}function gl(e){let t=[e],n=ml.themes[e]||{};do n.$extend&&!n.$resetCss?(t.push(n.$extend),n=ml.themes[n.$extend]||{}):n=null;while(n);return t.map(e=>`v-popper--theme-${e}`)}function _l(e){let t=[e],n=ml.themes[e]||{};do n.$extend?(t.push(n.$extend),n=ml.themes[n.$extend]||{}):n=null;while(n);return t}var vl=!1;if(typeof window<`u`){vl=!1;try{let e=Object.defineProperty({},"passive",{get(){vl=!0}});window.addEventListener(`test`,null,e)}catch{}}var yl=!1;typeof window<`u`&&typeof navigator<`u`&&(yl=/iPad|iPhone|iPod/.test(navigator.userAgent)&&!window.MSStream);var bl=[`auto`,`top`,`bottom`,`left`,`right`].reduce((e,t)=>e.concat([t,`${t}-start`,`${t}-end`]),[]),xl={hover:`mouseenter`,focus:`focus`,click:`click`,touch:`touchstart`,pointer:`pointerdown`},Sl={hover:`mouseleave`,focus:`blur`,click:`click`,touch:`touchend`,pointer:`pointerup`};function Cl(e,t){let n=e.indexOf(t);n!==-1&&e.splice(n,1)}function wl(){return new Promise(e=>requestAnimationFrame(()=>{requestAnimationFrame(e)}))}var Tl=[],El=null,Dl={};function Ol(e){let t=Dl[e];return t||=Dl[e]=[],t}var kl=function(){};typeof window<`u`&&(kl=window.Element);function $(e){return function(t){return hl(t.theme,e)}}var Al=`__floating-vue__popper`,jl=()=>J({name:`VPopper`,provide(){return{[Al]:{parentPopper:this}}},inject:{[Al]:{default:null}},props:{theme:{type:String,required:!0},targetNodes:{type:Function,required:!0},referenceNode:{type:Function,default:null},popperNode:{type:Function,required:!0},shown:{type:Boolean,default:!1},showGroup:{type:String,default:null},ariaId:{default:null},disabled:{type:Boolean,default:$(`disabled`)},positioningDisabled:{type:Boolean,default:$(`positioningDisabled`)},placement:{type:String,default:$(`placement`),validator:e=>bl.includes(e)},delay:{type:[String,Number,Object],default:$(`delay`)},distance:{type:[Number,String],default:$(`distance`)},skidding:{type:[Number,String],default:$(`skidding`)},triggers:{type:Array,default:$(`triggers`)},showTriggers:{type:[Array,Function],default:$(`showTriggers`)},hideTriggers:{type:[Array,Function],default:$(`hideTriggers`)},popperTriggers:{type:Array,default:$(`popperTriggers`)},popperShowTriggers:{type:[Array,Function],default:$(`popperShowTriggers`)},popperHideTriggers:{type:[Array,Function],default:$(`popperHideTriggers`)},container:{type:[String,Object,kl,Boolean],default:$(`container`)},boundary:{type:[String,kl],default:$(`boundary`)},strategy:{type:String,validator:e=>[`absolute`,`fixed`].includes(e),default:$(`strategy`)},autoHide:{type:[Boolean,Function],default:$(`autoHide`)},handleResize:{type:Boolean,default:$(`handleResize`)},instantMove:{type:Boolean,default:$(`instantMove`)},eagerMount:{type:Boolean,default:$(`eagerMount`)},popperClass:{type:[String,Array,Object],default:$(`popperClass`)},computeTransformOrigin:{type:Boolean,default:$(`computeTransformOrigin`)},autoMinSize:{type:Boolean,default:$(`autoMinSize`)},autoSize:{type:[Boolean,String],default:$(`autoSize`)},autoMaxSize:{type:Boolean,default:$(`autoMaxSize`)},autoBoundaryMaxSize:{type:Boolean,default:$(`autoBoundaryMaxSize`)},preventOverflow:{type:Boolean,default:$(`preventOverflow`)},overflowPadding:{type:[Number,String],default:$(`overflowPadding`)},arrowPadding:{type:[Number,String],default:$(`arrowPadding`)},arrowOverflow:{type:Boolean,default:$(`arrowOverflow`)},flip:{type:Boolean,default:$(`flip`)},shift:{type:Boolean,default:$(`shift`)},shiftCrossAxis:{type:Boolean,default:$(`shiftCrossAxis`)},noAutoFocus:{type:Boolean,default:$(`noAutoFocus`)},disposeTimeout:{type:Number,default:$(`disposeTimeout`)}},emits:{show:()=>!0,hide:()=>!0,"update:shown":e=>!0,"apply-show":()=>!0,"apply-hide":()=>!0,"close-group":()=>!0,"close-directive":()=>!0,"auto-hide":()=>!0,resize:()=>!0},data(){return{isShown:!1,isMounted:!1,skipTransition:!1,classes:{showFrom:!1,showTo:!1,hideFrom:!1,hideTo:!0},result:{x:0,y:0,placement:``,strategy:this.strategy,arrow:{x:0,y:0,centerOffset:0},transformOrigin:null},randomId:`popper_${[Math.random(),Date.now()].map(e=>e.toString(36).substring(2,10)).join(`_`)}`,shownChildren:new Set,lastAutoHide:!0,pendingHide:!1,containsGlobalTarget:!1,isDisposed:!0,mouseDownContains:!1}},computed:{popperId(){return this.ariaId==null?this.randomId:this.ariaId},shouldMountContent(){return this.eagerMount||this.isMounted},slotData(){return{popperId:this.popperId,isShown:this.isShown,shouldMountContent:this.shouldMountContent,skipTransition:this.skipTransition,autoHide:typeof this.autoHide==`function`?this.lastAutoHide:this.autoHide,show:this.show,hide:this.hide,handleResize:this.handleResize,onResize:this.onResize,classes:{...this.classes,popperClass:this.popperClass},result:this.positioningDisabled?null:this.result,attrs:this.$attrs}},parentPopper(){return this[Al]?.parentPopper},hasPopperShowTriggerHover(){return this.popperTriggers?.includes(`hover`)||this.popperShowTriggers?.includes(`hover`)}},watch:{shown:`$_autoShowHide`,disabled(e){e?this.dispose():this.init()},async container(){this.isShown&&(this.$_ensureTeleport(),await this.$_computePosition())},triggers:{handler:`$_refreshListeners`,deep:!0},positioningDisabled:`$_refreshListeners`,...[`placement`,`distance`,`skidding`,`boundary`,`strategy`,`overflowPadding`,`arrowPadding`,`preventOverflow`,`shift`,`shiftCrossAxis`,`flip`].reduce((e,t)=>(e[t]=`$_computePosition`,e),{})},created(){this.autoMinSize&&console.warn('[floating-vue] `autoMinSize` option is deprecated. Use `autoSize="min"` instead.'),this.autoMaxSize&&console.warn("[floating-vue] `autoMaxSize` option is deprecated. Use `autoBoundaryMaxSize` instead.")},mounted(){this.init(),this.$_detachPopperNode()},activated(){this.$_autoShowHide()},deactivated(){this.hide()},beforeUnmount(){this.dispose()},methods:{show({event:e=null,skipDelay:t=!1,force:n=!1}={}){var r;(r=this.parentPopper)!=null&&r.lockedChild&&this.parentPopper.lockedChild!==this||(this.pendingHide=!1,(n||!this.disabled)&&(this.parentPopper?.lockedChild===this&&(this.parentPopper.lockedChild=null),this.$_scheduleShow(e,t),this.$emit(`show`),this.$_showFrameLocked=!0,requestAnimationFrame(()=>{this.$_showFrameLocked=!1})),this.$emit(`update:shown`,!0))},hide({event:e=null,skipDelay:t=!1}={}){if(!this.$_hideInProgress){if(this.shownChildren.size>0){this.pendingHide=!0;return}if(this.hasPopperShowTriggerHover&&this.$_isAimingPopper()){this.parentPopper&&(this.parentPopper.lockedChild=this,clearTimeout(this.parentPopper.lockedChildTimer),this.parentPopper.lockedChildTimer=setTimeout(()=>{this.parentPopper.lockedChild===this&&(this.parentPopper.lockedChild.hide({skipDelay:t}),this.parentPopper.lockedChild=null)},1e3));return}this.parentPopper?.lockedChild===this&&(this.parentPopper.lockedChild=null),this.pendingHide=!1,this.$_scheduleHide(e,t),this.$emit(`hide`),this.$emit(`update:shown`,!1)}},init(){this.isDisposed&&(this.isDisposed=!1,this.isMounted=!1,this.$_events=[],this.$_preventShow=!1,this.$_referenceNode=this.referenceNode?.call(this)??this.$el,this.$_targetNodes=this.targetNodes().filter(e=>e.nodeType===e.ELEMENT_NODE),this.$_popperNode=this.popperNode(),this.$_innerNode=this.$_popperNode.querySelector(`.v-popper__inner`),this.$_arrowNode=this.$_popperNode.querySelector(`.v-popper__arrow-container`),this.$_swapTargetAttrs(`title`,`data-original-title`),this.$_detachPopperNode(),this.triggers.length&&this.$_addEventListeners(),this.shown&&this.show())},dispose(){this.isDisposed||(this.isDisposed=!0,this.$_removeEventListeners(),this.hide({skipDelay:!0}),this.$_detachPopperNode(),this.isMounted=!1,this.isShown=!1,this.$_updateParentShownChildren(!1),this.$_swapTargetAttrs(`data-original-title`,`title`))},async onResize(){this.isShown&&(await this.$_computePosition(),this.$emit(`resize`))},async $_computePosition(){if(this.isDisposed||this.positioningDisabled)return;let e={strategy:this.strategy,middleware:[]};(this.distance||this.skidding)&&e.middleware.push(jc({mainAxis:this.distance,crossAxis:this.skidding}));let t=this.placement.startsWith(`auto`);if(t?e.middleware.push(Dc({alignment:this.placement.split(`-`)[1]??``})):e.placement=this.placement,this.preventOverflow&&(this.shift&&e.middleware.push(Mc({padding:this.overflowPadding,boundary:this.boundary,crossAxis:this.shiftCrossAxis})),!t&&this.flip&&e.middleware.push(Oc({padding:this.overflowPadding,boundary:this.boundary}))),e.middleware.push(Tc({element:this.$_arrowNode,padding:this.arrowPadding})),this.arrowOverflow&&e.middleware.push({name:`arrowOverflow`,fn:({placement:e,rects:t,middlewareData:n})=>{let r,{centerOffset:i}=n.arrow;return r=e.startsWith(`top`)||e.startsWith(`bottom`)?Math.abs(i)>t.reference.width/2:Math.abs(i)>t.reference.height/2,{data:{overflow:r}}}}),this.autoMinSize||this.autoSize){let t=this.autoSize?this.autoSize:this.autoMinSize?`min`:null;e.middleware.push({name:`autoSize`,fn:({rects:e,placement:n,middlewareData:r})=>{var i;if((i=r.autoSize)!=null&&i.skip)return{};let a,o;return n.startsWith(`top`)||n.startsWith(`bottom`)?a=e.reference.width:o=e.reference.height,this.$_innerNode.style[t===`min`?`minWidth`:t===`max`?`maxWidth`:`width`]=a==null?null:`${a}px`,this.$_innerNode.style[t===`min`?`minHeight`:t===`max`?`maxHeight`:`height`]=o==null?null:`${o}px`,{data:{skip:!0},reset:{rects:!0}}}})}(this.autoMaxSize||this.autoBoundaryMaxSize)&&(this.$_innerNode.style.maxWidth=null,this.$_innerNode.style.maxHeight=null,e.middleware.push(Nc({boundary:this.boundary,padding:this.overflowPadding,apply:({availableWidth:e,availableHeight:t})=>{this.$_innerNode.style.maxWidth=e==null?null:`${e}px`,this.$_innerNode.style.maxHeight=t==null?null:`${t}px`}})));let n=await pl(this.$_referenceNode,this.$_popperNode,e);Object.assign(this.result,{x:n.x,y:n.y,placement:n.placement,strategy:n.strategy,arrow:{...n.middlewareData.arrow,...n.middlewareData.arrowOverflow}})},$_scheduleShow(e,t=!1){if(this.$_updateParentShownChildren(!0),this.$_hideInProgress=!1,clearTimeout(this.$_scheduleTimer),El&&this.instantMove&&El.instantMove&&El!==this.parentPopper){El.$_applyHide(!0),this.$_applyShow(!0);return}t?this.$_applyShow():this.$_scheduleTimer=setTimeout(this.$_applyShow.bind(this),this.$_computeDelay(`show`))},$_scheduleHide(e,t=!1){if(this.shownChildren.size>0){this.pendingHide=!0;return}this.$_updateParentShownChildren(!1),this.$_hideInProgress=!0,clearTimeout(this.$_scheduleTimer),this.isShown&&(El=this),t?this.$_applyHide():this.$_scheduleTimer=setTimeout(this.$_applyHide.bind(this),this.$_computeDelay(`hide`))},$_computeDelay(e){let t=this.delay;return parseInt(t&&t[e]||t||0)},async $_applyShow(e=!1){clearTimeout(this.$_disposeTimer),clearTimeout(this.$_scheduleTimer),this.skipTransition=e,!this.isShown&&(this.$_ensureTeleport(),await wl(),await this.$_computePosition(),await this.$_applyShowEffect(),this.positioningDisabled||this.$_registerEventListeners([...sl(this.$_referenceNode),...sl(this.$_popperNode)],`scroll`,()=>{this.$_computePosition()}))},async $_applyShowEffect(){if(this.$_hideInProgress)return;if(this.computeTransformOrigin){let e=this.$_referenceNode.getBoundingClientRect(),t=this.$_popperNode.querySelector(`.v-popper__wrapper`),n=t.parentNode.getBoundingClientRect(),r=e.x+e.width/2-(n.left+t.offsetLeft),i=e.y+e.height/2-(n.top+t.offsetTop);this.result.transformOrigin=`${r}px ${i}px`}this.isShown=!0,this.$_applyAttrsToTarget({"aria-describedby":this.popperId,"data-popper-shown":``});let e=this.showGroup;if(e){let t;for(let n=0;n0){this.pendingHide=!0,this.$_hideInProgress=!1;return}if(clearTimeout(this.$_scheduleTimer),!this.isShown)return;this.skipTransition=e,Cl(Tl,this),Tl.length===0&&document.body.classList.remove(`v-popper--some-open`);for(let e of _l(this.theme)){let t=Ol(e);Cl(t,this),t.length===0&&document.body.classList.remove(`v-popper--some-open--${e}`)}El===this&&(El=null),this.isShown=!1,this.$_applyAttrsToTarget({"aria-describedby":void 0,"data-popper-shown":void 0}),clearTimeout(this.$_disposeTimer);let t=this.disposeTimeout;t!==null&&(this.$_disposeTimer=setTimeout(()=>{this.$_popperNode&&(this.$_detachPopperNode(),this.isMounted=!1)},t)),this.$_removeEventListeners(`scroll`),this.$emit(`apply-hide`),this.classes.showFrom=!1,this.classes.showTo=!1,this.classes.hideFrom=!0,this.classes.hideTo=!1,await wl(),this.classes.hideFrom=!1,this.classes.hideTo=!0},$_autoShowHide(){this.shown?this.show():this.hide()},$_ensureTeleport(){if(this.isDisposed)return;let e=this.container;if(typeof e==`string`?e=window.document.querySelector(e):e===!1&&(e=this.$_targetNodes[0].parentNode),!e)throw Error(`No container for popover: `+this.container);e.appendChild(this.$_popperNode),this.isMounted=!0},$_addEventListeners(){let e=e=>{this.isShown&&!this.$_hideInProgress||(e.usedByTooltip=!0,!this.$_preventShow&&this.show({event:e}))};this.$_registerTriggerListeners(this.$_targetNodes,xl,this.triggers,this.showTriggers,e),this.$_registerTriggerListeners([this.$_popperNode],xl,this.popperTriggers,this.popperShowTriggers,e);let t=e=>{e.usedByTooltip||this.hide({event:e})};this.$_registerTriggerListeners(this.$_targetNodes,Sl,this.triggers,this.hideTriggers,t),this.$_registerTriggerListeners([this.$_popperNode],Sl,this.popperTriggers,this.popperHideTriggers,t)},$_registerEventListeners(e,t,n){this.$_events.push({targetNodes:e,eventType:t,handler:n}),e.forEach(e=>e.addEventListener(t,n,vl?{passive:!0}:void 0))},$_registerTriggerListeners(e,t,n,r,i){let a=n;r!=null&&(a=typeof r==`function`?r(a):r),a.forEach(n=>{let r=t[n];r&&this.$_registerEventListeners(e,r,i)})},$_removeEventListeners(e){let t=[];this.$_events.forEach(n=>{let{targetNodes:r,eventType:i,handler:a}=n;!e||e===i?r.forEach(e=>e.removeEventListener(i,a)):t.push(n)}),this.$_events=t},$_refreshListeners(){this.isDisposed||(this.$_removeEventListeners(),this.$_addEventListeners())},$_handleGlobalClose(e,t=!1){this.$_showFrameLocked||(this.hide({event:e}),e.closePopover?this.$emit(`close-directive`):this.$emit(`auto-hide`),t&&(this.$_preventShow=!0,setTimeout(()=>{this.$_preventShow=!1},300)))},$_detachPopperNode(){this.$_popperNode.parentNode&&this.$_popperNode.parentNode.removeChild(this.$_popperNode)},$_swapTargetAttrs(e,t){for(let n of this.$_targetNodes){let r=n.getAttribute(e);r&&(n.removeAttribute(e),n.setAttribute(t,r))}},$_applyAttrsToTarget(e){for(let t of this.$_targetNodes)for(let n in e){let r=e[n];r==null?t.removeAttribute(n):t.setAttribute(n,r)}},$_updateParentShownChildren(e){let t=this.parentPopper;for(;t;)e?t.shownChildren.add(this.randomId):(t.shownChildren.delete(this.randomId),t.pendingHide&&t.hide()),t=t.parentPopper},$_isAimingPopper(){let e=this.$_referenceNode.getBoundingClientRect();if(Bl>=e.left&&Bl<=e.right&&Vl>=e.top&&Vl<=e.bottom){let e=this.$_popperNode.getBoundingClientRect(),t=Bl-Rl,n=Vl-zl,r=e.left+e.width/2-Rl+(e.top+e.height/2)-zl+e.width+e.height,i=Rl+t*r,a=zl+n*r;return Hl(Rl,zl,i,a,e.left,e.top,e.left,e.bottom)||Hl(Rl,zl,i,a,e.left,e.top,e.right,e.top)||Hl(Rl,zl,i,a,e.right,e.top,e.right,e.bottom)||Hl(Rl,zl,i,a,e.left,e.bottom,e.right,e.bottom)}return!1}},render(){return this.$slots.default(this.slotData)}});if(typeof document<`u`&&typeof window<`u`){if(yl){let e=!vl||{passive:!0,capture:!0};document.addEventListener(`touchstart`,e=>Ml(e,!0),e),document.addEventListener(`touchend`,e=>Nl(e,!0),e)}else window.addEventListener(`mousedown`,e=>Ml(e,!1),!0),window.addEventListener(`click`,e=>Nl(e,!1),!0);window.addEventListener(`resize`,Ll)}function Ml(e,t){if(ml.autoHideOnMousedown)Pl(e,t);else for(let t=0;t=0;r--){let i=Tl[r];try{let r=i.containsGlobalTarget=i.mouseDownContains||i.popperNode().contains(e.target);i.pendingHide=!1,requestAnimationFrame(()=>{if(i.pendingHide=!1,!n[i.randomId]&&Fl(i,r,e)){if(i.$_handleGlobalClose(e,t),!e.closeAllPopover&&e.closePopover&&r){let e=i.parentPopper;for(;e;)n[e.randomId]=!0,e=e.parentPopper;return}let a=i.parentPopper;for(;a&&Fl(a,a.containsGlobalTarget,e);)a.$_handleGlobalClose(e,t),a=a.parentPopper}})}catch{}}}function Fl(e,t,n){return n.closeAllPopover||n.closePopover&&t||Il(e,n)&&!t}function Il(e,t){if(typeof e.autoHide==`function`){let n=e.autoHide(t);return e.lastAutoHide=n,n}return e.autoHide}function Ll(){for(let e=0;e{Rl=Bl,zl=Vl,Bl=e.clientX,Vl=e.clientY},vl?{passive:!0}:void 0);function Hl(e,t,n,r,i,a,o,s){let c=((o-i)*(t-a)-(s-a)*(e-i))/((s-a)*(n-e)-(o-i)*(r-t)),l=((n-e)*(t-a)-(r-t)*(e-i))/((s-a)*(n-e)-(o-i)*(r-t));return c>=0&&c<=1&&l>=0&&l<=1}var Ul={extends:jl()},Wl=(e,t)=>{let n=e.__vccOpts||e;for(let[e,r]of t)n[e]=r;return n};function Gl(e,t,n,r,i,a){return G(),j(`div`,{ref:`reference`,class:V([`v-popper`,{"v-popper--shown":e.slotData.isShown}])},[Pe(e.$slots,`default`,Ue(_e(e.slotData)))],2)}var Kl=Wl(Ul,[[`render`,Gl]]);function ql(){var e=window.navigator.userAgent,t=e.indexOf(`MSIE `);if(t>0)return parseInt(e.substring(t+5,e.indexOf(`.`,t)),10);if(e.indexOf(`Trident/`)>0){var n=e.indexOf(`rv:`);return parseInt(e.substring(n+3,e.indexOf(`.`,n)),10)}var r=e.indexOf(`Edge/`);return r>0?parseInt(e.substring(r+5,e.indexOf(`.`,r)),10):-1}var Jl;function Yl(){Yl.init||(Yl.init=!0,Jl=ql()!==-1)}var Xl={name:`ResizeObserver`,props:{emitOnMount:{type:Boolean,default:!1},ignoreWidth:{type:Boolean,default:!1},ignoreHeight:{type:Boolean,default:!1}},emits:[`notify`],mounted(){Yl(),Ae(()=>{this._w=this.$el.offsetWidth,this._h=this.$el.offsetHeight,this.emitOnMount&&this.emitSize()});let e=document.createElement(`object`);this._resizeObject=e,e.setAttribute(`aria-hidden`,`true`),e.setAttribute(`tabindex`,-1),e.onload=this.addResizeHandlers,e.type=`text/html`,Jl&&this.$el.appendChild(e),e.data=`about:blank`,Jl||this.$el.appendChild(e)},beforeUnmount(){this.removeResizeHandlers()},methods:{compareAndNotify(){(!this.ignoreWidth&&this._w!==this.$el.offsetWidth||!this.ignoreHeight&&this._h!==this.$el.offsetHeight)&&(this._w=this.$el.offsetWidth,this._h=this.$el.offsetHeight,this.emitSize())},emitSize(){this.$emit(`notify`,{width:this._w,height:this._h})},addResizeHandlers(){this._resizeObject.contentDocument.defaultView.addEventListener(`resize`,this.compareAndNotify),this.compareAndNotify()},removeResizeHandlers(){this._resizeObject&&this._resizeObject.onload&&(!Jl&&this._resizeObject.contentDocument&&this._resizeObject.contentDocument.defaultView.removeEventListener(`resize`,this.compareAndNotify),this.$el.removeChild(this._resizeObject),this._resizeObject.onload=null,this._resizeObject=null)}}},Zl=et(`data-v-b329ee4c`);Ne(`data-v-b329ee4c`);var Ql={class:`resize-observer`,tabindex:`-1`};Ce(),Xl.render=Zl((e,t,n,r,i,a)=>(G(),F(`div`,Ql))),Xl.__scopeId=`data-v-b329ee4c`,Xl.__file=`src/components/ResizeObserver.vue`;var $l=(e=`theme`)=>({computed:{themeClass(){return gl(this[e])}}}),eu=J({name:`VPopperContent`,components:{ResizeObserver:Xl},mixins:[$l()],props:{popperId:String,theme:String,shown:Boolean,mounted:Boolean,skipTransition:Boolean,autoHide:Boolean,handleResize:Boolean,classes:Object,result:Object},emits:[`hide`,`resize`],methods:{toPx(e){return e!=null&&!isNaN(e)?`${e}px`:null}}}),tu=[`id`,`aria-hidden`,`tabindex`,`data-popper-placement`],nu={ref:`inner`,class:`v-popper__inner`},ru=[P(`div`,{class:`v-popper__arrow-outer`},null,-1),P(`div`,{class:`v-popper__arrow-inner`},null,-1)];function iu(e,t,n,r,i,a){let o=De(`ResizeObserver`);return G(),j(`div`,{id:e.popperId,ref:`popover`,class:V([`v-popper__popper`,[e.themeClass,e.classes.popperClass,{"v-popper__popper--shown":e.shown,"v-popper__popper--hidden":!e.shown,"v-popper__popper--show-from":e.classes.showFrom,"v-popper__popper--show-to":e.classes.showTo,"v-popper__popper--hide-from":e.classes.hideFrom,"v-popper__popper--hide-to":e.classes.hideTo,"v-popper__popper--skip-transition":e.skipTransition,"v-popper__popper--arrow-overflow":e.result&&e.result.arrow.overflow,"v-popper__popper--no-positioning":!e.result}]]),style:R(e.result?{position:e.result.strategy,transform:`translate3d(${Math.round(e.result.x)}px,${Math.round(e.result.y)}px,0)`}:void 0),"aria-hidden":e.shown?`false`:`true`,tabindex:e.autoHide?0:void 0,"data-popper-placement":e.result?e.result.placement:void 0,onKeyup:t[2]||=ye(t=>e.autoHide&&e.$emit(`hide`),[`esc`])},[P(`div`,{class:`v-popper__backdrop`,onClick:t[0]||=t=>e.autoHide&&e.$emit(`hide`)}),P(`div`,{class:`v-popper__wrapper`,style:R(e.result?{transformOrigin:e.result.transformOrigin}:void 0)},[P(`div`,nu,[e.mounted?(G(),j(H,{key:0},[P(`div`,null,[Pe(e.$slots,`default`)]),e.handleResize?(G(),F(o,{key:0,onNotify:t[1]||=t=>e.$emit(`resize`,t)})):A(``,!0)],64)):A(``,!0)],512),P(`div`,{ref:`arrow`,class:`v-popper__arrow-container`,style:R(e.result?{left:e.toPx(e.result.arrow.x),top:e.toPx(e.result.arrow.y)}:void 0)},ru,4)],4)],46,tu)}var au=Wl(eu,[[`render`,iu]]),ou={methods:{show(...e){return this.$refs.popper.show(...e)},hide(...e){return this.$refs.popper.hide(...e)},dispose(...e){return this.$refs.popper.dispose(...e)},onResize(...e){return this.$refs.popper.onResize(...e)}}},su=function(){};typeof window<`u`&&(su=window.Element);var cu=J({name:`VPopperWrapper`,components:{Popper:Kl,PopperContent:au},mixins:[ou,$l(`finalTheme`)],props:{theme:{type:String,default:null},referenceNode:{type:Function,default:null},shown:{type:Boolean,default:!1},showGroup:{type:String,default:null},ariaId:{default:null},disabled:{type:Boolean,default:void 0},positioningDisabled:{type:Boolean,default:void 0},placement:{type:String,default:void 0},delay:{type:[String,Number,Object],default:void 0},distance:{type:[Number,String],default:void 0},skidding:{type:[Number,String],default:void 0},triggers:{type:Array,default:void 0},showTriggers:{type:[Array,Function],default:void 0},hideTriggers:{type:[Array,Function],default:void 0},popperTriggers:{type:Array,default:void 0},popperShowTriggers:{type:[Array,Function],default:void 0},popperHideTriggers:{type:[Array,Function],default:void 0},container:{type:[String,Object,su,Boolean],default:void 0},boundary:{type:[String,su],default:void 0},strategy:{type:String,default:void 0},autoHide:{type:[Boolean,Function],default:void 0},handleResize:{type:Boolean,default:void 0},instantMove:{type:Boolean,default:void 0},eagerMount:{type:Boolean,default:void 0},popperClass:{type:[String,Array,Object],default:void 0},computeTransformOrigin:{type:Boolean,default:void 0},autoMinSize:{type:Boolean,default:void 0},autoSize:{type:[Boolean,String],default:void 0},autoMaxSize:{type:Boolean,default:void 0},autoBoundaryMaxSize:{type:Boolean,default:void 0},preventOverflow:{type:Boolean,default:void 0},overflowPadding:{type:[Number,String],default:void 0},arrowPadding:{type:[Number,String],default:void 0},arrowOverflow:{type:Boolean,default:void 0},flip:{type:Boolean,default:void 0},shift:{type:Boolean,default:void 0},shiftCrossAxis:{type:Boolean,default:void 0},noAutoFocus:{type:Boolean,default:void 0},disposeTimeout:{type:Number,default:void 0}},emits:{show:()=>!0,hide:()=>!0,"update:shown":e=>!0,"apply-show":()=>!0,"apply-hide":()=>!0,"close-group":()=>!0,"close-directive":()=>!0,"auto-hide":()=>!0,resize:()=>!0},computed:{finalTheme(){return this.theme??this.$options.vPopperTheme}},methods:{getTargetNodes(){return Array.from(this.$el.children).filter(e=>e!==this.$refs.popperContent.$el)}}});function lu(e,t,n,r,i,a){let o=De(`PopperContent`),s=De(`Popper`);return G(),F(s,Se({ref:`popper`},e.$props,{theme:e.finalTheme,"target-nodes":e.getTargetNodes,"popper-node":()=>e.$refs.popperContent.$el,class:[e.themeClass],onShow:t[0]||=()=>e.$emit(`show`),onHide:t[1]||=()=>e.$emit(`hide`),"onUpdate:shown":t[2]||=t=>e.$emit(`update:shown`,t),onApplyShow:t[3]||=()=>e.$emit(`apply-show`),onApplyHide:t[4]||=()=>e.$emit(`apply-hide`),onCloseGroup:t[5]||=()=>e.$emit(`close-group`),onCloseDirective:t[6]||=()=>e.$emit(`close-directive`),onAutoHide:t[7]||=()=>e.$emit(`auto-hide`),onResize:t[8]||=()=>e.$emit(`resize`)}),{default:z(({popperId:t,isShown:n,shouldMountContent:r,skipTransition:i,autoHide:a,show:s,hide:c,handleResize:l,onResize:u,classes:d,result:f})=>[Pe(e.$slots,`default`,{shown:n,show:s,hide:c}),I(o,{ref:`popperContent`,"popper-id":t,theme:e.finalTheme,shown:n,mounted:r,"skip-transition":i,"auto-hide":a,"handle-resize":l,classes:d,result:f,onHide:c,onResize:u},{default:z(()=>[Pe(e.$slots,`popper`,{shown:n,hide:c})]),_:2},1032,[`popper-id`,`theme`,`shown`,`mounted`,`skip-transition`,`auto-hide`,`handle-resize`,`classes`,`result`,`onHide`,`onResize`])]),_:3},16,[`theme`,`target-nodes`,`popper-node`,`class`])}var uu=Wl(cu,[[`render`,lu]]);({...uu}),{...uu},{...uu};var du=J({name:`VTooltipDirective`,components:{Popper:jl(),PopperContent:au},mixins:[ou],inheritAttrs:!1,props:{theme:{type:String,default:`tooltip`},html:{type:Boolean,default:e=>hl(e.theme,`html`)},content:{type:[String,Number,Function],default:null},loadingContent:{type:String,default:e=>hl(e.theme,`loadingContent`)},targetNodes:{type:Function,required:!0}},data(){return{asyncContent:null}},computed:{isContentAsync(){return typeof this.content==`function`},loading(){return this.isContentAsync&&this.asyncContent==null},finalContent(){return this.isContentAsync?this.loading?this.loadingContent:this.asyncContent:this.content}},watch:{content:{handler(){this.fetchContent(!0)},immediate:!0},async finalContent(){await this.$nextTick(),this.$refs.popper.onResize()}},created(){this.$_fetchId=0},methods:{fetchContent(e){if(typeof this.content==`function`&&this.$_isShown&&(e||!this.$_loading&&this.asyncContent==null)){this.asyncContent=null,this.$_loading=!0;let e=++this.$_fetchId,t=this.content(this);t.then?t.then(t=>this.onResult(e,t)):this.onResult(e,t)}},onResult(e,t){e===this.$_fetchId&&(this.$_loading=!1,this.asyncContent=t)},onShow(){this.$_isShown=!0,this.fetchContent()},onHide(){this.$_isShown=!1}}}),fu=[`innerHTML`],pu=[`textContent`];function mu(e,t,n,r,i,a){let o=De(`PopperContent`),s=De(`Popper`);return G(),F(s,Se({ref:`popper`},e.$attrs,{theme:e.theme,"target-nodes":e.targetNodes,"popper-node":()=>e.$refs.popperContent.$el,onApplyShow:e.onShow,onApplyHide:e.onHide}),{default:z(({popperId:t,isShown:n,shouldMountContent:r,skipTransition:i,autoHide:a,hide:s,handleResize:c,onResize:l,classes:u,result:d})=>[I(o,{ref:`popperContent`,class:V({"v-popper--tooltip-loading":e.loading}),"popper-id":t,theme:e.theme,shown:n,mounted:r,"skip-transition":i,"auto-hide":a,"handle-resize":c,classes:u,result:d,onHide:s,onResize:l},{default:z(()=>[e.html?(G(),j(`div`,{key:0,innerHTML:e.finalContent},null,8,fu)):(G(),j(`div`,{key:1,textContent:q(e.finalContent)},null,8,pu))]),_:2},1032,[`class`,`popper-id`,`theme`,`shown`,`mounted`,`skip-transition`,`auto-hide`,`handle-resize`,`classes`,`result`,`onHide`,`onResize`])]),_:1},16,[`theme`,`target-nodes`,`popper-node`,`onApplyShow`,`onApplyHide`])}var hu=Wl(du,[[`render`,mu]]),gu=`v-popper--has-tooltip`;function _u(e,t){let n=e.placement;if(!n&&t)for(let e of bl)t[e]&&(n=e);return n||=hl(e.theme||`tooltip`,`placement`),n}function vu(e,t,n){let r,i=typeof t;return r=i===`string`?{content:t}:t&&i===`object`?t:{content:!1},r.placement=_u(r,n),r.targetNodes=()=>[e],r.referenceNode=()=>e,r}var yu,bu,xu=0;function Su(){if(yu)return;bu=B([]),yu=Oe({name:`VTooltipDirectiveApp`,setup(){return{directives:bu}},render(){return this.directives.map(e=>je(hu,{...e.options,shown:e.shown||e.options.shown,key:e.id}))},devtools:{hide:!0}});let e=document.createElement(`div`);document.body.appendChild(e),yu.mount(e)}function Cu(e,t,n){Su();let r=B(vu(e,t,n)),i=B(!1),a={id:xu++,options:r,shown:i};return bu.value.push(a),e.classList&&e.classList.add(gu),e.$_popper={options:r,item:a,show(){i.value=!0},hide(){i.value=!1}}}function wu(e){if(e.$_popper){let t=bu.value.indexOf(e.$_popper.item);t!==-1&&bu.value.splice(t,1),delete e.$_popper,delete e.$_popperOldShown,delete e.$_popperMountTarget}e.classList&&e.classList.remove(gu)}function Tu(e,{value:t,modifiers:n}){let r=vu(e,t,n);if(!r.content||hl(r.theme||`tooltip`,`disabled`))wu(e);else{let i;e.$_popper?(i=e.$_popper,i.options.value=r):i=Cu(e,t,n),typeof t.shown<`u`&&t.shown!==e.$_popperOldShown&&(e.$_popperOldShown=t.shown,t.shown?i.show():i.hide())}}var Eu={beforeMount:Tu,updated:Tu,beforeUnmount(e){wu(e)}},Du=J({__name:`ActionIconButton`,props:{as:{},to:{},href:{},icon:{},tooltip:{},active:{type:Boolean},disabled:{type:Boolean},label:{},compact:{type:Boolean},activeClass:{}},setup(e){let t=e,n=N(()=>t.as?t.as:t.href!=null||t.to!=null?`a`:`button`),r=N(()=>n.value===`a`),i=N(()=>n.value===`button`),a=N(()=>t.compact?`btn-icon-compact`:`btn-icon`),o=N(()=>t.compact?`w-auto! h-auto! p-[0.25em]`:`w-auto! h-auto! p-[0.5em]`),s=N(()=>t.active?t.activeClass||`color-active bg-active op100`:``);return(t,c)=>Ze((G(),F(he(n.value),{type:i.value?`button`:void 0,class:V([a.value,o.value,s.value,{"pointer-events-none op-mute":e.disabled&&!i.value}]),href:r.value?e.href??e.to:void 0,to:e.as&&e.to!=null?e.to:void 0,disabled:i.value?e.disabled:void 0,"aria-disabled":e.disabled&&!i.value||void 0,"aria-label":e.label??e.tooltip,"aria-pressed":e.active||void 0},{default:z(()=>[e.icon?(G(),j(`span`,{key:0,class:V(e.icon),"aria-hidden":`true`},null,2)):A(``,!0),Pe(t.$slots,`default`),Pe(t.$slots,`badge`)]),_:3},8,[`type`,`class`,`href`,`to`,`disabled`,`aria-disabled`,`aria-label`,`aria-pressed`])),[[U(Eu),e.tooltip]])}}),Ou={class:`devframes-view-assets-error absolute inset-0 flex items-center justify-center overflow-auto bg-base p6`},ku={class:`w-full max-w-100 flex flex-col items-center gap-2 text-center`},Au={class:`text-xs color-faint`},ju={class:`font-mono`},Mu={class:`mt-2 w-full flex items-center gap-1 border border-base rounded bg-secondary py1 pl2.5 pr1`},Nu={class:`flex-1 truncate text-left text-xs font-mono color-base`},Pu={class:`w-full max-h-24 overflow-auto text-left text-xs font-mono color-faint break-words`},Fu=J({__name:`ViewAssetsError`,props:{error:{}},emits:[`retry`],setup(e,{emit:t}){let n=e,r=t,i=N(()=>`npm install ${n.error.package}@${n.error.version}`),a=B(!1),o;async function s(){try{await navigator.clipboard.writeText(i.value),a.value=!0,clearTimeout(o),o=setTimeout(()=>a.value=!1,1500)}catch{}}return(t,n)=>(G(),j(`div`,Ou,[P(`div`,ku,[n[5]||=P(`div`,{class:`i-ph:cloud-warning-duotone text-3xl color-faint`},null,-1),n[6]||=P(`div`,{class:`text-sm color-muted`},` Failed to load assets remotely `,-1),P(`div`,Au,[n[2]||=M(` This tool's UI is published as `,-1),P(`span`,ju,q(e.error.package)+`@`+q(e.error.version),1),n[3]||=M(`. Install it locally, or enable network access to continue. `,-1)]),P(`div`,Mu,[P(`code`,Nu,q(i.value),1),I(Du,{class:`text-sm`,icon:a.value?`i-ph:check`:`i-ph:copy`,label:a.value?`Copied`:`Copy install command`,onClick:n[0]||=e=>s()},null,8,[`icon`,`label`])]),P(`div`,Pu,q(e.error.reason),1),I(ma,{class:`mt-2`,onClick:n[1]||=e=>r(`retry`)},{default:z(()=>[...n[4]||=[M(` Retry `,-1)]]),_:1})])]))}}),Iu={class:`devframes-view-iframe-loading absolute inset-0 flex flex-col items-center justify-center gap-2`},Lu=J({__name:`ViewIframeLoading`,setup(e){return(e,t)=>(G(),j(`div`,Iu,[...t[0]||=[P(`div`,{class:`i-ph:circle-notch-duotone animate-spin text-3xl color-faint`},null,-1),P(`div`,{class:`text-sm color-muted`},` Loading… `,-1)]]))}}),Ru={class:`w-full h-full flex flex-col`},zu={key:1,class:`flex items-center gap-1 px2 py1 rounded text-xs bg-amber/10 text-amber border border-amber/20 shrink-0`,title:`Cross-origin iframe`},Bu={class:`flex-1 flex items-center h-7 px-2.5 rounded bg-gray/5 border border-transparent hover:border-gray/10 focus-within:border-gray/15 transition-colors`},Vu=[`value`,`readonly`],Hu={key:0,class:`i-ph-circle-notch text-sm op40 ml-2 shrink-0 animate-spin`},Uu=40,Wu=J({__name:`ViewIframe`,props:{context:{},entry:{},panes:{},iframeStyle:{}},setup(e){let t=e,n=oe(t.context),r=N(()=>t.context.panel.store.mode===`edge`),i=N(()=>typeof t.entry.addressBar==`object`?t.entry.addressBar:void 0),a=N(()=>n.value.showIframeAddressBar||!!t.entry.addressBar),o=B(!0),s=B(!1),c=B(!1),l=B(null),u=N(()=>!l.value&&(o.value||s.value)),d=K(`viewFrame`),f=K(`urlInput`),p=B(t.entry.url),m=B(t.entry.url),h=B(!1),g=N(()=>t.entry.frameId??t.entry.id),_=N(()=>t.panes.get(g.value)?.iframe),v=N(()=>{try{return window.location.origin}catch{return``}}),y=N(()=>{try{return new URL(p.value).origin!==v.value}catch{return!0}}),b=N(()=>i.value?.back??!y.value),x=N(()=>i.value?.reload??!y.value),S=N(()=>i.value?.openExternal??!1),C=N(()=>{let e=re(p.value);if(y.value)return e;try{let t=new URL(e);return t.pathname+t.search+t.hash}catch{return e}});function w(e){let t=e.data;typeof t!=`object`||!t||t.type!==ce||e.source===_.value?.contentWindow&&(l.value={type:ce,package:String(t.package??``),version:String(t.version??``),reason:String(t.reason??``)})}function T(e){let t=_.value;if(!t)return;l.value=null;let n=e.trim();n&&!/^https?:\/\//i.test(n)&&(n=n.startsWith(`/`)?`${window.location.origin}${n}`:`http://${n}`),p.value=n,m.value=n,t.src=n,s.value=!0}function E(){h.value=!1,m.value!==p.value&&T(m.value)}function ee(){h.value=!0,m.value=p.value,Ae(()=>{f.value?.select()})}function te(){h.value=!1,m.value=p.value}function D(e){e.key===`Escape`&&(h.value=!1,m.value=p.value,f.value?.blur())}function ne(){try{_.value?.contentWindow?.history.back()}catch{}}function ie(){let e=_.value;if(!e)return;l.value=null,s.value=!0;let t=p.value;e.src=``,e.src=t,p.value=t,m.value=t}function ae(){try{let e=new URL(re(p.value));(e.protocol===`http:`||e.protocol===`https:`)&&window.open(e.href,`_blank`,`noopener,noreferrer`)}catch{}}let se,le;return Me(()=>{let e=t.panes.has(g.value),n=t.context.panel.consumeBootRoute?.(t.entry.id)??t.entry.url;!e&&n!==p.value&&(p.value=n,m.value=n);let i=t.panes.ensure(g.value,{src:n,style:{boxShadow:`none`,outline:`none`}}),u=i.iframe;le=ot({iframe:u,initial:p.value,onChange:e=>{p.value=e}}),e||(s.value=!0);let f=t.context.panel.session;$e(()=>{t.context.docks.selectedId===t.entry.id&&(f.selectedDockRoute=p.value)}),se=()=>{s.value=!1},u.addEventListener(`load`,se);let h=t.context.docks.getStateById(t.entry.id);h&&(h.domElements.iframe=u),$e(()=>{Object.assign(u.style,t.iframeStyle),a.value&&!r.value?(u.style.borderTopLeftRadius=`0px`,u.style.borderTopRightRadius=`0px`):(u.style.borderTopLeftRadius=``,u.style.borderTopRightRadius=``),r.value&&(u.style.borderRadius=`0px`,u.style.border=`none`)}),$e(()=>{c.value&&(l.value||s.value?i.hide():i.show())}),window.addEventListener(`message`,w),i.mount(d.value),o.value=!1,c.value=!0,Ae(()=>{i.update()})}),xe(()=>{window.removeEventListener(`message`,w),le?.(),le=void 0;let e=t.panes.get(g.value);e&&se&&e.iframe?.removeEventListener(`load`,se),e&&e.target===d.value&&e.unmount()}),(e,t)=>(G(),j(`div`,Ru,[a.value?(G(),j(`div`,{key:0,class:`flex-none px-2 w-full flex items-center gap-1 color-base border-base border-b`,style:R({height:`${Uu}px`})},[b.value?(G(),j(`button`,{key:0,class:`w-7 h-7 flex items-center justify-center rounded hover:bg-gray/15 transition-colors shrink-0`,title:`Back`,onClick:ne},[...t[1]||=[P(`div`,{class:`i-ph-caret-left op60 w-4.5 h-4.5`},null,-1)]])):A(``,!0),y.value?(G(),j(`div`,zu,[...t[2]||=[P(`div`,{class:`i-ph-globe text-sm`},null,-1),P(`span`,null,`Cross-Origin`,-1)]])):A(``,!0),x.value?(G(),j(`button`,{key:2,class:`w-7 h-7 flex items-center justify-center rounded hover:bg-gray/15 transition-colors shrink-0`,title:`Reload`,onClick:ie},[...t[3]||=[P(`div`,{class:`i-ph-arrow-clockwise op60 w-4.5 h-4.5`},null,-1)]])):A(``,!0),P(`div`,Bu,[P(`input`,{ref:`urlInput`,value:h.value?m.value:C.value,type:`text`,class:`flex-1 bg-transparent outline-none text-sm font-mono`,placeholder:`Enter URL...`,readonly:y.value,onInput:t[0]||=e=>m.value=e.target.value,onFocus:ee,onBlur:te,onKeydown:[D,ye(E,[`enter`])]},null,40,Vu),s.value?(G(),j(`div`,Hu)):A(``,!0)]),S.value?(G(),j(`button`,{key:3,class:`w-7 h-7 flex items-center justify-center rounded hover:bg-gray/15 transition-colors shrink-0`,title:`Open externally`,onClick:ae},[...t[4]||=[P(`div`,{class:`i-ph-arrow-square-out-duotone op60 w-4.5 h-4.5`},null,-1)]])):A(``,!0)],4)):A(``,!0),P(`div`,{ref_key:`viewFrame`,ref:d,class:`devframes-view-iframe relative w-full h-full flex-1 items-center justify-center`},[u.value?(G(),F(Lu,{key:0})):A(``,!0),l.value?(G(),F(Fu,{key:1,error:l.value,onRetry:ie},null,8,[`error`])):A(``,!0)],512)]))}}),Gu={class:`flex flex-col gap-4 items-center justify-center h-full relative`},Ku={class:`text-2xl font-bold`},qu={key:0,class:`flex flex-col gap-1 max-w-full w-64 items-start`},Ju=[`disabled`,`title`],Yu=[`value`],Xu={class:`min-h-16 max-w-full flex flex-col gap-2 items-center`},Zu={key:0,class:`max-w-full text-sm text-red-600 dark:text-red-400 text-center text-balance`},Qu={key:1,class:`inline-flex items-center gap-2 text-sm color-base op70`},$u={key:0,class:`i-ph-dots-three-outline-duotone animate-pulse flex-none`},ed={class:`truncate`},td=J({__name:`ViewLauncher`,props:{context:{},entry:{}},setup(e){let t=e,n=N(()=>t.entry.launcher.roots),r=B(n.value?.[0]?.value);Xe(n,e=>{let t=e?.[0];if(!t){r.value=void 0;return}e.some(e=>e.value===r.value)||(r.value=t.value)},{immediate:!0});function i(){let e=t.entry.launcher.command;e&&t.context.commands.execute(e,n.value?.length?{root:r.value}:void 0)}let a=N(()=>t.entry.launcher.status||`idle`),o=N(()=>t.entry.launcher.digest),s=N(()=>t.entry.launcher.terminalSessionId);function c(){s.value&&t.context.rpc.call(Ee.rpc.docksActivate,{dockId:p,params:{sessionId:s.value}})}let l={error:`i-ph-arrow-clockwise-duotone`,idle:`i-ph-rocket-launch-duotone`,loading:`i-ph-spinner-gap-duotone animate-spin`,success:`i-ph:check`},u=N(()=>t.entry.launcher.error),d=N(()=>a.value===`idle`?t.entry.launcher.buttonStart||`Launch`:a.value===`loading`?t.entry.launcher.buttonLoading||`Loading...`:a.value===`error`?`Retry`:a.value===`success`?`Success`:`UNKNOWN STATUS: ${a.value}`),f=N(()=>a.value===`idle`||a.value===`error`);return(t,p)=>(G(),j(`div`,Gu,[I(Ui,{icon:e.entry.launcher.icon||e.entry.icon,class:`w-10 h-10`},null,8,[`icon`]),P(`h1`,Ku,q(e.entry.launcher.title),1),P(`p`,null,q(e.entry.launcher.description),1),n.value?.length?(G(),j(`label`,qu,[p[1]||=P(`span`,{class:`text-xs op60`},`Launch root`,-1),Ze(P(`select`,{"onUpdate:modelValue":p[0]||=e=>r.value=e,disabled:a.value===`loading`,title:n.value.find(e=>e.value===r.value)?.description,class:`w-full px3 py2 text-sm rounded-lg bg-base color-base border border-base outline-none transition-all focus-visible:ring-3 focus-visible:ring-primary-500/30 disabled:op50 disabled:pointer-events-none`},[(G(!0),j(H,null,L(n.value,e=>(G(),j(`option`,{key:e.value,value:e.value},q(e.label),9,Yu))),128))],8,Ju),[[it,r.value]])])):A(``,!0),I(ma,{variant:`primary`,class:V([`min-w-40 justify-center`,a.value===`error`?`bg-error! border-error!`:``]),icon:l[a.value],loading:a.value===`loading`,disabled:!f.value,onClick:i},{default:z(()=>[M(q(d.value),1)]),_:1},8,[`class`,`icon`,`loading`,`disabled`]),P(`div`,Xu,[a.value===`error`&&u.value?(G(),j(`p`,Zu,q(u.value),1)):o.value?(G(),j(`div`,Qu,[a.value===`loading`?(G(),j(`div`,$u)):A(``,!0),P(`span`,ed,q(o.value),1)])):A(``,!0),s.value?(G(),F(ma,{key:2,variant:`text`,size:`sm`,icon:`i-ph-arrow-square-out-duotone`,onClick:c},{default:z(()=>[...p[2]||=[M(` View in Terminal `,-1)]]),_:1})):A(``,!0)])]))}}),nd={key:2},rd=J({__name:`ViewEntry`,props:{context:{},entry:{},panes:{},iframeStyle:{},divStyle:{}},setup(e){return(t,n)=>(G(),F(ge,null,{fallback:z(()=>[...n[0]||=[P(`div`,null,` Loading... `,-1)]]),default:z(()=>[e.entry.type===`~builtin`?(G(),j(H,{key:0},[e.entry.id===`~settings`?(G(),F(zs,{key:0,context:e.context,entry:e.entry},null,8,[`context`,`entry`])):e.entry.id===`~client-auth-notice`?(G(),F(_o,{key:1,context:e.context},null,8,[`context`])):(G(),j(`div`,nd,` Unknown builtin entry: `+q(e.entry),1))],64)):e.entry.type===`action`?(G(),j(H,{key:1},[],64)):e.entry.type===`iframe`?(G(),F(Wu,{key:2,context:e.context,entry:e.entry,panes:e.panes,"iframe-style":e.iframeStyle},null,8,[`context`,`entry`,`panes`,`iframe-style`])):e.entry.type===`custom-render`?(G(),F(Bs,{key:3,context:e.context,entry:e.entry,panes:e.panes,"div-style":e.divStyle},null,8,[`context`,`entry`,`panes`,`div-style`])):e.entry.type===`launcher`?(G(),F(td,{key:4,context:e.context,entry:e.entry},null,8,[`context`,`entry`])):(G(),F(qs,{key:5,context:e.context,entry:e.entry},null,8,[`context`,`entry`]))]),_:1}))}}),id=J({__name:`ColorSchemeRoot`,setup(e){return(e,t)=>(G(),j(`div`,{class:V([`devframes-color-root`,U(Be)?`dark`:`light`]),style:R({display:`contents`,colorScheme:U(Be)?`dark`:`light`})},[Pe(e.$slots,`default`)],6))}});function ad(e){return je(`button`,{class:`flex items-center gap-2 px3 py1.5 rounded hover:bg-active transition text-left`,onClick:e.action},[je(`div`,{class:`${e.icon} text-base op60`}),je(`span`,e.label)])}function od(e,t){let n=e.docks.settings,r=t.id;n.mutate(e=>{e.docksHidden.includes(r)||(e.docksHidden=[...e.docksHidden,r])}),e.docks.selected?.id===r&&e.docks.switchEntry(null),le(null)}function sd(e,t){let n=e.docks.getStateById(t.id)?.domElements.iframe;if(!n){le(null);return}let r=n.src;n.src=``,n.src=r,le(null)}function cd(e,t){return t.id!==`~settings`&&e.docks.entries.some(e=>e.id===t.id)}function ld(e){return e.type===`iframe`}function ud(e){let{context:t,entry:n,el:r,gap:i=6}=e,a=t.panel.store.mode===`edge`,o=[{label:`Hide`,icon:`i-ph-eye-slash-duotone`,action:()=>od(t,n),visible:cd(t,n)},{label:`Refresh`,icon:`i-ph-arrow-clockwise-duotone`,action:()=>sd(t,n),visible:ld(n)},{label:a?`Float Mode`:`Edge Mode`,icon:a?`i-ph-arrows-out-duotone`:`i-ph-square-half-bottom-duotone`,action:()=>{if(a){let e=t.panel.store;switch(e.position){case`bottom`:e.left=50,e.top=100;break;case`top`:e.left=50,e.top=0;break;case`left`:e.left=0,e.top=50;break;case`right`:e.left=100,e.top=50}e.mode=`float`}else t.panel.store.mode=`edge`;le(null)},visible:t.clientType===`embedded`},{label:`Popup`,icon:`i-ph-arrow-square-out-duotone`,action:()=>{le(null),se(t)},visible:ue()&&!ie().value&&t.clientType===`embedded`}].filter(e=>e.visible);o.length!==0&&le({el:r,gap:i,content:()=>je(`div`,{class:`flex flex-col text-sm min-w-36 mx--1`},o.map(ad))})}var dd=[`aria-label`],fd=J({__name:`DockEntry`,props:{context:{},dock:{},isAction:{type:Boolean},isSelected:{type:Boolean},isDimmed:{type:Boolean},isVertical:{type:Boolean},badge:{},badgeVariant:{},tooltip:{type:Boolean,default:!0}},setup(e){let t=e,n={info:{bg:`#3b82f6`,fg:`#eff6ff`},success:{bg:`#22c55e`,fg:`#f0fdf4`},warning:{bg:`#f59e0b`,fg:`#fffbeb`},danger:{bg:`#ef4444`,fg:`#fef2f2`}},r=N(()=>{if(!t.badgeVariant||t.badgeVariant==="default")return;let{bg:e,fg:r}=n[t.badgeVariant];return{backgroundColor:e,color:r}}),i=K(`button`);function a(){t.tooltip&&i.value&&w({content:t.dock.title,el:i.value})}function o(){t.tooltip&&w(null)}function s(e){if(!i.value||t.dock.id===`overflow`)return;e.preventDefault(),o();let n=t.context.docks.entries.find(e=>e.id===t.dock.id);n&&ud({context:t.context,entry:n,el:i.value,gap:6})}return Fe(`pointerdown`,()=>{t.tooltip&&w(null)}),(t,n)=>(G(),j(`div`,{key:e.dock.id,class:`relative group devframes-dock-entry`,onPointerenter:a,onPointerleave:o,onContextmenu:s},[P(`button`,{ref_key:`button`,ref:i,"aria-label":e.dock.title,class:V([[e.isVertical?`rotate-270`:``,e.isDimmed?`op50 saturate-0`:``,e.isSelected?`scale-120 text-primary`:``,e.isAction?`bg-[#8881] hover:bg-[#8882] rounded-full`:`rounded-xl`],`flex items-center justify-center p1.5 hover:bg-[#8881] hover:scale-110 transition-all duration-300 relative outline-none`])},[I(Ui,{icon:e.dock.icon,class:`w-5 h-5 select-none`},null,8,[`icon`]),e.badge?(G(),j(`div`,{key:0,class:V([`absolute top-0.5 right-0 text-0.6em px-1 rounded-full shadow`,r.value?``:`bg-gray-500 text-white`]),style:R(r.value)},q(e.badge),7)):A(``,!0)],10,dd)],32))}});function pd(e){return e?{"--devframe-primary":e}:void 0}var md={class:`truncate`},hd={key:0,class:`border-t border-base mx--2 my1`},gd=[`onClick`],_d={class:`truncate flex-1`},vd={key:0,class:`bg-gray-6 text-white text-0.6em px-1 rounded-full shadow`},yd={key:0,class:`px2 py1.5 op50 text-sm italic`},bd=J({__name:`DockGroupPopover`,props:{context:{},group:{},members:{},selectedId:{}},emits:[`select`],setup(e,{emit:t}){let n=e,r=t,i=N(()=>n.members.every(([,e])=>e.length===0));return(t,n)=>(G(),j(`div`,{class:V([`flex flex-col gap-0.5 min-w-44 max-w-64`,e.group.accentColor?`devframes-accent-scope`:``]),style:R(U(pd)(e.group.accentColor)),onMousemove:n[0]||=Te(()=>{},[`stop`])},[P(`div`,{class:V([`flex items-center gap-1.5 px4 mx--2 pt2 pb2.5 font-bold text-2.75 uppercase tracking-wide border-b border-base`,e.group.accentColor?`color-active`:``])},[I(Ui,{icon:e.group.icon,class:`w-4.5 h-4.5`},null,8,[`icon`]),P(`span`,md,q(e.group.title),1)],2),(G(!0),j(H,null,L(e.members,([t,n],i)=>(G(),j(H,{key:t},[i>0&&n.length?(G(),j(`div`,hd)):A(``,!0),(G(!0),j(H,null,L(n,t=>(G(),j(`button`,{key:t.id,class:V([`flex items-center gap-2 w-full px2 py1.5 rounded text-sm text-left transition`,e.selectedId===t.id?`text-primary bg-active`:`op80 hover:op100 hover:bg-active`]),onClick:e=>r(`select`,t)},[I(Ui,{icon:t.icon,class:`w-4.5 h-4.5 flex-none`},null,8,[`icon`]),P(`span`,_d,q(t.title),1),t.badge?(G(),j(`div`,vd,q(t.badge),1)):A(``,!0)],10,gd))),128))],64))),128)),i.value?(G(),j(`div`,yd,` No tools yet `)):A(``,!0)],38))}}),xd=J({__name:`DockGroupButton`,props:{context:{},group:{},isVertical:{type:Boolean},selected:{},dimInactive:{type:Boolean,default:!0}},emits:[`select`],setup(e,{emit:t}){let r=e,i=t,a=oe(r.context),o=N(()=>n(r.context.docks.entries,r.group.id,a.value,{whenContext:r.context.when.context})),s=N(()=>v(r.context.docks.entries,r.group.id,a.value,{whenContext:r.context.when.context})),l=N(()=>{let e=r.selected?.id;return!!e&&o.value.some(t=>t.id===e)}),u=K(`groupButton`),d=B(!1),f=ne();function p(){u.value&&(d.value=!0,S({el:u.value,content:()=>je(bd,{context:r.context,group:r.group,members:s.value,selectedId:r.selected?.id??null,onSelect:e=>{i(`select`,e),m()}})}))}function m(){d.value=!1,S(null)}function h(){d.value?m():p()}Ie(()=>f.value,e=>{d.value=e?.el===u.value},{debounce:1e3});function g(){if(l.value){m(),i(`select`,void 0);return}let e=c(r.context.docks.entries,r.group,r.context.panel.session.groupLastChildIds?.[r.group.id],r.context.when.context);if(e){m(),i(`select`,e);return}h()}return(t,n)=>(G(),j(`div`,{ref_key:`groupButton`,ref:u,class:V(e.group.accentColor?`devframes-accent-scope`:``),style:R(U(pd)(e.group.accentColor))},[I(fd,{context:e.context,dock:e.group,"is-vertical":e.isVertical,"is-selected":l.value||d.value,"is-dimmed":e.dimInactive&&e.selected?!l.value:!1,badge:e.group.badge,"badge-variant":e.group.badgeVariant,onClick:g},null,8,[`context`,`dock`,`is-vertical`,`is-selected`,`is-dimmed`,`badge`,`badge-variant`])],6))}}),Sd=J({__name:`DockEntries`,props:{context:{},entries:{},selected:{},isVertical:{type:Boolean},dimInactive:{type:Boolean,default:!0}},emits:[`select`],setup(t,{emit:r}){let i=t,a=r,{selected:s,isVertical:c,entries:l}=W(i),u=oe(i.context);function d(t){return!(t.type===`group`&&n(i.context.docks.entries,t.id,u.value,{whenContext:i.context.when.context}).length===0&&!o(i.context.docks.entries,t.id,t.defaultChildId,i.context.when.context)||t.when&&!e(t.when,i.context.when.context)||t.visibility&&!e(t.visibility,i.context.when.context))}function f(e){s.value?.id===e.id?a(`select`,void 0):a(`select`,e)}return(e,n)=>(G(!0),j(H,null,L(U(l),e=>(G(),j(H,{key:e.id},[d(e)?(G(),j(H,{key:0},[e.type===`group`?(G(),F(xd,{key:0,context:t.context,group:e,"is-vertical":U(c),selected:U(s),"dim-inactive":t.dimInactive,onSelect:n[0]||=e=>a(`select`,e)},null,8,[`context`,`group`,`is-vertical`,`selected`,`dim-inactive`])):(G(),F(fd,{key:1,context:t.context,dock:e,"is-action":e.type===`action`,"is-selected":U(s)?.id===e.id,"is-dimmed":t.dimInactive&&U(s)?U(s).id!==e.id:!1,"is-vertical":U(c),badge:e.badge,"badge-variant":e.badgeVariant,onClick:t=>f(e)},null,8,[`context`,`dock`,`is-action`,`is-selected`,`is-dimmed`,`is-vertical`,`badge`,`badge-variant`,`onClick`]))],64)):A(``,!0)],64))),128))}}),Cd=J({__name:`DockEntriesWithCategories`,props:{context:{},groups:{},selected:{},isVertical:{type:Boolean},rotate:{type:Boolean,default:!0},dimInactive:{type:Boolean,default:!0}},emits:[`select`],setup(e,{emit:t}){let n=t;return(t,r)=>(G(!0),j(H,null,L(e.groups,([i,a],o)=>(G(),j(H,{key:i},[o>0?Pe(t.$slots,`separator`,{category:i,index:o,isVertical:e.isVertical},()=>[P(`div`,{class:V([`border-base m1`,e.isVertical&&!e.rotate?`w-20px h-px border-b-1.5`:`h-20px w-px border-r-1.5`])},null,2)],void 0,0):A(``,!0),I(Sd,{context:e.context,entries:a,"is-vertical":e.isVertical&&e.rotate,selected:e.selected,"dim-inactive":e.dimInactive,onSelect:r[0]||=e=>n(`select`,e)},null,8,[`context`,`entries`,`is-vertical`,`selected`,`dim-inactive`])],64))),128))}}),wd={key:0,class:`w-8 h-px border-t border-base my0.5`},Td=[`aria-label`,`onPointerenter`,`onClick`],Ed={key:0,class:`absolute top-0.5 right-0.5 bg-gray-6 text-white text-0.6em px-0.5 rounded-full shadow leading-none`},Dd={class:`absolute top-0.5 right-0.5 bg-gray-6 text-white text-0.6em px-0.5 rounded-full shadow leading-none`},Od=34,kd=34,Ad=7,jd=53,Md=J({__name:`DockGroupSidebar`,props:{context:{},group:{},selectedId:{}},setup(e){let t=e,n=oe(t.context),r=N(()=>v(t.context.docks.entries,t.group.id,n.value,{whenContext:t.context.when.context})),i=K(`sidebar`),o=B(0);Xe(i,(e,t,n)=>{if(!e){o.value=0;return}let r=e.ownerDocument?.defaultView?.ResizeObserver??globalThis.ResizeObserver;if(!r)return;let i=new r(([e])=>{e&&(o.value=e.borderBoxSize?.[0]?.blockSize??e.contentRect.height)});i.observe(e),n(()=>i.disconnect())},{immediate:!0,flush:`post`});let s=N(()=>r.value.reduce((e,[,t])=>e+t.length,0)),c=N(()=>a({availableHeight:o.value,reservedHeight:jd,itemHeight:Od,dividerHeight:Ad,moreButtonHeight:kd,dividerCount:Math.max(0,r.value.filter(([,e])=>e.length).length-1),totalItems:s.value})),u=N(()=>l(r.value,c.value)),d=N(()=>u.value.visible),f=N(()=>u.value.overflow),p=N(()=>f.value.reduce((e,[,t])=>e+t.length,0)),m=N(()=>p.value>0),h=N(()=>p.value>9?`9+`:p.value.toString()),g=N(()=>!!t.selectedId&&f.value.some(([,e])=>e.some(e=>e.id===t.selectedId)));function _(e){t.context.docks.switchEntry(e)}function y(e,t){w({content:t,el:e.currentTarget})}function b(){w(null)}let x=K(`moreButton`),S=B(!1),C=T();function E(){x.value&&(S.value=!0,O({el:x.value,placement:`right`,content:()=>je(bd,{context:t.context,group:t.group,members:f.value,selectedId:t.selectedId,onSelect:e=>{_(e.id),ee()}})}))}function ee(){S.value=!1,O(null)}function te(){S.value?ee():E()}Ie(()=>C.value,e=>{S.value=e?.el===x.value},{debounce:1e3});let D=N(()=>g.value||S.value);return(t,n)=>(G(),j(`div`,{ref_key:`sidebar`,ref:i,class:V([`devframes-group-sidebar flex flex-col flex-none w-12 h-full border-r border-base of-y-hidden of-x-hidden select-none py1.5 items-center gap-0.5`,e.group.accentColor?`devframes-accent-scope`:``]),style:R(U(pd)(e.group.accentColor))},[P(`div`,{class:V([`flex items-center justify-center w-8 h-8 op60`,e.group.accentColor?`color-active`:``]),onPointerenter:n[0]||=t=>y(t,e.group.title),onPointerleave:b},[I(Ui,{icon:e.group.icon,class:`w-5 h-5 flex-none`},null,8,[`icon`])],34),n[2]||=P(`div`,{class:`w-8 h-px border-t border-base my0.5`},null,-1),(G(!0),j(H,null,L(d.value,([t,n],r)=>(G(),j(H,{key:t},[r>0&&n.length?(G(),j(`div`,wd)):A(``,!0),(G(!0),j(H,null,L(n,t=>(G(),j(`button`,{key:t.id,"aria-label":t.title,class:V([`relative flex items-center justify-center w-8 h-8 rounded-lg transition`,e.selectedId===t.id?`text-primary bg-active`:`op60 hover:op100 hover:bg-active`]),onPointerenter:e=>y(e,t.title),onPointerleave:b,onPointerdown:b,onClick:e=>_(t.id)},[I(Ui,{icon:t.icon,class:`w-5 h-5 flex-none`},null,8,[`icon`]),t.badge?(G(),j(`div`,Ed,q(t.badge),1)):A(``,!0)],42,Td))),128))],64))),128)),m.value?(G(),j(`button`,{key:0,ref_key:`moreButton`,ref:x,"aria-label":`Show more`,class:V([`relative flex items-center justify-center w-8 h-8 rounded-lg transition mt-auto flex-none`,D.value?`text-primary bg-active`:`op60 hover:op100 hover:bg-active`]),onPointerenter:n[1]||=e=>y(e,`Show more`),onPointerleave:b,onPointerdown:b,onClick:te},[I(Ui,{icon:`ph:dots-three-circle-duotone`,class:`w-5 h-5 flex-none`}),P(`div`,Dd,q(h.value),1)],34)):A(``,!0)],6))}}),Nd={key:0,class:`h-screen w-screen of-hidden`},Pd={key:1,class:`h-screen w-screen of-hidden grid cols-[max-content_1fr]`},Fd={class:`border-r border-base flex flex-col min-h-0`},Id={class:`p2 border-b border-base flex`},Ld={class:`transition duration-200 p2 of-y-auto`},Rd={class:`min-h-0 flex`},zd={class:`relative flex-1 min-w-0 min-h-0`},Bd=ve(J({__name:`DockStandalone`,props:{context:{}},setup(e){let t=e.context,n=K(`viewsContainer`),r=ft(n,t.panel,()=>dt(t.docks.selected)),i=pt(t,e=>{e||t.docks.switchEntry(null)});Xe(()=>t.docks.entries,()=>{t.docks.selectedId||=t.docks.entries[0]?.id??null},{immediate:!0});let a=N(()=>t.docks.groupedEntries),o=N(()=>me(t.docks.entries,t.docks.selected));function s(e){e&&t.docks.switchEntry(e)}return(e,c)=>(G(),F(id,null,{default:z(()=>[U(i)?(G(),j(`div`,Pd,[P(`div`,Fd,[P(`div`,Id,[I(ur,{class:`w-7 h-7 ma`})]),P(`div`,Ld,[I(Cd,{context:U(t),groups:a.value,"is-vertical":!1,selected:U(t).docks.selected,"dim-inactive":!1,onSelect:c[0]||=e=>s(e?.id)},{separator:z(()=>[...c[1]||=[P(`div`,{class:`border-base border-b w-full my-2`},null,-1)]]),_:1},8,[`context`,`groups`,`selected`])])]),P(`div`,Rd,[o.value?(G(),F(Md,{key:0,context:U(t),group:o.value,"selected-id":U(t).docks.selected?.id??null},null,8,[`context`,`group`,`selected-id`])):A(``,!0),P(`div`,zd,[U(t).docks.selected&&U(r)?(G(),F(rd,{key:U(t).docks.selected.id,entry:U(t).docks.selected,context:U(t),panes:U(r)},null,8,[`entry`,`context`,`panes`])):A(``,!0),P(`div`,{id:`devframes-views-container`,ref_key:`viewsContainer`,ref:n,class:`pointer-events-auto`},null,512)])])])):(G(),j(`div`,Nd,[I(_o,{context:U(t)},null,8,[`context`])])),I(ao),I(fa,{context:U(t)},null,8,[`context`]),I(qa,{context:U(t)},null,8,[`context`]),I(Sa)]),_:1}))}}),{shadowRoot:!0,styles:[ct]});customElements.get(`devframes-dock-standalone`)||customElements.define(`devframes-dock-standalone`,Bd);export{Bd as DockStandalone}; +\ No newline at end of file ++`+u),Ee&&(u=ht(u)),w&&L?ne(u):u},t.setConfig=function(){let e=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{};K(e),I=!0,Oe=k,ke=A},t.clearConfig=function(){G=null,I=!1,Oe=null,ke=null,w=E,T=``},t.isValidAttribute=function(e,t,n){G||K({});let r=W(e),i=W(t);return Et(r,i,n)},t.addHook=function(e,t){typeof t==`function`&&Wr(O,e)&&jr(O[e],t)},t.removeHook=function(e,t){if(Wr(O,e)){if(t!==void 0){let n=kr(O[e],t);return n===-1?void 0:Mr(O[e],n,1)[0]}return Ar(O[e])}},t.removeHooks=function(e){Wr(O,e)&&(O[e]=[])},t.removeAllHooks=function(){O=ji()},t}var Fi=Pi(),Ii=new Map,Li=Fi();async function Ri(e,t){let n=`${e}:${t}`;if(Ii.has(n))return Ii.get(n);let r=i().then(e=>(Ii.set(n,e),e)).catch(e=>{throw Ii.delete(n),e});return Ii.set(n,r),r;async function i(){let n=`https://api.iconify.design/${e}/${t}.svg?color=currentColor&width=100%`,r=await fetch(n,{signal:AbortSignal.timeout(1e4)}).then(e=>e.text());return Li.sanitize(r)}}var zi=[`innerHTML`],Bi=[`src`],Vi=J({__name:`IconifyIcon`,props:{icon:{}},setup(e){let t=e,n=N(()=>t.icon.includes(`/`)||t.icon.startsWith(`data:`)||t.icon.startsWith(`builtin:`)),r=N(()=>{if(n.value)return;let e=t.icon.match(/^(?:i-)?([\w-]+):([\w-]+)$/);if(e)return{collection:e[1],icon:e[2]}}),i=B(void 0);return $e(async()=>{if(!r.value){i.value=void 0;return}try{i.value=await Ri(r.value.collection,r.value.icon)}catch{i.value=void 0}}),(t,n)=>r.value?(G(),j(`div`,{key:0,innerHTML:i.value},null,8,zi)):(G(),j(`img`,{key:1,src:e.icon,class:`w-full h-full m-auto`,draggable:`false`},null,8,Bi))}}),Hi={key:1,class:`flex items-center justify-center`},Ui=J({__name:`DockIcon`,props:{icon:{},title:{}},setup(e){let t=e,n=N(()=>typeof t.icon==`string`?{dark:t.icon,light:t.icon}:t.icon);return(t,r)=>n.value.light===`builtin:devframes`?(G(),F(ur,{key:0})):(G(),j(`div`,Hi,[n.value.light===n.value.dark?(G(),F(Vi,{key:0,icon:n.value.light,title:e.title},null,8,[`icon`,`title`])):(G(),j(H,{key:1},[I(Vi,{class:`dark-hidden`,icon:n.value.light,title:e.title},null,8,[`icon`,`title`]),I(Vi,{class:`light-hidden`,icon:n.value.dark,title:e.title},null,8,[`icon`,`title`])],64))]))}}),Wi=[`id`],Gi={class:`flex items-center gap-2 flex-1 of-hidden min-w-0`},Ki={class:`flex-1 min-w-0`},qi={class:`flex items-center gap-1.5`},Ji={class:`truncate`},Yi={key:0,class:`op50`},Xi={key:0,class:`text-[10px] px-1 py-0 rounded bg-blue/10 text-blue shrink-0 leading-4`},Zi={key:0,class:`truncate text-xs op40 mt-0.5`},Qi={class:`flex items-center gap-1.5 flex-none`},$i={key:0,class:`i-ph-spinner-gap-duotone w-3.5 h-3.5 animate-spin op50`},ea={key:1,class:`i-ph-caret-right w-3 h-3 op40`},ta=J({__name:`CommandPaletteItem`,props:{entry:{},parentTitle:{},showParentTitle:{type:Boolean},selected:{type:Boolean},loading:{type:Boolean},keybindings:{}},emits:[`select`,`activate`],setup(e){return(t,n)=>(G(),j(`button`,{id:`cmd-${e.entry.id}`,class:`w-full text-left`,onClick:n[0]||=e=>t.$emit(`activate`),onMouseover:n[1]||=e=>t.$emit(`select`)},[P(`div`,{class:V([`flex items-center gap-2 justify-between rounded-md px-2.5 py-1.5 text-sm transition-colors`,e.selected?`bg-primary/10 text-primary`:`op80 hover:op100`])},[P(`div`,Gi,[e.entry.icon?(G(),F(Ui,{key:0,icon:e.entry.icon,class:`w-4 h-4 flex-none op70`},null,8,[`icon`])):A(``,!0),P(`div`,Ki,[P(`div`,qi,[P(`span`,Ji,[e.parentTitle&&e.showParentTitle?(G(),j(`span`,Yi,q(e.parentTitle)+` › `,1)):A(``,!0),M(` `+q(e.entry.title),1)]),e.entry.source===`server`?(G(),j(`span`,Xi,`server`)):A(``,!0)]),e.selected&&e.entry.description?(G(),j(`div`,Zi,q(e.entry.description),1)):A(``,!0)])]),P(`div`,Qi,[(G(!0),j(H,null,L(e.keybindings,(e,t)=>(G(),F(sr,{key:t,keys:e.key},null,8,[`keys`]))),128)),e.loading?(G(),j(`span`,$i)):e.entry.children?.length?(G(),j(`span`,ea)):A(``,!0)])],2)],40,Wi))}}),na={class:`absolute inset-0 flex items-start justify-center relative pt-[20vh] pointer-events-none`},ra={class:`w-full w-lg bg-glass:80 color-base border border-base rounded-lg shadow-xl pointer-events-auto of-hidden flex flex-col max-h-[60vh]`},ia={class:`border-b border-base flex items-center px-3`},aa=[`onClick`],oa={key:0,class:`py-8 flex flex-col items-center justify-center gap-2 op50 text-sm`},sa={key:0},ca={class:`text-primary op100`},la={key:1},ua={class:`border-t border-base flex items-center justify-between gap-4 px-3 py-1.5 text-[10px] op50`},da={class:`flex items-center gap-1.5`},fa=J({__name:`CommandPalette`,props:{context:{}},setup(e){let t=e,n=N(()=>t.context.commands),i=N({get:()=>n.value.paletteOpen,set:e=>{n.value.paletteOpen=e}}),a=B(``),o=B(0),s=K(`searchInput`),c=K(`listContainer`),l=B(!1),u=B([]),d=N(()=>x(n.value.paletteCommands)),f=N(()=>u.value.length>0?u.value.at(-1).items.map(e=>({entry:e,searchTitle:e.title})):d.value),p=B(),m=N(()=>p.value?p.value.map(e=>({entry:e,searchTitle:e.title})):f.value),_=N(()=>new zn(m.value,{keys:[`searchTitle`,`entry.description`,`entry.id`],distance:50,threshold:.4})),v=N(()=>a.value?_.value.search(a.value).map(e=>e.item):m.value);Xe(a,()=>{o.value=0});function y(e){a.value=``,o.value=0,p.value=void 0;let t=h(n.value.paletteCommands,e);u.value=t,e!=null&&!t.some(t=>t.id===e)&&(n.value.paletteScopeId=null)}Xe(i,e=>{e?(y(n.value.paletteScopeId),requestAnimationFrame(()=>{l.value=!0}),Ae(()=>s.value?.focus())):(l.value=!1,n.value.paletteScopeId=null)}),Xe(()=>n.value.paletteScopeId,e=>{i.value&&y(e)}),Xe(()=>n.value.paletteCommands,e=>{if(!i.value||u.value.length===0)return;let t=n.value.paletteScopeId,r=t!=null&&u.value.some(e=>e.id===t),a=fe(e,u.value,t);u.value=a,o.value=Math.min(o.value,Math.max(v.value.length-1,0)),r&&t!=null&&!a.some(e=>e.id===t)&&(n.value.paletteScopeId=null)});function b(e){let t=v.value.length;t!==0&&(o.value=(o.value+e+t)%t,S())}function S(){let e=v.value[o.value];e&&((c.value?.getRootNode())?.getElementById(`cmd-${e.entry.id}`))?.scrollIntoView({block:`nearest`})}let C=B(null);async function w(e){let r=m.value.find(t=>t.entry.id===e.entry.id)?.entry;if(r){if(g(r,t.context.docks.entries)===`drill`){n.value.paletteScopeId=r.id;return}if(r.source===`client`&&r.action){try{let e=await r.action();if(Array.isArray(e)){p.value=e,a.value=``,o.value=0;return}}catch(e){console.error(`[@devframes/hub-ui] Command "${r.id}" failed:`,e)}if(pe(r,t.context.docks.entries,n.value.paletteOpen,n.value.paletteScopeId))return;T();return}if(r.source===`server`){C.value=r.id;try{await n.value.execute(r.id)}catch(e){console.error(`[@devframes/hub-ui] Command "${r.id}" failed:`,e)}finally{C.value=null}T();return}T()}}function T(){l.value=!1,setTimeout(()=>{i.value=!1,a.value=``,u.value=[],p.value=void 0},150)}function E(){if(p.value){p.value=void 0,a.value=``,o.value=0;return}if(u.value.length>0){u.value.pop(),n.value.paletteScopeId=r(u.value),a.value=``,o.value=0;return}T()}function ee(e){u.value.splice(e),n.value.paletteScopeId=r(u.value),a.value=``,o.value=0}function te(e){e.key===`Backspace`&&!a.value&&(u.value.length>0||p.value)&&(e.preventDefault(),E())}function D(e){if(i.value)switch(e.key){case`ArrowDown`:case`ArrowUp`:e.preventDefault(),b(e.key===`ArrowDown`?1:-1);break;case`Enter`:{let t=v.value[o.value];t&&(e.preventDefault(),w(t));break}case`Escape`:e.preventDefault(),u.value.length>0||p.value?E():T()}}function ne(e){return n.value.getKeybindings(e)}return(e,t)=>i.value?(G(),j(`div`,{key:0,class:`devframes-command-palette fixed inset-0 z-command-palette`,onKeydown:D},[P(`div`,{class:V([`absolute inset-0 bg-white/50 dark:bg-black/30 transition-opacity duration-150`,l.value?`opacity-100 backdrop-blur-1`:`opacity-0 backdrop-blur-0`]),onClick:T},null,2),P(`div`,na,[P(`div`,{class:V([`flex flex-col transition-all duration-150`,l.value?`opacity-100 scale-100 translate-y-0`:`opacity-0 scale-98 -translate-y-2`])},[I(qn,{class:`absolute top--32px left-5px pointer-events-none`}),P(`div`,ra,[P(`header`,ia,[u.value.length>0?(G(!0),j(H,{key:0},L(u.value,(e,n)=>(G(),j(`button`,{key:n,class:`text-xs op60 hover:op80 mr-1 flex items-center gap-0.5`,onClick:e=>ee(n)},[M(q(e.title)+` `,1),t[1]||=P(`span`,{class:`op40`},`›`,-1)],8,aa))),128)):A(``,!0),Ze(P(`input`,{ref_key:`searchInput`,ref:s,"onUpdate:modelValue":t[0]||=e=>a.value=e,class:`flex-1 bg-transparent py-3 outline-none text-sm color-base`,placeholder:`Type a command...`,onKeydown:te},null,544),[[ke,a.value]])]),P(`div`,{ref_key:`listContainer`,ref:c,class:`flex-1 of-y-auto p-1.5`},[(G(!0),j(H,null,L(v.value,(e,t)=>(G(),F(ta,{key:e.entry.id,entry:e.entry,"parent-title":e.parentTitle,"show-parent-title":!u.value.length,selected:o.value===t,loading:C.value===e.entry.id,keybindings:ne(e.entry.id),onSelect:e=>o.value=t,onActivate:t=>w(e)},null,8,[`entry`,`parent-title`,`show-parent-title`,`selected`,`loading`,`keybindings`,`onSelect`,`onActivate`]))),128)),v.value.length?A(``,!0):(G(),j(`div`,oa,[t[4]||=P(`div`,{class:`i-ph-magnifying-glass-duotone w-6 h-6`},null,-1),a.value?(G(),j(`div`,sa,[t[2]||=M(` No results for "`,-1),P(`strong`,ca,q(a.value),1),t[3]||=M(`" `,-1)])):(G(),j(`div`,la,` No commands available `))]))],512),P(`footer`,ua,[t[6]||=P(`div`,{class:`flex items-center gap-1.5`},[P(`kbd`,{class:`px-1 py-0.5 rounded border border-base bg-base font-mono`},`↓↑`),P(`span`,null,`navigate`)],-1),P(`div`,da,[t[5]||=P(`kbd`,{class:`px-1 py-0.5 rounded border border-base bg-base font-mono`},`esc`,-1),P(`span`,null,q(u.value.length>0||p.value?`back`:`close`),1)]),t[7]||=P(`div`,{class:`flex items-center gap-1.5`},[P(`kbd`,{class:`px-1 py-0.5 rounded border border-base bg-base font-mono`},`↵`),P(`span`,null,`select`)],-1)])])],2)])],32)):A(``,!0)}}),pa=J({__name:`FeedbackSpinner`,props:{size:{default:`1em`}},setup(e){let t=e,n=N(()=>typeof t.size==`number`?`${t.size}px`:t.size);return(e,t)=>(G(),j(`span`,{class:`border-2 border-current border-t-transparent rounded-full inline-block animate-spin`,style:R({width:n.value,height:n.value}),role:`status`,"aria-label":`Loading`},null,4))}}),ma=J({__name:`ActionButton`,props:{as:{},to:{},href:{},icon:{},variant:{default:`action`},size:{default:`md`},loading:{type:Boolean},disabled:{type:Boolean}},setup(e){let t=e,n=N(()=>t.as?t.as:t.href!=null||t.to!=null?`a`:`button`),r=N(()=>n.value===`a`),i=N(()=>n.value===`button`),a=N(()=>t.size===`sm`),o=N(()=>t.variant===`primary`?a.value?`btn-primary text-sm`:`btn-primary`:t.variant===`text`?a.value?`btn-text text-sm`:`btn-text`:a.value?`btn-action-sm`:`btn-action`),s=N(()=>t.disabled||t.loading);return(t,a)=>(G(),F(he(n.value),{class:V([o.value,{"pointer-events-none op-mute":s.value&&!i.value}]),href:r.value?e.href??e.to:void 0,to:e.as&&e.to!=null?e.to:void 0,disabled:i.value?s.value:void 0,"aria-disabled":s.value||void 0,"aria-busy":e.loading||void 0},{default:z(()=>[e.loading?(G(),F(pa,{key:0,size:`1em`})):e.icon?(G(),j(`span`,{key:1,class:V(e.icon),"aria-hidden":`true`},null,2)):A(``,!0),Pe(t.$slots,`default`)]),_:3},8,[`class`,`href`,`to`,`disabled`,`aria-disabled`,`aria-busy`]))}}),ha=Ye({singleton:!0});function ga(){return ha.start}var _a=[`aria-labelledby`,`onKeydown`],va=[`onClick`],ya={class:`relative w-full max-w-96 bg-base border border-base rounded-lg shadow-xl p-5`},ba={key:0,id:`devframes-confirm-title`,class:`text-sm font-medium leading-5`},xa={class:`flex items-center justify-end gap-2 mt-6`},Sa=J({__name:`Confirm`,setup(e){let t=K(`confirmButton`);Ge(()=>{(t.value?.$el)?.focus({preventScroll:!0})});function n(e,t){e(t)}return(e,r)=>(G(),F(U(ha),null,{default:z(({resolve:e,args:[r]})=>[P(`div`,{class:`devframes-confirm fixed inset-0 z-dock-confirm flex items-center justify-center p-4 color-base`,role:`dialog`,"aria-modal":`true`,"aria-labelledby":r.title?`devframes-confirm-title`:void 0,"aria-describedby":`devframes-confirm-message`,onKeydown:ye(Te(t=>n(e,!1),[`prevent`,`stop`]),[`esc`])},[P(`div`,{class:`absolute inset-0 bg-black/30 dark:bg-black/45 backdrop-blur-1 cursor-default`,"aria-hidden":`true`,onClick:t=>n(e,!1)},null,8,va),P(`div`,ya,[r.title?(G(),j(`h3`,ba,q(r.title),1)):A(``,!0),P(`p`,{id:`devframes-confirm-message`,class:V([`text-xs op60 leading-5`,r.title?`mt-1.5`:``])},q(r.message),3),P(`div`,xa,[I(ma,{variant:`text`,size:`sm`,onClick:t=>n(e,!1)},{default:z(()=>[M(q(r.cancelText??`Cancel`),1)]),_:2},1032,[`onClick`]),I(ma,{ref_key:`confirmButton`,ref:t,variant:`primary`,size:`sm`,onClick:t=>n(e,!0)},{default:z(()=>[M(q(r.confirmText??`OK`),1)]),_:2},1032,[`onClick`])])])],40,_a)]),_:1}))}}),Ca=nt([]),wa=new Map;function Ta(){return Ca}function Ea(e){let t=Ca.find(t=>t.id===e.id);if(t){t.entry=e;let n=wa.get(e.id);n&&(clearTimeout(n),wa.delete(e.id));let r=e.autoDismiss??5e3;r!==!1&&wa.set(e.id,setTimeout(Da,r,e.id));return}let n={id:e.id,entry:e};Ca.push(n);let r=e.autoDismiss??5e3;r!==!1&&wa.set(e.id,setTimeout(Da,r,e.id))}function Da(e){let t=Ca.findIndex(t=>t.id===e);t!==-1&&Ca.splice(t,1);let n=wa.get(e);n&&(clearTimeout(n),wa.delete(e))}function Oa(e,t,n){return e.notify?t?JSON.stringify(e)!==JSON.stringify(t):!n||e.status===`loading`:!1}var ka;function Aa(e){if(ka)return ka;let t=ka=nt({entries:[],unreadCount:0,pendingSelectId:null}),n=new Map,r=!0,i=null;async function a(){let a=await e.rpc.call(`devframes:plugin:messages:list`,i),o=0;let removedIds=a.removedIds;if(a.full){const retainedIds=new Set(a.entries.map(entry=>entry.id));removedIds=[...n.keys()].filter(id=>!retainedIds.has(id))}for(const id of removedIds){n.delete(id);Da(id)}for(let e of a.entries){let t=n.get(e.id);t||o++,Oa(e,t,r)&&Ea(e),n.set(e.id,e)}t.entries=Array.from(n.values()),t.unreadCount+=o,i=a.version,r=!1}let o=()=>a().catch(()=>{});return e.rpc.client.register({name:`devframe:messages:updated`,type:`action`,handler:()=>{e.rpc.isTrusted&&o()}}),e.rpc.ensureTrusted().then(()=>o()),t}function ja(e){ka&&(ka.pendingSelectId=e)}var Ma={info:{icon:`i-ph:info-duotone`,color:`text-blue`,bg:`bg-blue`,label:`Info`},warn:{icon:`i-ph:warning-duotone`,color:`text-amber`,bg:`bg-amber`,label:`Warning`},error:{icon:`i-ph:x-circle-duotone`,color:`text-red`,bg:`bg-red`,label:`Error`},success:{icon:`i-ph:check-circle-duotone`,color:`text-green`,bg:`bg-green`,label:`Success`},debug:{icon:`i-ph:bug-duotone`,color:`text-gray`,bg:`bg-gray`,label:`Debug`}};function Na(e,t=1){let n=0;for(let t=0;t({color:Na(t.label),backgroundColor:Na(t.label,.1)}));return(t,r)=>(G(),j(`span`,{class:`text-xs px-1 rounded`,style:R(n.value)},q(e.label),5))}}),Fa={class:`flex items-start gap-2 relative`},Ia={key:0,class:`flex-none mt-0.5 border-2 border-current border-t-transparent rounded-full animate-spin op50 w-4 h-4`},La={class:`flex-1 min-w-0 space-y-0.5`},Ra={class:`flex items-center gap-2`},za=[`title`],Ba={key:0,class:`text-xs op80 whitespace-pre-wrap`},Va={key:1,class:`flex flex-wrap items-center gap-x-2 gap-y-0.5`},Ha=J({__name:`MessageItem`,props:{entry:{},compact:{type:Boolean}},setup(e){let t=e,n=rt(()=>t.entry.timestamp);return(t,r)=>(G(),j(`div`,Fa,[P(`div`,{class:V([`w-2px flex-none absolute left-0 top-4px bottom-4px rounded-r`,[U(Ma)[e.entry.level]?.bg||`bg-gray`]])},null,2),e.entry.status===`loading`?(G(),j(`div`,Ia)):(G(),j(`div`,{key:1,class:V([`flex-none mt-0.5 w-4 h-4`,[U(Ma)[e.entry.level]?.icon,U(Ma)[e.entry.level]?.color]])},null,2)),P(`div`,La,[P(`div`,Ra,[P(`div`,{class:V([`flex-1 min-w-0 truncate text-sm font-medium`,[e.entry.status===`loading`?`op60`:``]])},q(e.entry.message),3),e.compact?A(``,!0):(G(),j(`span`,{key:0,class:`text-xs op40 flex-none`,title:new Date(e.entry.timestamp).toLocaleString()},q(U(n)),9,za)),Pe(t.$slots,`actions`)]),e.entry.description?(G(),j(`div`,Ba,q(e.entry.description),1)):A(``,!0),e.compact?A(``,!0):(G(),j(`div`,Va,[e.entry.category?(G(),F(Pa,{key:0,label:e.entry.category},null,8,[`label`])):A(``,!0),(G(!0),j(H,null,L(e.entry.labels,e=>(G(),F(Pa,{key:e,label:e},null,8,[`label`]))),128))]))])]))}}),Ua={key:0,class:`fixed bottom-4 right-4 z-dock-toast flex flex-col gap-2 pointer-events-auto w-72`},Wa=[`onClick`],Ga=[`onClick`],Ka=[`onClick`],qa=J({__name:`ToastOverlay`,props:{context:{}},setup(e){let t=e;t.context&&Aa(t.context);let n=Ta();function r(e){Da(e),ja(e),t.context?.docks.switchEntry(y)}function i(e){e.kind===`activate`?t.context?.docks.switchEntry(e.activate.dockId):e.kind===`command`&&t.context?.commands.execute(e.command.id,...e.command.params??[])}return(e,t)=>U(n).length>0?(G(),j(`div`,Ua,[I(at,{"enter-active-class":`transition-all duration-300 ease-out`,"leave-active-class":`transition-all duration-200 ease-in`,"enter-from-class":`opacity-0 translate-x-4`,"leave-to-class":`opacity-0 translate-x-4`},{default:z(()=>[(G(!0),j(H,null,L(U(n),e=>(G(),j(`div`,{key:e.id,class:`bg-toast-glass border color-base border-base shadow-xl cursor-pointer transition-colors rounded`,onClick:t=>r(e.id)},[I(Ha,{entry:e.entry,compact:``,class:`px-3 py-2.5`},{actions:z(()=>[(G(!0),j(H,null,L(e.entry.actions,e=>(G(),j(`button`,{key:e.id,class:`flex-none text-xs px-1.5 py-0.5 rounded border border-base op70 hover:op100 hover:bg-active transition`,onClick:Te(t=>i(e),[`stop`])},q(e.label),9,Ga))),128)),P(`button`,{class:`flex-none op30 hover:op100 p-0.5 rounded hover:bg-active transition`,onClick:Te(t=>U(Da)(e.id),[`stop`])},[...t[0]||=[P(`div`,{class:`i-ph-x w-3 h-3`},null,-1)]],8,Ka)]),_:2},1032,[`entry`])],8,Wa))),128))]),_:1})])):A(``,!0)}}),Ja=100,Ya=10,Xa=8;function Za(e,t,n){return Math.min(Math.max(e,t),Math.max(n,t))}function Qa({rect:e,anchorRight:t,anchorBottom:n,vw:r,vh:i}){return e.leftr-Ja?`left`:e.topi-Ja?`top`:`bottom`}function $a(e,t){let{rect:n,anchorRight:r,anchorBottom:i,vw:a,vh:o,gap:s,panelWidth:c,panelHeight:l}=t;return e===`bottom`&&i+s+l>o-Xa&&n.top-s-l>=Xa?`top`:e===`top`&&n.top-s-la-Xa&&n.left-s-c>=Xa?`left`:e===`left`&&n.left-s-ce[t]!==`none`)||/paint|layout|strict|content/.test(e.contain))&&(t=n)}return t?.parentElement??void 0}var io=J({name:`FloatingPopover`,props:{item:{type:Object,required:!1},dismissOnClickOutside:{type:Boolean,default:!0},panelClass:{type:[String,Array],required:!1},ignore:{type:Array,required:!1}},emits:[`dismiss`],setup(e,{emit:t}){let n=K(`panel`),r=B(e.item?.el),i=B(0),a=B();function o(e){a.value=e?ro(e):void 0}let s=nt({width:0,height:0}),c=B(!1);function l(){if(!e.item||!n.value)return;let{width:t,height:r}=n.value.getBoundingClientRect();(Math.abs(t-s.width)>.5||Math.abs(r-s.height)>.5)&&(s.width=t,s.height=r),Ae(()=>{c.value=!0})}Me(()=>{o(e.item?.el),l()}),we(l),Fe(window,`resize`,()=>{r.value&&i.value++}),Fe(window,`scroll`,()=>{r.value&&i.value++},{capture:!0,passive:!0});let u=st(()=>{e.item?.el??(r.value=void 0,s.width=0,s.height=0,c.value=!1)},800);e.dismissOnClickOutside&&We(n,()=>{t(`dismiss`)},{ignore:e.ignore}),Xe(()=>e.item,e=>{e?(r.value===e.el?i.value++:r.value=e.el,o(e.el)):u()});let d,f={},p=e=>a.value?je(Ke,{to:a.value},[e]):e;return()=>{if(i.value,!r.value)return null;let t=c.value?`transition-all duration-300`:`transition-opacity duration-300`;if(!e.item)return p(je(`div`,{ref:`panel`,class:[`fixed z-floating-tooltip text-xs ${t} w-max bg-glass:80 color-base border border-base rounded px2 p1`,`op0 pointer-events-none`,e.panelClass],style:f},d));let{style:n}=to({rect:r.value.getBoundingClientRect(),viewportWidth:window.innerWidth,viewportHeight:window.innerHeight,panelWidth:s.width,panelHeight:s.height,gap:e.item.gap,placement:e.item.placement});f=n;let a=(typeof e.item?.content==`string`?je(`span`,e.item?.content):e.item?.content())??d;return d=a,p(je(`div`,{ref:`panel`,class:[`fixed z-floating-tooltip text-xs ${t} w-max bg-glass:80 color-base border border-base rounded px2 p1`,e.item?`op100`:`op0 pointer-events-none`,e.panelClass],style:n},a))}}}),ao=J({__name:`FloatingElements`,setup(e){let t=de(),n=E(),r=T(),i=ne(),a=ae(),o=b();return Fe(window,`keydown`,e=>{e.key===`Escape`&&a.value&&le(null)}),(e,s)=>(G(),j(H,null,[I(U(io),{item:U(n),onDismiss:s[0]||=()=>U(D)(null)},null,8,[`item`]),I(U(io),{item:U(r),onDismiss:s[1]||=()=>U(O)(null)},null,8,[`item`]),I(U(io),{item:U(i),onDismiss:s[2]||=()=>U(S)(null)},null,8,[`item`]),I(U(io),{item:U(a),onDismiss:s[3]||=()=>U(le)(null)},null,8,[`item`]),I(U(io),{item:U(o),onDismiss:s[4]||=()=>U(C)(null)},null,8,[`item`]),I(U(io),{item:U(t)},null,8,[`item`])],64))}}),oo=[`aria-label`],so=[`value`,`autofocus`,`aria-label`,`aria-invalid`,`disabled`,`onInput`,`onKeydown`,`onPaste`],co=J({__name:`OtpInput`,props:{modelValue:{},length:{default:6},disabled:{type:Boolean,default:!1},invalid:{type:Boolean,default:!1},autofocus:{type:Boolean,default:!0},label:{default:`One-time authorization code`}},emits:[`update:modelValue`,`complete`],setup(e,{expose:t,emit:n}){let r=e,i=n,a=B([]),o=N(()=>Array.from({length:r.length},(e,t)=>r.modelValue[t]??``));function s(e,t){e&&(a.value[t]=e)}function c(e){let t=Math.max(0,Math.min(r.length-1,e));Ae(()=>{let e=a.value[t];e?.focus(),e?.select()})}function l(e){let t=e.replace(/\D/g,``).slice(0,r.length);t!==r.modelValue&&i(`update:modelValue`,t),t.length===r.length&&i(`complete`,t)}function u(e,t){let n=o.value.slice(),i=e;for(let e of t){if(i>=r.length)break;n[i]=e,i++}return l(n.join(``)),i}function d(e,t){let n=t.target.value.replace(/\D/g,``);if(!n){let t=o.value.slice();t[e]=``,l(t.join(``));return}c(u(e,n))}function f(e,t){switch(t.key){case`Backspace`:{t.preventDefault();let n=o.value.slice();n[e]?(n[e]=``,l(n.join(``))):e>0&&(n[e-1]=``,l(n.join(``)),c(e-1));break}case`Delete`:{t.preventDefault();let n=o.value.slice();n[e]=``,l(n.join(``));break}case`ArrowLeft`:t.preventDefault(),c(e-1);break;case`ArrowRight`:t.preventDefault(),c(e+1);break;case`Home`:t.preventDefault(),c(0);break;case`End`:t.preventDefault(),c(r.length-1)}}function p(e,t){t.preventDefault();let n=(t.clipboardData?.getData(`text`)??``).replace(/\D/g,``);n&&c(u(e,n.slice(0,r.length-e)))}function m(e){e.target.select()}function h(e){return r.invalid?`border-red-500/70 text-red-500`:o.value[e]?`border-primary-400/60 dark:border-primary-400/50`:`border-base`}return t({focus(){let e=o.value.findIndex(e=>!e);c(e===-1?r.length-1:e)}}),(t,n)=>(G(),j(`div`,{role:`group`,"aria-label":e.label,class:V([`flex items-center justify-center gap-2 sm:gap-2.5`,e.invalid?`devframes-shake`:``])},[(G(!0),j(H,null,L(o.value,(t,n)=>(G(),j(`input`,{key:n,ref_for:!0,ref:e=>s(e,n),value:t,type:`text`,inputmode:`numeric`,autocomplete:`one-time-code`,"data-1p-ignore":``,"data-lpignore":`true`,"data-bwignore":`true`,"data-form-type":`other`,autofocus:e.autofocus&&n===0,"aria-label":`Digit ${n+1} of ${e.length}`,"aria-invalid":e.invalid||void 0,disabled:e.disabled,maxlength:`1`,class:V([`w-11 h-14 text-center text-2xl font-mono tabular-nums font-medium rounded-xl border bg-base color-base caret-primary-500 outline-none transition-all duration-150 focus:border-primary-500 focus:ring-3 focus:ring-primary-500/25 disabled:op40 disabled:pointer-events-none`,h(n)]),onInput:e=>d(n,e),onKeydown:e=>f(n,e),onPaste:e=>p(n,e),onFocus:m},null,42,so))),128))],10,oo))}}),lo={class:`w-full h-full flex items-center justify-center p8 color-base overflow-auto`},uo={class:`w-full max-w-108 flex flex-col items-center text-center`},fo={class:`relative flex items-center justify-center`},po={class:`text-2xl font-bold tracking-tight`},mo={class:`mt2 text-sm op-fade leading-relaxed max-w-92`},ho={class:`mt6 w-full rounded-lg border border-base bg-base/60 shadow-sm p6 flex flex-col items-center gap-4`},go=6,_o=J({__name:`ViewBuiltinClientAuthNotice`,props:{context:{}},setup(e){let t=e,n=qe(),r=B(``),i=B(``),a=B(!1),o=K(`otp`),s=!1;Xe(r,()=>{if(s){s=!1;return}i.value&&=``});async function c(){let e=r.value;if(!(e.length(G(),j(`div`,lo,[P(`div`,uo,[P(`div`,fo,[t[1]||=P(`div`,{class:`absolute w-24 h-24 rounded-full bg-primary-500/20 blur-2xl`,"aria-hidden":`true`},null,-1),I(ur,{class:`relative w-16 h-16`})]),P(`h1`,po,` Authorize `+q(U(n).productName),1),P(`p`,mo,q(U(n).productName)+` can access your server, read your filesystem, and run commands. Confirm it's you before continuing. `,1),P(`div`,ho,[t[2]||=P(`div`,{class:`flex flex-col items-center gap-1`},[P(`p`,{class:`text-sm op-fade`},[M(` Find the `),P(`span`,{class:`font-mono text-primary-600 dark:text-primary-300`},`6-digit code`),M(` printed in your terminal. `)])],-1),P(`form`,{class:`flex flex-col items-center gap-4 w-full`,autocomplete:`off`,onSubmit:Te(c,[`prevent`])},[I(co,{ref_key:`otp`,ref:o,modelValue:r.value,"onUpdate:modelValue":t[0]||=e=>r.value=e,length:go,invalid:!!i.value,disabled:a.value,label:`Enter your one-time authorization code`,onComplete:c},null,8,[`modelValue`,`invalid`,`disabled`]),I(ma,{type:`submit`,variant:`primary`,icon:`i-ph-shield-check-duotone`,class:`w-full justify-center text-base py-2! disabled:saturate-0`,loading:a.value,disabled:r.value.length[M(q(a.value?`Authorizing`:`Authorize`),1)]),_:1},8,[`loading`,`disabled`]),i.value||a.value?(G(),j(`p`,{key:0,class:V([`text-sm min-h-5 transition-colors`,i.value?`text-red-500`:`op-mute`]),role:`alert`,"aria-live":`assertive`},[i.value?(G(),j(H,{key:0},[M(q(i.value),1)],64)):a.value?(G(),j(H,{key:1},[M(` Authorizing... `)],64)):A(``,!0)],2)):A(``,!0)],32)]),t[3]||=P(`p`,{class:`mt4 text-xs op-mute max-w-92 leading-relaxed`},` You can also use the magic link to authorize automatically. `,-1)])]))}}),vo={class:`flex flex-col gap-6`},yo={class:`flex items-center gap-3 cursor-pointer group`},bo=J({__name:`SettingsAdvanced`,props:{context:{}},setup(e){let t=e,n=oe(t.context),r=t.context.docks.settings,i=qe(),a=ga();async function o(){await a({title:`Reset All Settings`,message:`Reset all settings to defaults? This includes appearance, docks, and shortcuts.`})&&r.mutate(()=>be())}async function s(){await a({title:`Reset Keyboard Shortcuts`,message:`Reset all keyboard shortcuts to defaults?`})&&r.mutate(e=>{e.commandShortcuts={}})}async function c(){await a({title:`Reset Dock Settings`,message:`Reset dock visibility, order, and pinning to defaults?`})&&r.mutate(e=>{let t=be();e.docksHidden=t.docksHidden,e.docksCategoriesHidden=t.docksCategoriesHidden,e.docksCustomOrder=t.docksCustomOrder,e.docksPinned=t.docksPinned})}async function l(){await a({title:`Revoke Authorization`,message:`Revoke this browser's access to ${i.value.productName}? You'll need to authorize again with a new code to reconnect.`})&&await t.context.rpc.call(`devframe:auth:revoke`)}return(e,t)=>(G(),j(`div`,vo,[P(`label`,yo,[P(`button`,{class:V([`w-10 h-6 rounded-full transition-colors relative shrink-0`,U(n).showDevframeInspector?`bg-primary`:`bg-gray/30`]),onClick:t[0]||=e=>U(r).mutate(e=>{e.showDevframeInspector=!U(n).showDevframeInspector})},[P(`div`,{class:V([`absolute top-1 w-4 h-4 rounded-full bg-white shadow transition-transform`,U(n).showDevframeInspector?`translate-x-5`:`translate-x-1`])},null,2)],2),t[1]||=P(`div`,{class:`flex flex-col`},[P(`span`,{class:`text-sm`},`Show Devframe Inspector`),P(`span`,{class:`text-xs op50`},`Reveal the experimental Devframe Inspector dock, the devtools for the devtools`)],-1)]),t[10]||=P(`div`,{class:`border-t border-base`},null,-1),P(`div`,{class:`flex items-start gap-4`},[t[3]||=P(`div`,{class:`flex-1`},[P(`div`,{class:`text-sm`},` Reset Keyboard Shortcuts `),P(`div`,{class:`text-xs op50 mt-0.5`},` Remove all custom shortcut overrides and restore default keybindings `)],-1),P(`button`,{class:`px-4 py-2 rounded bg-orange/10 text-orange hover:bg-orange/20 transition-colors flex items-center gap-2 text-sm shrink-0`,onClick:s},[...t[2]||=[P(`div`,{class:`i-ph-keyboard-duotone w-4 h-4`},null,-1),M(` Reset Shortcuts `,-1)]])]),P(`div`,{class:`flex items-start gap-4`},[t[5]||=P(`div`,{class:`flex-1`},[P(`div`,{class:`text-sm`},` Reset Dock Settings `),P(`div`,{class:`text-xs op50 mt-0.5`},` Restore default dock visibility, order, and pinning `)],-1),P(`button`,{class:`px-4 py-2 rounded bg-orange/10 text-orange hover:bg-orange/20 transition-colors flex items-center gap-2 text-sm shrink-0`,onClick:c},[...t[4]||=[P(`div`,{class:`i-ph-layout-duotone w-4 h-4`},null,-1),M(` Reset Docks `,-1)]])]),P(`div`,{class:`border-t border-base pt-6`},[P(`div`,{class:`flex items-start gap-4`},[t[7]||=P(`div`,{class:`flex-1`},[P(`div`,{class:`text-sm`},` Reset All Settings `),P(`div`,{class:`text-xs op50 mt-0.5`},` Reset everything to defaults including appearance, docks, and shortcuts `)],-1),P(`button`,{class:`px-4 py-2 rounded bg-red/10 text-red hover:bg-red/20 transition-colors flex items-center gap-2 text-sm shrink-0`,onClick:o},[...t[6]||=[P(`div`,{class:`i-ph-arrow-counter-clockwise w-4 h-4`},null,-1),M(` Reset All `,-1)]])])]),P(`div`,{class:`border-t border-base pt-6`},[P(`div`,{class:`flex items-start gap-4`},[t[9]||=P(`div`,{class:`flex-1`},[P(`div`,{class:`text-sm`},` Revoke Authorization `),P(`div`,{class:`text-xs op50 mt-0.5`},` De-authorize this browser and revoke its access token; you'll re-authorize with a new code `)],-1),P(`button`,{class:`px-4 py-2 rounded bg-red/10 text-red hover:bg-red/20 transition-colors flex items-center gap-2 text-sm shrink-0`,onClick:l},[...t[8]||=[P(`div`,{class:`i-ph-sign-out-duotone w-4 h-4`},null,-1),M(` Revoke Access `,-1)]])])])]))}}),xo={class:`flex flex-col gap-4`},So={class:`flex flex-col gap-2`},Co={class:`flex flex-col`},wo={class:`text-xs op50`},To={class:`flex items-center gap-1 bg-gray/10 rounded-lg p1 w-fit`},Eo=[`onClick`],Do={key:0,class:`flex flex-col gap-2`},Oo={class:`flex flex-col`},ko={class:`text-xs op50`},Ao={class:`flex items-center gap-1 bg-gray/10 rounded-lg p1 w-fit`},jo=[`onClick`],Mo={class:`flex items-center gap-3 cursor-pointer group`},No={class:`flex items-center gap-3 cursor-pointer group`},Po={class:`flex flex-col`},Fo={class:`text-xs op50`},Io={class:`flex items-center gap-3 cursor-pointer group`},Lo=J({__name:`SettingsAppearance`,props:{context:{}},setup(e){let t=e,n=oe(t.context),r=t.context.docks.settings,i=qe(),a=t.context.panel.store,o=t.context.clientType===`embedded`,s=ie(),c=N(()=>{let e=[{value:`float`,label:`Float`,icon:`i-ph-cards-three-duotone`},{value:`edge`,label:`Edge`,icon:`i-ph-square-half-bottom-duotone`}];return ue()&&e.push({value:`popup`,label:`Popup`,icon:`i-ph-arrow-square-out-duotone`}),e}),l=N(()=>a.mode),u=[{value:`auto`,label:`Auto`,icon:`i-ph-laptop-duotone`},{value:`light`,label:`Light`,icon:`i-ph-sun-duotone`},{value:`dark`,label:`Dark`,icon:`i-ph-moon-duotone`}];function d(e){e===`popup`?se(t.context):a.mode=e}return(e,t)=>(G(),j(`div`,xo,[P(`div`,So,[P(`div`,Co,[t[3]||=P(`span`,{class:`text-sm`},`Color mode`,-1),P(`span`,wo,`Theme for `+q(U(i).productName)+` and its inner panels`,1)]),P(`div`,To,[(G(),j(H,null,L(u,e=>P(`button`,{key:e.value,class:V([`flex items-center gap-1.5 px3 py1.5 rounded-md text-sm transition-all`,U(Re)===e.value?`bg-base shadow text-primary font-medium`:`op60 hover:op100 hover:bg-gray/10`]),onClick:t=>U(Qe)(e.value)},[P(`div`,{class:V([e.icon,`w-4 h-4`])},null,2),M(` `+q(e.label),1)],10,Eo)),64))])]),o&&!U(s)?(G(),j(`div`,Do,[P(`div`,Oo,[t[4]||=P(`span`,{class:`text-sm`},`Dock mode`,-1),P(`span`,ko,`How the `+q(U(i).productName)+` panel is displayed`,1)]),P(`div`,Ao,[(G(!0),j(H,null,L(c.value,e=>(G(),j(`button`,{key:e.value,class:V([`flex items-center gap-1.5 px3 py1.5 rounded-md text-sm transition-all`,l.value===e.value?`bg-base shadow text-primary font-medium`:`op60 hover:op100 hover:bg-gray/10`]),onClick:t=>d(e.value)},[P(`div`,{class:V([e.icon,`w-4 h-4`])},null,2),M(` `+q(e.label),1)],10,jo))),128))])])):A(``,!0),P(`label`,Mo,[P(`button`,{class:V([`w-10 h-6 rounded-full transition-colors relative shrink-0`,U(n).showIframeAddressBar?`bg-primary`:`bg-gray/30`]),onClick:t[0]||=e=>U(r).mutate(e=>{e.showIframeAddressBar=!U(n).showIframeAddressBar})},[P(`div`,{class:V([`absolute top-1 w-4 h-4 rounded-full bg-white shadow transition-transform`,U(n).showIframeAddressBar?`translate-x-5`:`translate-x-1`])},null,2)],2),t[5]||=P(`div`,{class:`flex flex-col`},[P(`span`,{class:`text-sm`},`Always show iframe address bars`),P(`span`,{class:`text-xs op50`},`Show the address bar for every iframe dock.`)],-1)]),P(`label`,No,[P(`button`,{class:V([`w-10 h-6 rounded-full transition-colors relative shrink-0`,U(n).closeOnOutsideClick?`bg-primary`:`bg-gray/30`]),onClick:t[1]||=e=>U(r).mutate(e=>{e.closeOnOutsideClick=!U(n).closeOnOutsideClick})},[P(`div`,{class:V([`absolute top-1 w-4 h-4 rounded-full bg-white shadow transition-transform`,U(n).closeOnOutsideClick?`translate-x-5`:`translate-x-1`])},null,2)],2),P(`div`,Po,[t[6]||=P(`span`,{class:`text-sm`},`Close panel on outside click`,-1),P(`span`,Fo,`Close the `+q(U(i).productName)+` panel when clicking outside of it (embedded mode only)`,1)])]),P(`label`,Io,[P(`button`,{class:V([`w-10 h-6 rounded-full transition-colors relative shrink-0`,U(n).autoCollapseEdgeToolbar?`bg-primary`:`bg-gray/30`]),onClick:t[2]||=e=>U(r).mutate(e=>{e.autoCollapseEdgeToolbar=!U(n).autoCollapseEdgeToolbar})},[P(`div`,{class:V([`absolute top-1 w-4 h-4 rounded-full bg-white shadow transition-transform`,U(n).autoCollapseEdgeToolbar?`translate-x-5`:`translate-x-1`])},null,2)],2),t[7]||=P(`div`,{class:`flex flex-col`},[P(`span`,{class:`text-sm`},`Auto-collapse edge toolbar`),P(`span`,{class:`text-xs op50`},`Shrink the edge-docked toolbar to a small handle when idle and closed (edge mode only)`)],-1)])]))}}),Ro={ref:`sortContainer`,class:`flex flex-col gap-4`},zo={class:`flex items-center gap-2 px-4 py-3 bg-gray/5 cursor-pointer select-none border-b border-base`},Bo=[`disabled`,`onClick`],Vo={class:`font-medium capitalize`},Ho={class:`text-xs op40`},Uo=[`onClick`],Wo=[`data-dock-id`,`data-container`],Go=[`disabled`,`title`,`onClick`],Ko={key:0,class:`i-ph-check-bold text-xs`},qo={class:`flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity`},Jo=[`onClick`],Yo=[`onClick`],Xo=[`title`,`onClick`],Zo={class:`bg-gray/3 flex flex-col w-full flex-auto`},Qo={key:0,class:`px-2 py-2 text-xs op40 italic border-b border-base border-t-0`},$o={key:0,class:`flex items-center gap-2 px-2 py-1.5 bg-gray/5 border-b border-base border-t-0`},es={class:`text-xs uppercase tracking-wide op50`},ts=[`onClick`],ns=[`data-dock-id`,`data-container`],rs=[`title`,`onClick`],is={key:0,class:`i-ph-check-bold text-xs`},as={class:`flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity`},os=[`onClick`],ss=[`onClick`],cs=[`title`,`onClick`],ls=`:::`,us=4,ds=J({__name:`SettingsDocks`,props:{context:{}},setup(e){let t=e,r=oe(t.context),i=t.context.docks.settings,a=N(()=>s(t.context.docks.entries,r.value,{includeHidden:!0,collapseGroups:!0}));function o(e){return n(t.context.docks.entries,e,r.value,{includeHidden:!0})}function c(e){return v(t.context.docks.entries,e,r.value,{includeHidden:!0})}let l=e=>`cat:${e}`,u=(e,t)=>`grp:${e}${ls}${t}`;function d(e){let t=e.slice(4),n=t.indexOf(ls);return{groupId:t.slice(0,n),subcategory:t.slice(n+3)}}function f(e){if(e.startsWith(`grp:`)){let{groupId:t,subcategory:n}=d(e);return c(t).find(([e])=>e===n)?.[1]??[]}let t=e.slice(4);return a.value.find(([e])=>e===t)?.[1]??[]}function p(e){let n={...r.value,docksCustomOrder:{}};if(e.startsWith(`grp:`)){let{groupId:r,subcategory:i}=d(e);return v(t.context.docks.entries,r,n,{includeHidden:!0}).find(([e])=>e===i)?.[1]??[]}let i=e.slice(4);return s(t.context.docks.entries,n,{includeHidden:!0,collapseGroups:!0}).find(([e])=>e===i)?.[1]??[]}let h=K(`sortContainer`),g=new Map,y=B(null),b=B(null),x=B(null),S=B(!1),C=0;function w(e){let t=e.target;for(;t&&t!==h.value;){if(t.dataset.dockId)return{dockId:t.dataset.dockId,container:t.dataset.container??null};t=t.parentElement}return{dockId:null,container:null}}function T(e){for(;e&&e!==h.value;){if(e.tagName===`BUTTON`||e.tagName===`A`||e.tagName===`INPUT`)return!0;e=e.parentElement}return!1}function E(e,t){let n=p(e).map(e=>e.id),r=t.length===n.length&&t.every((e,t)=>e.id===n[t]);i.mutate(e=>{t.forEach((t,n)=>{r?delete e.docksCustomOrder[t.id]:e.docksCustomOrder[t.id]=n})})}He(h,{draggingElement:()=>h.value?.ownerDocument?.defaultView??window,onStart(e,t){if(t.button!==0||T(t.target))return!1;let{dockId:n,container:r}=w(t);if(!n||!r)return!1;y.value=n,b.value=r,S.value=!1,C=t.clientY},onMove(e,t){if(!y.value)return;if(!S.value){if(Math.abs(t.clientY-C)=a.top&&t.clientY<=a.bottom){n=e;break}}x.value=n},onEnd(){if(y.value&&S.value&&x.value&&b.value){let e=[...f(b.value)],t=e.findIndex(e=>e.id===y.value),n=e.findIndex(e=>e.id===x.value);t!==-1&&n!==-1&&(e.splice(n,0,e.splice(t,1)[0]),E(b.value,e))}y.value=null,b.value=null,x.value=null,S.value=!1}});function ee(e,t,n){e?g.set(t,{el:e,container:n}):g.delete(t)}function te(e,t){if(e===`~settings`)return;let n=r.value.docksHidden.includes(e);t??n?i.mutate(t=>{t.docksHidden=t.docksHidden.filter(t=>t!==e)}):i.mutate(t=>{t.docksHidden=[...t.docksHidden,e]})}function D(e,t){if(!_(e))return;let n=r.value.docksCategoriesHidden.includes(e);t??n?i.mutate(t=>{t.docksCategoriesHidden=t.docksCategoriesHidden.filter(t=>t!==e)}):i.mutate(t=>{t.docksCategoriesHidden=[...t.docksCategoriesHidden,e]})}function ne(e){r.value.docksPinned.includes(e)?i.mutate(t=>{t.docksPinned=t.docksPinned.filter(t=>t!==e)}):i.mutate(t=>{t.docksPinned=[...t.docksPinned,e]})}function re(e,t,n){let r=[...f(e)],i=r.findIndex(e=>e.id===t),a=i+n;a<0||a>=r.length||(r.splice(a,0,r.splice(i,1)[0]),E(e,r))}function ie(e){let t=f(e),n=t.map(e=>e.id);if(e.startsWith(`cat:`))for(let e of t)e.type===`group`&&n.push(...o(e.id).map(e=>e.id));return n}function ae(e){let t=f(e).map(e=>e.id),n=p(e).map(e=>e.id);return t.length!==n.length||t.some((e,t)=>e!==n[t])?!0:e.startsWith(`cat:`)?f(e).some(e=>e.type===`group`&&c(e.id).some(([t])=>ae(u(e.id,t)))):!1}function se(e){let t=ie(e);i.mutate(e=>{t.forEach(t=>{delete e.docksCustomOrder[t]})})}return(e,t)=>(G(),j(H,null,[t[11]||=P(`p`,{class:`text-sm op50 mb-4`},` Manage visibility and order of dock entries. Hidden entries will not appear in the dock bar. `,-1),P(`div`,Ro,[(G(!0),j(H,null,L(a.value,([e,n])=>(G(),j(`div`,{key:e,class:V([`border border-base rounded-lg overflow-hidden transition-opacity`,U(r).docksCategoriesHidden.includes(e)?`op40`:``])},[P(`div`,zo,[P(`button`,{class:V([`w-5 h-5 flex items-center justify-center rounded transition-colors`,[U(_)(e)?U(r).docksCategoriesHidden.includes(e)?`bg-gray/20`:`bg-primary/20 text-primary`:`bg-gray/20 cursor-not-allowed op50`]]),disabled:!U(_)(e),onClick:t=>D(e)},[P(`div`,{class:V([`transition-transform`,U(r).docksCategoriesHidden.includes(e)?`i-ph-eye-slash text-sm op50`:`i-ph-check-bold text-xs`])},null,2)],10,Bo),P(`span`,Vo,q(U(m)(e)),1),P(`span`,Ho,`(`+q(n.length)+`)`,1),t[1]||=P(`span`,{class:`flex-auto`},null,-1),ae(l(e))?(G(),j(`button`,{key:0,class:`w-6 h-6 flex items-center justify-center rounded hover:bg-gray/20 transition-colors`,title:`Reset custom order`,onClick:t=>se(l(e))},[...t[0]||=[P(`div`,{class:`i-ph-arrows-counter-clockwise-duotone text-sm op60`},null,-1)]],8,Uo)):A(``,!0)]),P(`div`,null,[(G(!0),j(H,null,L(n,(i,a)=>(G(),j(H,{key:i.id},[P(`div`,{ref_for:!0,ref:t=>ee(t,i.id,l(e)),"data-dock-id":i.id,"data-container":l(e),class:V([`flex items-center gap-3 px-2 py-2.5 hover:bg-gray/5 transition-all group border-b border-base border-t-0`,[U(r).docksHidden.includes(i.id)?`op40`:``,S.value&&y.value===i.id?`op30 bg-gray/10`:``,x.value===i.id?`ring-1.5 ring-purple/50 rounded`:``,S.value?`select-none`:``]])},[P(`div`,{class:`i-ph-dots-six-vertical w-4 h-4 shrink-0 op25 group-hover:op50 transition-opacity cursor-grab`,style:R(S.value&&y.value===i.id?`cursor: grabbing`:``)},null,4),P(`button`,{class:V([`w-6 h-6 flex items-center justify-center rounded border border-transparent hover:border-base transition-colors shrink-0`,i.id===`~settings`?`cursor-not-allowed op50`:U(r).docksHidden.includes(i.id)?`op50`:``]),disabled:i.id===`~settings`,title:i.id===`~settings`?`Always visible`:U(r).docksHidden.includes(i.id)?`Show`:`Hide`,onClick:e=>te(i.id)},[P(`div`,{class:V([`w-4 h-4 rounded flex items-center justify-center transition-colors`,i.id===`~settings`||U(r).docksHidden.includes(i.id)?`bg-gray/30`:`bg-primary/20 text-primary`])},[U(r).docksHidden.includes(i.id)?A(``,!0):(G(),j(`div`,Ko))],2)],10,Go),I(Ui,{icon:i.icon,title:i.title,class:V([`w-5 h-5 shrink-0`,U(r).docksHidden.includes(i.id)?`saturate-0`:``])},null,8,[`icon`,`title`,`class`]),P(`span`,{class:V([`truncate`,U(r).docksHidden.includes(i.id)?`line-through op60`:``])},q(i.title),3),i.type===`action`?(G(),F(Pa,{key:0,label:`Action`,class:`flex-none text-xs`})):i.type===`group`?(G(),F(Pa,{key:1,label:`Group`,class:`flex-none text-xs`})):A(``,!0),t[4]||=P(`div`,{class:`flex flex-auto`},null,-1),P(`div`,qo,[a>0?(G(),j(`button`,{key:0,class:`w-6 h-6 flex items-center justify-center rounded hover:bg-gray/20 transition-colors`,title:`Move up (higher priority)`,onClick:t=>re(l(e),i.id,-1)},[...t[2]||=[P(`div`,{class:`i-ph-caret-up text-sm op60`},null,-1)]],8,Jo)):A(``,!0),are(l(e),i.id,1)},[...t[3]||=[P(`div`,{class:`i-ph-caret-down text-sm op60`},null,-1)]],8,Yo)):A(``,!0)]),P(`button`,{class:V([`w-7 h-7 flex items-center justify-center rounded hover:bg-gray/20 transition-colors shrink-0`,U(r).docksPinned.includes(i.id)?`text-amber`:`op40 hover:op70`]),title:U(r).docksPinned.includes(i.id)?`Unpin`:`Pin`,onClick:e=>ne(i.id)},[P(`div`,{class:V([U(r).docksPinned.includes(i.id)?`i-ph-push-pin-fill rotate--45`:`i-ph-push-pin`,`color-base`])},null,2)],10,Xo)],10,Wo),i.type===`group`?(G(),j(`div`,{key:0,class:V([`flex`,U(r).docksHidden.includes(i.id)?`op50`:``])},[t[10]||=P(`div`,{class:`border-base border-r border-b w-6 flex-none`},null,-1),P(`div`,Zo,[o(i.id).length===0?(G(),j(`div`,Qo,` No tools in this group yet `)):A(``,!0),(G(!0),j(H,null,L(c(i.id),([e,n])=>(G(),j(H,{key:e},[c(i.id).length>1?(G(),j(`div`,$o,[P(`span`,es,q(U(m)(e)),1),t[6]||=P(`span`,{class:`flex-auto`},null,-1),ae(u(i.id,e))?(G(),j(`button`,{key:0,class:`w-5 h-5 flex items-center justify-center rounded hover:bg-gray/20 transition-colors`,title:`Reset custom order`,onClick:t=>se(u(i.id,e))},[...t[5]||=[P(`div`,{class:`i-ph-arrows-counter-clockwise-duotone text-xs op60`},null,-1)]],8,ts)):A(``,!0)])):A(``,!0),(G(!0),j(H,null,L(n,(a,o)=>(G(),j(`div`,{key:a.id,ref_for:!0,ref:t=>ee(t,a.id,u(i.id,e)),"data-dock-id":a.id,"data-container":u(i.id,e),class:V([`flex items-center gap-3 px-2 py-2 hover:bg-gray/5 transition-all group border-b border-base border-t-0`,[U(r).docksHidden.includes(a.id)?`op40`:``,S.value&&y.value===a.id?`op30 bg-gray/10`:``,x.value===a.id?`ring-1.5 ring-purple/50 rounded`:``,S.value?`select-none`:``]])},[P(`div`,{class:`i-ph-dots-six-vertical w-4 h-4 shrink-0 op25 group-hover:op50 transition-opacity cursor-grab`,style:R(S.value&&y.value===a.id?`cursor: grabbing`:``)},null,4),P(`button`,{class:V([`w-6 h-6 flex items-center justify-center rounded border border-transparent hover:border-base transition-colors shrink-0`,U(r).docksHidden.includes(a.id)?`op50`:``]),title:U(r).docksHidden.includes(a.id)?`Show`:`Hide`,onClick:e=>te(a.id)},[P(`div`,{class:V([`w-4 h-4 rounded flex items-center justify-center transition-colors`,U(r).docksHidden.includes(a.id)?`bg-gray/30`:`bg-primary/20 text-primary`])},[U(r).docksHidden.includes(a.id)?A(``,!0):(G(),j(`div`,is))],2)],10,rs),I(Ui,{icon:a.icon,title:a.title,class:V([`w-5 h-5 shrink-0`,U(r).docksHidden.includes(a.id)?`saturate-0`:``])},null,8,[`icon`,`title`,`class`]),P(`span`,{class:V([`truncate text-sm`,U(r).docksHidden.includes(a.id)?`line-through op60`:``])},q(a.title),3),a.type===`action`?(G(),F(Pa,{key:0,label:`Action`,class:`flex-none text-xs`})):A(``,!0),t[9]||=P(`div`,{class:`flex flex-auto`},null,-1),P(`div`,as,[o>0?(G(),j(`button`,{key:0,class:`w-6 h-6 flex items-center justify-center rounded hover:bg-gray/20 transition-colors`,title:`Move up (higher priority)`,onClick:t=>re(u(i.id,e),a.id,-1)},[...t[7]||=[P(`div`,{class:`i-ph-caret-up text-sm op60`},null,-1)]],8,os)):A(``,!0),ore(u(i.id,e),a.id,1)},[...t[8]||=[P(`div`,{class:`i-ph-caret-down text-sm op60`},null,-1)]],8,ss)):A(``,!0)]),P(`button`,{class:V([`w-7 h-7 flex items-center justify-center rounded hover:bg-gray/20 transition-colors shrink-0`,U(r).docksPinned.includes(a.id)?`text-amber`:`op40 hover:op70`]),title:U(r).docksPinned.includes(a.id)?`Unpin`:`Pin`,onClick:e=>ne(a.id)},[P(`div`,{class:V([U(r).docksPinned.includes(a.id)?`i-ph-push-pin-fill rotate--45`:`i-ph-push-pin`,`color-base`])},null,2)],10,cs)],10,ns))),128))],64))),128))])],2)):A(``,!0)],64))),128))])],2))),128))],512)],64))}}),fs={class:`mb-3`},ps={class:`border border-base rounded-lg overflow-hidden`},ms={class:`flex-1 min-w-0`},hs={class:`flex items-center gap-1.5`},gs={class:`truncate text-sm`},_s={key:0,class:`text-[10px] px-1.5 py-0.5 rounded bg-blue/10 text-blue shrink-0`},vs={key:0,class:`text-xs op40 truncate mt-0.5`},ys={key:2,class:`flex items-center gap-1.5 shrink-0`},bs=[`onClick`],xs=[`onClick`],Ss=[`onClick`],Cs=[`onClick`],ws={key:0,class:`py-6 text-center text-sm op40`},Ts={class:`relative bg-base border border-base rounded-lg shadow-xl w-96 p-5`},Es={class:`mb-4`},Ds=[`value`],Os={class:`flex items-center gap-2 mb-4`},ks={key:0,class:`mb-4 flex flex-col gap-1.5`},As={class:`flex items-center justify-end gap-2`},js=[`disabled`],Ms=J({__name:`SettingsShortcuts`,props:{context:{}},setup(e){let n=e,r=n.context.commands,a=oe(n.context),o=N(()=>a.value.commandShortcuts??{}),s=B(``),c=N(()=>d(r.commands,{...n.context.when.context,dockOpen:!0,paletteOpen:!1})),l=N(()=>{let e=[];return f(c.value,(t,n)=>{let r=n.at(-1)?.title;e.push({command:t,...r?{parentTitle:r}:{},depth:n.length})}),e}),p=N(()=>{if(!s.value)return l.value;let e=s.value.toLowerCase();return l.value.filter(t=>t.command.title.toLowerCase().includes(e)||t.command.id.toLowerCase().includes(e)||t.command.description?.toLowerCase().includes(e))});function m(e){return r.getKeybindings(e)}function h(e){return e.depth>0?{marginLeft:`${e.depth*1.5}rem`}:{}}function g(e){return e.source===`server`||!!e.action}function _(e){return ee(o.value[e],v(e))}function v(e){return te(r.commands,e)?.keybindings??[]}function y(e){r.settings.mutate(t=>{v(e).length>0?t.commandShortcuts[e]=[]:delete t.commandShortcuts[e]})}function b(e){r.settings.mutate(t=>{delete t.commandShortcuts[e]})}let x=B(!1),S=B(null),C=B(!1),w=B(!1),T=B(!1),E=B(!1),D=B(``);function ne(e){let t=m(e);if(S.value=e,t.length>0){let e=t[0].key.split(`+`);C.value=e.includes(`Mod`),w.value=e.includes(`Ctrl`),T.value=e.includes(`Alt`),E.value=e.includes(`Shift`),D.value=e.filter(e=>![`Mod`,`Ctrl`,`Alt`,`Shift`,`Meta`].includes(e)).join(`+`)}else C.value=!1,w.value=!1,T.value=!1,E.value=!1,D.value=``;x.value=!0}function re(){x.value=!1,S.value=null}let ie=N(()=>{let e=[];return C.value&&e.push(`Mod`),w.value&&e.push(`Ctrl`),T.value&&e.push(`Alt`),E.value&&e.push(`Shift`),D.value&&e.push(D.value),e.join(`+`)}),ae=N(()=>{let e=[],n=ie.value;if(!n)return e;let r=C.value||w.value||T.value;!r&&D.value&&e.push(`Single key without modifiers may interfere with typing`),!r&&E.value&&D.value.length===1&&e.push(`Shift + letter may interfere with typing`);let a=i[n];if(a){let r=t(n).join(`+`);e.push(`Conflicts with browser shortcut: ${a} (${r})`)}if(S.value){for(let t of l.value)if(t.command.id!==S.value&&m(t.command.id).some(e=>e.key===n)){e.push(`Conflicts with "${t.command.title}"`);break}}return e}),se=N(()=>D.value.length>0);function ce(e){if(e.preventDefault(),e.stopPropagation(),[`Control`,`Meta`,`Alt`,`Shift`].includes(e.key))return;C.value=u?e.metaKey:e.ctrlKey,u&&e.ctrlKey&&(w.value=!0),T.value=e.altKey,E.value=e.shiftKey;let t=e.key;t.length===1&&(t=t.toUpperCase()),D.value=t}function le(){if(!S.value||!se.value)return;let e=[{key:ie.value}];ee(e,v(S.value))?r.settings.mutate(t=>{t.commandShortcuts[S.value]=e}):r.settings.mutate(e=>{delete e.commandShortcuts[S.value]}),re()}let ue=B(null);return Xe(x,async e=>{if(!e)return;await Ae();let t=ue.value?.ownerDocument??document,n=t.defaultView??window,r=e=>{if(e.key===`Escape`){let e=t.activeElement;(!e||!e.classList.contains(`shortcut-key-input`))&&(re(),n.removeEventListener(`keydown`,r))}};n.addEventListener(`keydown`,r)}),(e,n)=>(G(),j(H,null,[n[10]||=P(`p`,{class:`text-sm op50 mb-4`},` Customize keyboard shortcuts for commands. `,-1),P(`div`,fs,[Ze(P(`input`,{"onUpdate:modelValue":n[0]||=e=>s.value=e,class:`w-full bg-gray/10 rounded-lg px-3 py-2 text-sm outline-none focus:ring-1 focus:ring-primary/30`,placeholder:`Search commands...`},null,512),[[ke,s.value]])]),P(`div`,ps,[(G(!0),j(H,null,L(p.value,e=>(G(),j(`div`,{key:e.command.id,class:`flex items-center gap-3 px-4 py-2.5 hover:bg-gray/5 transition-colors border-b border-base last:border-b-0`},[e.command.icon?(G(),F(Ui,{key:0,icon:e.command.icon,class:`w-4 h-4 shrink-0 op60`,style:R(h(e))},null,8,[`icon`,`style`])):(G(),j(`div`,{key:1,style:R(h(e)),class:`w-4 h-4 shrink-0`},null,4)),P(`div`,ms,[P(`div`,hs,[P(`span`,gs,q(e.command.title),1),e.command.source===`server`?(G(),j(`span`,_s,`server`)):A(``,!0)]),e.command.description?(G(),j(`div`,vs,q(e.command.description),1)):A(``,!0)]),g(e.command)?(G(),j(`div`,ys,[m(e.command.id).length>0?(G(!0),j(H,{key:0},L(m(e.command.id),(t,n)=>(G(),j(`button`,{key:n,class:`flex items-center gap-0.5 hover:bg-gray/10 rounded px-1 py-0.5 transition-colors`,title:`Click to edit`,onClick:t=>ne(e.command.id)},[I(sr,{keys:t.key},null,8,[`keys`])],8,bs))),128)):(G(),j(`button`,{key:1,class:`text-xs op30 hover:op60 px-2 py-1 rounded hover:bg-gray/10 transition-colors`,onClick:t=>ne(e.command.id)},` Add shortcut `,8,xs)),m(e.command.id).length>0?(G(),j(`button`,{key:2,class:`w-5 h-5 flex items-center justify-center rounded hover:bg-gray/20 transition-colors op30 hover:op70`,title:`Clear shortcut`,onClick:t=>y(e.command.id)},[...n[5]||=[P(`div`,{class:`i-ph-x text-xs`},null,-1)]],8,Ss)):A(``,!0),_(e.command.id)?(G(),j(`button`,{key:3,class:`w-5 h-5 flex items-center justify-center rounded hover:bg-gray/20 transition-colors op30 hover:op70`,title:`Reset to default`,onClick:t=>b(e.command.id)},[...n[6]||=[P(`div`,{class:`i-ph-arrow-counter-clockwise text-xs`},null,-1)]],8,Cs)):A(``,!0)])):A(``,!0)]))),128)),p.value.length===0?(G(),j(`div`,ws,` No commands found `)):A(``,!0)]),x.value?(G(),j(`div`,{key:0,ref_key:`editorModal`,ref:ue,class:`fixed inset-0 z-command-palette flex items-center justify-center`},[P(`div`,{class:`absolute inset-0 bg-black/30`,onClick:re}),P(`div`,Ts,[n[9]||=P(`h3`,{class:`text-sm font-medium mb-4`},` Edit Shortcut `,-1),P(`div`,Es,[n[7]||=P(`label`,{class:`text-xs op50 mb-1.5 block`},`Press a key combination or toggle modifiers below`,-1),P(`input`,{class:`shortcut-key-input w-full bg-gray/10 border border-base rounded-lg px-3 py-2.5 text-center text-sm font-mono outline-none focus:border-primary/40`,value:ie.value?U(t)(ie.value).join(` + `):``,placeholder:`Press keys...`,readonly:``,onKeydown:ce},null,40,Ds)]),P(`div`,Os,[P(`button`,{class:V([`px-3 py-1.5 rounded-md text-xs font-mono border transition-colors`,C.value?`bg-primary/15 border-primary/30 text-primary`:`border-base op50 hover:op80`]),onClick:n[1]||=e=>C.value=!C.value},q(U(u)?`⌘ Cmd`:`Ctrl`),3),U(u)?(G(),j(`button`,{key:0,class:V([`px-3 py-1.5 rounded-md text-xs font-mono border transition-colors`,w.value?`bg-primary/15 border-primary/30 text-primary`:`border-base op50 hover:op80`]),onClick:n[2]||=e=>w.value=!w.value},` \\u2303 Ctrl `,2)):A(``,!0),P(`button`,{class:V([`px-3 py-1.5 rounded-md text-xs font-mono border transition-colors`,T.value?`bg-primary/15 border-primary/30 text-primary`:`border-base op50 hover:op80`]),onClick:n[3]||=e=>T.value=!T.value},q(U(u)?`⌥ Alt`:`Alt`),3),P(`button`,{class:V([`px-3 py-1.5 rounded-md text-xs font-mono border transition-colors`,E.value?`bg-primary/15 border-primary/30 text-primary`:`border-base op50 hover:op80`]),onClick:n[4]||=e=>E.value=!E.value},q(U(u)?`⇧ Shift`:`Shift`),3)]),ae.value.length>0?(G(),j(`div`,ks,[(G(!0),j(H,null,L(ae.value,(e,t)=>(G(),j(`div`,{key:t,class:`flex items-center gap-1.5 text-xs text-amber`},[n[8]||=P(`div`,{class:`i-ph-warning-duotone w-3.5 h-3.5 shrink-0`},null,-1),M(` `+q(e),1)]))),128))])):A(``,!0),P(`div`,As,[P(`button`,{class:`px-3 py-1.5 rounded text-xs op60 hover:op100 hover:bg-gray/10 transition-colors`,onClick:re},` Cancel `),P(`button`,{class:V([`px-3 py-1.5 rounded text-xs transition-colors`,se.value?`bg-primary/15 text-primary hover:bg-primary/25`:`op30 cursor-not-allowed`]),disabled:!se.value,onClick:le},` OK `,10,js)])])],512)):A(``,!0)],64))}}),Ns={class:`h-full w-full overflow-hidden flex flex-col`},Ps={class:`flex items-center border-b border-base px-6 shrink-0`},Fs=[`onClick`],Is={key:0,class:`absolute bottom-0 left-2 right-2 h-0.5 bg-primary rounded-full`},Ls={class:`flex-1 overflow-auto p8`,style:{"scrollbar-gutter":`stable`}},Rs={class:`max-w-200 mx-auto`},zs=J({__name:`ViewBuiltinSettings`,props:{context:{},entry:{}},setup(e){let t=[{id:`appearance`,label:`Appearance`,icon:`i-ph-paint-brush-duotone`},{id:`docks`,label:`Docks`,icon:`i-ph-layout-duotone`},{id:`shortcuts`,label:`Shortcuts`,icon:`i-ph-keyboard-duotone`},{id:`advanced`,label:`Advanced`,icon:`i-ph-wrench-duotone`}],n=B(`appearance`);return(r,i)=>(G(),j(`div`,Ns,[P(`div`,Ps,[(G(),j(H,null,L(t,e=>P(`button`,{key:e.id,class:V([`flex items-center gap-1.5 px-4 py-3 text-sm transition-colors relative`,n.value===e.id?`op100 text-primary`:`op50 hover:op80`]),onClick:t=>n.value=e.id},[P(`div`,{class:V([e.icon,`w-4 h-4`])},null,2),M(` `+q(e.label)+` `,1),n.value===e.id?(G(),j(`div`,Is)):A(``,!0)],10,Fs)),64))]),P(`div`,Ls,[P(`div`,Rs,[n.value===`appearance`?(G(),F(Lo,{key:0,context:e.context},null,8,[`context`])):A(``,!0),n.value===`shortcuts`?(G(),F(Ms,{key:1,context:e.context},null,8,[`context`])):A(``,!0),n.value===`docks`?(G(),F(ds,{key:2,context:e.context},null,8,[`context`])):A(``,!0),n.value===`advanced`?(G(),F(bo,{key:3,context:e.context},null,8,[`context`])):A(``,!0)])])]))}}),Bs=J({__name:`ViewCustomRenderer`,props:{context:{},entry:{},panes:{},divStyle:{}},setup(e){let t=e,n=K(`viewFrame`);return Me(()=>{let e=t.panes.ensure(t.entry.id,{tagName:`div`}),r=e.element;r.style.boxShadow=`none`,r.style.outline=`none`,Object.assign(r.style,t.divStyle);let i=t.context.docks.getStateById(t.entry.id);i&&(i.domElements.panel=r),e.mount(n.value)}),xe(()=>{t.panes.get(t.entry.id)?.unmount()}),(e,t)=>(G(),j(`div`,{ref_key:`viewFrame`,ref:n,class:`devframes-view-custom-renderer w-full h-full flex items-center justify-center`},null,512))}}),Vs={class:`devframes-view-dock-renderer relative h-full w-full`},Hs={key:0,class:`absolute inset-0 flex flex-col items-center justify-center gap-2 p6 text-center`},Us={class:`text-sm color-muted`},Ws={key:1,class:`absolute inset-0 flex flex-col items-center justify-center gap-2 p6 text-center`},Gs={class:`text-sm color-muted`},Ks={key:0,class:`max-w-100 text-xs color-faint`},qs=J({__name:`ViewDockRenderer`,props:{context:{},entry:{}},setup(e){let t=e,n=K(`container`),r=B(`loading`),i=B(null),a,o=0;async function s(){let e=++o;if(a?.(),a=void 0,!t.context.renderers.has(t.entry.type)){r.value=`missing-renderer`;return}r.value=`loading`,i.value=null;let s=await t.context.renderers.mount(t.entry,n.value);if(e!==o){s.status===`mounted`&&s.dispose();return}s.status===`mounted`?(a=s.dispose,r.value=`mounted`):s.status===`missing-renderer`?r.value=`missing-renderer`:(r.value=`load-error`,i.value=String(s.error?.message??s.error))}return Me(()=>{s()}),Xe(()=>t.entry.id,()=>{s()}),xe(()=>{o++,a?.(),a=void 0}),(t,a)=>(G(),j(`div`,Vs,[Ze(P(`div`,{ref_key:`container`,ref:n,class:V([`h-full w-full`,U(Be)?`dark`:`light`])},null,2),[[k,r.value===`mounted`||r.value===`loading`]]),r.value===`missing-renderer`?(G(),j(`div`,Hs,[a[1]||=P(`div`,{class:`i-ph:puzzle-piece-duotone text-3xl color-faint`},null,-1),P(`div`,Us,` No renderer for “`+q(e.entry.type)+`” in the current environment `,1),a[2]||=P(`div`,{class:`text-xs color-faint`},` The host has not registered a renderer for this dock type. `,-1)])):r.value===`load-error`?(G(),j(`div`,Ws,[a[4]||=P(`div`,{class:`i-ph:warning-duotone text-3xl color-faint`},null,-1),P(`div`,Gs,` The renderer for “`+q(e.entry.type)+`” failed to load `,1),i.value?(G(),j(`div`,Ks,q(i.value),1)):A(``,!0),I(ma,{class:`mt-2`,onClick:a[0]||=e=>s()},{default:z(()=>[...a[3]||=[M(` Retry `,-1)]]),_:1})])):A(``,!0)]))}}),Js=[`top`,`right`,`bottom`,`left`],Ys=[`start`,`end`],Xs=Js.reduce((e,t)=>e.concat(t,t+`-`+Ys[0],t+`-`+Ys[1]),[]),Zs=Math.min,Qs=Math.max,$s={left:`right`,right:`left`,bottom:`top`,top:`bottom`};function ec(e,t,n){return Qs(e,Zs(t,n))}function tc(e,t){return typeof e==`function`?e(t):e}function nc(e){return e.split(`-`)[0]}function rc(e){return e.split(`-`)[1]}function ic(e){return e===`x`?`y`:`x`}function ac(e){return e===`y`?`height`:`width`}function oc(e){let t=e[0];return t===`t`||t===`b`?`y`:`x`}function sc(e){return ic(oc(e))}function cc(e,t,n){n===void 0&&(n=!1);let r=rc(e),i=sc(e),a=ac(i),o=i===`x`?r===(n?`end`:`start`)?`right`:`left`:r===`start`?`bottom`:`top`;return t.reference[a]>t.floating[a]&&(o=_c(o)),[o,_c(o)]}function lc(e){let t=_c(e);return[uc(e),t,uc(t)]}function uc(e){return e.includes(`start`)?e.replace(`start`,`end`):e.replace(`end`,`start`)}var dc=[`left`,`right`],fc=[`right`,`left`],pc=[`top`,`bottom`],mc=[`bottom`,`top`];function hc(e,t,n){switch(e){case`top`:case`bottom`:return n?t?fc:dc:t?dc:fc;case`left`:case`right`:return t?pc:mc;default:return[]}}function gc(e,t,n,r){let i=rc(e),a=hc(nc(e),n===`start`,r);return i&&(a=a.map(e=>e+`-`+i),t&&(a=a.concat(a.map(uc)))),a}function _c(e){let t=nc(e);return $s[t]+e.slice(t.length)}function vc(e){return{top:e.top??0,right:e.right??0,bottom:e.bottom??0,left:e.left??0}}function yc(e){return typeof e==`number`?{top:e,right:e,bottom:e,left:e}:vc(e)}function bc(e){let{x:t,y:n,width:r,height:i}=e;return{width:r,height:i,top:n,left:t,right:t+r,bottom:n+i,x:t,y:n}}function xc(e,t,n){let{reference:r,floating:i}=e,a=oc(t),o=sc(t),s=ac(o),c=nc(t),l=a===`y`,u=r.x+r.width/2-i.width/2,d=r.y+r.height/2-i.height/2,f=r[s]/2-i[s]/2,p;switch(c){case`top`:p={x:u,y:r.y-i.height};break;case`bottom`:p={x:u,y:r.y+r.height};break;case`right`:p={x:r.x+r.width,y:d};break;case`left`:p={x:r.x-i.width,y:d};break;default:p={x:r.x,y:r.y}}let m=rc(t);return m&&(p[o]+=f*(m===`end`?1:-1)*(n&&l?-1:1)),p}async function Sc(e,t){t===void 0&&(t={});let{x:n,y:r,platform:i,rects:a,elements:o,strategy:s}=e,{boundary:c=`clippingAncestors`,rootBoundary:l=`viewport`,elementContext:u=`floating`,altBoundary:d=!1,padding:f=0}=tc(t,e),p=yc(f),m=o[d?u===`floating`?`reference`:`floating`:u],h=bc(await i.getClippingRect({element:await(i.isElement==null?void 0:i.isElement(m))??!0?m:m.contextElement||await(i.getDocumentElement==null?void 0:i.getDocumentElement(o.floating)),boundary:c,rootBoundary:l,strategy:s})),g=u===`floating`?{x:n,y:r,width:a.floating.width,height:a.floating.height}:a.reference,_=await(i.getOffsetParent==null?void 0:i.getOffsetParent(o.floating)),v=await(i.isElement==null?void 0:i.isElement(_))&&await(i.getScale==null?void 0:i.getScale(_))||{x:1,y:1},y=bc(i.convertOffsetParentRelativeRectToViewportRelativeRect?await i.convertOffsetParentRelativeRectToViewportRelativeRect({elements:o,rect:g,offsetParent:_,strategy:s}):g);return{top:(h.top-y.top+p.top)/v.y,bottom:(y.bottom-h.bottom+p.bottom)/v.y,left:(h.left-y.left+p.left)/v.x,right:(y.right-h.right+p.right)/v.x}}var Cc=50,wc=async(e,t,n)=>{let{placement:r=`bottom`,strategy:i=`absolute`,middleware:a=[],platform:o}=n,s=o.detectOverflow?o:{...o,detectOverflow:Sc},c=await(o.isRTL==null?void 0:o.isRTL(t)),l=await o.getElementRects({reference:e,floating:t,strategy:i}),{x:u,y:d}=xc(l,r,c),f=r,p=0,m={};for(let n=0;n({name:`arrow`,options:e,async fn(t){let{x:n,y:r,placement:i,rects:a,platform:o,elements:s,middlewareData:c}=t,{element:l,padding:u=0}=tc(e,t)||{};if(l==null)return{};let d=yc(u),f={x:n,y:r},p=sc(i),m=ac(p),h=await o.getDimensions(l),g=p===`y`,_=g?`top`:`left`,v=g?`bottom`:`right`,y=g?`clientHeight`:`clientWidth`,b=a.reference[m]+a.reference[p]-f[p]-a.floating[m],x=f[p]-a.reference[p],S=await(o.getOffsetParent==null?void 0:o.getOffsetParent(l)),C=S?S[y]:0;(!C||!await(o.isElement==null?void 0:o.isElement(S)))&&(C=s.floating[y]||a.floating[m]);let w=b/2-x/2,T=C/2-h[m]/2-1,E=Zs(d[_],T),ee=Zs(d[v],T),te=C-h[m]-ee,D=C/2-h[m]/2+w,ne=ec(E,D,te),re=!c.arrow&&rc(i)!=null&&D!==ne&&a.reference[m]/2-(Drc(t)===e),...n.filter(t=>rc(t)!==e)]:n.filter(e=>nc(e)===e)).filter(n=>!e||rc(n)===e||(t?uc(n)!==n:!1))}var Dc=function(e){return e===void 0&&(e={}),{name:`autoPlacement`,options:e,async fn(t){let{rects:n,middlewareData:r,placement:i,platform:a,elements:o}=t,{crossAxis:s=!1,alignment:c,allowedPlacements:l=Xs,autoAlignment:u=!0,...d}=tc(e,t),f=c!==void 0||l===Xs?Ec(c||null,u,l):l,p=r.autoPlacement?.index||0,m=f[p];if(m==null)return{};if(i!==m)return{reset:{placement:f[0]}};let h=await a.detectOverflow(t,d),g=cc(m,n,await(a.isRTL==null?void 0:a.isRTL(o.floating))),_=[h[nc(m)],h[g[0]],h[g[1]]],v=[...r.autoPlacement?.overflows||[],{placement:m,overflows:_}],y=f[p+1];if(y)return{data:{index:p+1,overflows:v},reset:{placement:y}};let b=v.map(e=>{let t=rc(e.placement);return[e.placement,t&&s?e.overflows.slice(0,2).reduce((e,t)=>e+t,0):e.overflows[0],e.overflows]}).sort((e,t)=>e[1]-t[1]),x=b.filter(e=>e[2].slice(0,rc(e[0])?2:3).every(e=>e<=0))[0]?.[0]||b[0][0];return x===i?{}:{data:{index:p+1,overflows:v},reset:{placement:x}}}}},Oc=function(e){return e===void 0&&(e={}),{name:`flip`,options:e,async fn(t){var n;let{placement:r,middlewareData:i,rects:a,initialPlacement:o,platform:s,elements:c}=t,{mainAxis:l=!0,crossAxis:u=!0,fallbackPlacements:d,fallbackStrategy:f=`bestFit`,fallbackAxisSideDirection:p=`none`,flipAlignment:m=!0,...h}=tc(e,t);if((n=i.arrow)!=null&&n.alignmentOffset)return{};let g=nc(r),_=oc(o),v=nc(o)===o,y=await(s.isRTL==null?void 0:s.isRTL(c.floating)),b=d||(v||!m?[_c(o)]:lc(o)),x=p!==`none`;!d&&x&&b.push(...gc(o,m,p,y));let S=[o,...b],C=await s.detectOverflow(t,h),w=[],T=i.flip?.overflows||[];if(l&&w.push(C[g]),u){let e=cc(r,a,y);w.push(C[e[0]],C[e[1]])}if(T=[...T,{placement:r,overflows:w}],!w.every(e=>e<=0)){let e=(i.flip?.index||0)+1,t=S[e];if(t&&(u!==`alignment`||_===oc(t)||T.every(e=>oc(e.placement)!==_||e.overflows[0]>0)))return{data:{index:e,overflows:T},reset:{placement:t}};let n=T.filter(e=>e.overflows[0]<=0).sort((e,t)=>e.overflows[1]-t.overflows[1])[0]?.placement;if(!n)switch(f){case`bestFit`:{let e=T.filter(e=>{if(x){let t=oc(e.placement);return t===_||t===`y`}return!0}).map(e=>[e.placement,e.overflows.filter(e=>e>0).reduce((e,t)=>e+t,0)]).sort((e,t)=>e[1]-t[1])[0]?.[0];e&&(n=e);break}case`initialPlacement`:n=o}if(r!==n)return{reset:{placement:n}}}return{}}}},kc=new Set([`left`,`top`]);async function Ac(e,t){let{placement:n,platform:r,elements:i}=e,a=await(r.isRTL==null?void 0:r.isRTL(i.floating)),o=nc(n),s=rc(n),c=oc(n)===`y`,l=kc.has(o)?-1:1,u=a&&c?-1:1,d=tc(t,e),{mainAxis:f,crossAxis:p,alignmentAxis:m}=typeof d==`number`?{mainAxis:d,crossAxis:0,alignmentAxis:null}:{mainAxis:d.mainAxis||0,crossAxis:d.crossAxis||0,alignmentAxis:d.alignmentAxis};return s&&typeof m==`number`&&(p=s===`end`?m*-1:m),c?{x:p*u,y:f*l}:{x:f*l,y:p*u}}var jc=function(e){return e===void 0&&(e=0),{name:`offset`,options:e,async fn(t){var n;let{x:r,y:i,placement:a,middlewareData:o}=t,s=await Ac(t,e);return a===o.offset?.placement&&(n=o.arrow)!=null&&n.alignmentOffset?{}:{x:r+s.x,y:i+s.y,data:{...s,placement:a}}}}},Mc=function(e){return e===void 0&&(e={}),{name:`shift`,options:e,async fn(t){let{x:n,y:r,placement:i,platform:a}=t,{mainAxis:o=!0,crossAxis:s=!1,limiter:c={fn:e=>{let{x:t,y:n}=e;return{x:t,y:n}}},...l}=tc(e,t),u={x:n,y:r},d=await a.detectOverflow(t,l),f=oc(i),p=ic(f),m=u[p],h=u[f],g=(e,t)=>ec(t+d[e===`y`?`top`:`left`],t,t-d[e===`y`?`bottom`:`right`]);o&&(m=g(p,m)),s&&(h=g(f,h));let _=c.fn({...t,[p]:m,[f]:h});return{..._,data:{x:_.x-n,y:_.y-r,enabled:{[p]:o,[f]:s}}}}}},Nc=function(e){return e===void 0&&(e={}),{name:`size`,options:e,async fn(t){let{placement:n,rects:r,platform:i,elements:a}=t,{apply:o=()=>{},...s}=tc(e,t),c=await i.detectOverflow(t,s),l=nc(n),u=rc(n),d=oc(n)===`y`,{width:f,height:p}=r.floating,m,h;l===`top`||l===`bottom`?(m=l,h=u===(await(i.isRTL==null?void 0:i.isRTL(a.floating))?`start`:`end`)?`left`:`right`):(h=l,m=u===`end`?`top`:`bottom`);let g=p-c.top-c.bottom,_=f-c.left-c.right,v=Zs(p-c[m],g),y=Zs(f-c[h],_),b=t.middlewareData.shift,x=!b,S=v,C=y;b!=null&&b.enabled.x&&(C=_),b!=null&&b.enabled.y&&(S=g),x&&!u&&(d?C=f-2*Qs(c.left,c.right):S=p-2*Qs(c.top,c.bottom)),await o({...t,availableWidth:C,availableHeight:S});let w=await i.getDimensions(a.floating);return f!==w.width||p!==w.height?{reset:{rects:!0}}:{}}}};function Pc(e){return e.ownerDocument?.defaultView||window}function Fc(e){return Pc(e).getComputedStyle(e)}var Ic=Math.min,Lc=Math.max,Rc=Math.round;function zc(e){let t=Fc(e),n=parseFloat(t.width),r=parseFloat(t.height),i=e.offsetWidth,a=e.offsetHeight,o=Rc(n)!==i||Rc(r)!==a;return o&&(n=i,r=a),{width:n,height:r,fallback:o}}function Bc(e){return Gc(e)?(e.nodeName||``).toLowerCase():``}var Vc;function Hc(){if(Vc)return Vc;let e=navigator.userAgentData;return e&&Array.isArray(e.brands)?(Vc=e.brands.map((e=>e.brand+`/`+e.version)).join(` `),Vc):navigator.userAgent}function Uc(e){return e instanceof Pc(e).HTMLElement}function Wc(e){return e instanceof Pc(e).Element}function Gc(e){return e instanceof Pc(e).Node}function Kc(e){return typeof ShadowRoot>`u`?!1:e instanceof Pc(e).ShadowRoot||e instanceof ShadowRoot}function qc(e){let{overflow:t,overflowX:n,overflowY:r,display:i}=Fc(e);return/auto|scroll|overlay|hidden|clip/.test(t+r+n)&&![`inline`,`contents`].includes(i)}function Jc(e){return[`table`,`td`,`th`].includes(Bc(e))}function Yc(e){let t=/firefox/i.test(Hc()),n=Fc(e),r=n.backdropFilter||n.WebkitBackdropFilter;return n.transform!==`none`||n.perspective!==`none`||!!r&&r!==`none`||t&&n.willChange===`filter`||t&&!!n.filter&&n.filter!==`none`||[`transform`,`perspective`].some((e=>n.willChange.includes(e)))||[`paint`,`layout`,`strict`,`content`].some((e=>{let t=n.contain;return t!=null&&t.includes(e)}))}function Xc(){return!/^((?!chrome|android).)*safari/i.test(Hc())}function Zc(e){return[`html`,`body`,`#document`].includes(Bc(e))}function Qc(e){return Wc(e)?e:e.contextElement}var $c={x:1,y:1};function el(e){let t=Qc(e);if(!Uc(t))return $c;let n=t.getBoundingClientRect(),{width:r,height:i,fallback:a}=zc(t),o=(a?Rc(n.width):n.width)/r,s=(a?Rc(n.height):n.height)/i;return o&&Number.isFinite(o)||(o=1),s&&Number.isFinite(s)||(s=1),{x:o,y:s}}function tl(e,t,n,r){t===void 0&&(t=!1),n===void 0&&(n=!1);let i=e.getBoundingClientRect(),a=Qc(e),o=$c;t&&(r?Wc(r)&&(o=el(r)):o=el(e));let s=a?Pc(a):window,c=!Xc()&&n,l=(i.left+(c&&s.visualViewport?.offsetLeft||0))/o.x,u=(i.top+(c&&s.visualViewport?.offsetTop||0))/o.y,d=i.width/o.x,f=i.height/o.y;if(a){let e=Pc(a),t=r&&Wc(r)?Pc(r):r,n=e.frameElement;for(;n&&r&&t!==e;){let e=el(n),t=n.getBoundingClientRect(),r=getComputedStyle(n);t.x+=(n.clientLeft+parseFloat(r.paddingLeft))*e.x,t.y+=(n.clientTop+parseFloat(r.paddingTop))*e.y,l*=e.x,u*=e.y,d*=e.x,f*=e.y,l+=t.x,u+=t.y,n=Pc(n).frameElement}}return{width:d,height:f,top:u,right:l+d,bottom:u+f,left:l,x:l,y:u}}function nl(e){return((Gc(e)?e.ownerDocument:e.document)||window.document).documentElement}function rl(e){return Wc(e)?{scrollLeft:e.scrollLeft,scrollTop:e.scrollTop}:{scrollLeft:e.pageXOffset,scrollTop:e.pageYOffset}}function il(e){return tl(nl(e)).left+rl(e).scrollLeft}function al(e){if(Bc(e)===`html`)return e;let t=e.assignedSlot||e.parentNode||Kc(e)&&e.host||nl(e);return Kc(t)?t.host:t}function ol(e){let t=al(e);return Zc(t)?t.ownerDocument.body:Uc(t)&&qc(t)?t:ol(t)}function sl(e,t){t===void 0&&(t=[]);let n=ol(e),r=n===e.ownerDocument?.body,i=Pc(n);return r?t.concat(i,i.visualViewport||[],qc(n)?n:[]):t.concat(n,sl(n))}function cl(e,t,n){return t===`viewport`?bc(function(e,t){let n=Pc(e),r=nl(e),i=n.visualViewport,a=r.clientWidth,o=r.clientHeight,s=0,c=0;if(i){a=i.width,o=i.height;let e=Xc();(e||!e&&t===`fixed`)&&(s=i.offsetLeft,c=i.offsetTop)}return{width:a,height:o,x:s,y:c}}(e,n)):Wc(t)?bc(function(e,t){let n=tl(e,!0,t===`fixed`),r=n.top+e.clientTop,i=n.left+e.clientLeft,a=Uc(e)?el(e):{x:1,y:1};return{width:e.clientWidth*a.x,height:e.clientHeight*a.y,x:i*a.x,y:r*a.y}}(t,n)):bc(function(e){let t=nl(e),n=rl(e),r=e.ownerDocument.body,i=Lc(t.scrollWidth,t.clientWidth,r.scrollWidth,r.clientWidth),a=Lc(t.scrollHeight,t.clientHeight,r.scrollHeight,r.clientHeight),o=-n.scrollLeft+il(e),s=-n.scrollTop;return Fc(r).direction===`rtl`&&(o+=Lc(t.clientWidth,r.clientWidth)-i),{width:i,height:a,x:o,y:s}}(nl(e)))}function ll(e){return Uc(e)&&Fc(e).position!==`fixed`?e.offsetParent:null}function ul(e){let t=Pc(e),n=ll(e);for(;n&&Jc(n)&&Fc(n).position===`static`;)n=ll(n);return n&&(Bc(n)===`html`||Bc(n)===`body`&&Fc(n).position===`static`&&!Yc(n))?t:n||function(e){let t=al(e);for(;Uc(t)&&!Zc(t);){if(Yc(t))return t;t=al(t)}return null}(e)||t}function dl(e,t,n){let r=Uc(t),i=nl(t),a=tl(e,!0,n===`fixed`,t),o={scrollLeft:0,scrollTop:0},s={x:0,y:0};if(r||!r&&n!==`fixed`){if((Bc(t)!==`body`||qc(i))&&(o=rl(t)),Uc(t)){let e=tl(t,!0);s.x=e.x+t.clientLeft,s.y=e.y+t.clientTop}else i&&(s.x=il(i))}return{x:a.left+o.scrollLeft-s.x,y:a.top+o.scrollTop-s.y,width:a.width,height:a.height}}var fl={getClippingRect:function(e){let{element:t,boundary:n,rootBoundary:r,strategy:i}=e,a=[...n===`clippingAncestors`?function(e,t){let n=t.get(e);if(n)return n;let r=sl(e).filter((e=>Wc(e)&&Bc(e)!==`body`)),i=null,a=Fc(e).position===`fixed`,o=a?al(e):e;for(;Wc(o)&&!Zc(o);){let e=Fc(o),t=Yc(o);(a?t||i:t||e.position!==`static`||!i||![`absolute`,`fixed`].includes(i.position))?i=e:r=r.filter((e=>e!==o)),o=al(o)}return t.set(e,r),r}(t,this._c):[].concat(n),r],o=a[0],s=a.reduce(((e,n)=>{let r=cl(t,n,i);return e.top=Lc(r.top,e.top),e.right=Ic(r.right,e.right),e.bottom=Ic(r.bottom,e.bottom),e.left=Lc(r.left,e.left),e}),cl(t,o,i));return{width:s.right-s.left,height:s.bottom-s.top,x:s.left,y:s.top}},convertOffsetParentRelativeRectToViewportRelativeRect:function(e){let{rect:t,offsetParent:n,strategy:r}=e,i=Uc(n),a=nl(n);if(n===a)return t;let o={scrollLeft:0,scrollTop:0},s={x:1,y:1},c={x:0,y:0};if((i||!i&&r!==`fixed`)&&((Bc(n)!==`body`||qc(a))&&(o=rl(n)),Uc(n))){let e=tl(n);s=el(n),c.x=e.x+n.clientLeft,c.y=e.y+n.clientTop}return{width:t.width*s.x,height:t.height*s.y,x:t.x*s.x-o.scrollLeft*s.x+c.x,y:t.y*s.y-o.scrollTop*s.y+c.y}},isElement:Wc,getDimensions:function(e){return Uc(e)?zc(e):e.getBoundingClientRect()},getOffsetParent:ul,getDocumentElement:nl,getScale:el,async getElementRects(e){let{reference:t,floating:n,strategy:r}=e,i=this.getOffsetParent||ul,a=this.getDimensions;return{reference:dl(t,await i(n),r),floating:{x:0,y:0,...await a(n)}}},getClientRects:e=>Array.from(e.getClientRects()),isRTL:e=>Fc(e).direction===`rtl`},pl=(e,t,n)=>{let r=new Map,i={platform:fl,...n},a={...i.platform,_c:r};return wc(e,t,{...i,platform:a})},ml={disabled:!1,distance:5,skidding:0,container:`body`,boundary:void 0,instantMove:!1,disposeTimeout:150,popperTriggers:[],strategy:`absolute`,preventOverflow:!0,flip:!0,shift:!0,overflowPadding:0,arrowPadding:0,arrowOverflow:!0,autoHideOnMousedown:!1,themes:{tooltip:{placement:`top`,triggers:[`hover`,`focus`,`touch`],hideTriggers:e=>[...e,`click`],delay:{show:200,hide:0},handleResize:!1,html:!1,loadingContent:`...`},dropdown:{placement:`bottom`,triggers:[`click`],delay:0,handleResize:!0,autoHide:!0},menu:{$extend:`dropdown`,triggers:[`hover`,`focus`],popperTriggers:[`hover`],delay:{show:0,hide:400}}}};function hl(e,t){let n=ml.themes[e]||{},r;do r=n[t],typeof r>`u`?n.$extend?n=ml.themes[n.$extend]||{}:(n=null,r=ml[t]):n=null;while(n);return r}function gl(e){let t=[e],n=ml.themes[e]||{};do n.$extend&&!n.$resetCss?(t.push(n.$extend),n=ml.themes[n.$extend]||{}):n=null;while(n);return t.map(e=>`v-popper--theme-${e}`)}function _l(e){let t=[e],n=ml.themes[e]||{};do n.$extend?(t.push(n.$extend),n=ml.themes[n.$extend]||{}):n=null;while(n);return t}var vl=!1;if(typeof window<`u`){vl=!1;try{let e=Object.defineProperty({},"passive",{get(){vl=!0}});window.addEventListener(`test`,null,e)}catch{}}var yl=!1;typeof window<`u`&&typeof navigator<`u`&&(yl=/iPad|iPhone|iPod/.test(navigator.userAgent)&&!window.MSStream);var bl=[`auto`,`top`,`bottom`,`left`,`right`].reduce((e,t)=>e.concat([t,`${t}-start`,`${t}-end`]),[]),xl={hover:`mouseenter`,focus:`focus`,click:`click`,touch:`touchstart`,pointer:`pointerdown`},Sl={hover:`mouseleave`,focus:`blur`,click:`click`,touch:`touchend`,pointer:`pointerup`};function Cl(e,t){let n=e.indexOf(t);n!==-1&&e.splice(n,1)}function wl(){return new Promise(e=>requestAnimationFrame(()=>{requestAnimationFrame(e)}))}var Tl=[],El=null,Dl={};function Ol(e){let t=Dl[e];return t||=Dl[e]=[],t}var kl=function(){};typeof window<`u`&&(kl=window.Element);function $(e){return function(t){return hl(t.theme,e)}}var Al=`__floating-vue__popper`,jl=()=>J({name:`VPopper`,provide(){return{[Al]:{parentPopper:this}}},inject:{[Al]:{default:null}},props:{theme:{type:String,required:!0},targetNodes:{type:Function,required:!0},referenceNode:{type:Function,default:null},popperNode:{type:Function,required:!0},shown:{type:Boolean,default:!1},showGroup:{type:String,default:null},ariaId:{default:null},disabled:{type:Boolean,default:$(`disabled`)},positioningDisabled:{type:Boolean,default:$(`positioningDisabled`)},placement:{type:String,default:$(`placement`),validator:e=>bl.includes(e)},delay:{type:[String,Number,Object],default:$(`delay`)},distance:{type:[Number,String],default:$(`distance`)},skidding:{type:[Number,String],default:$(`skidding`)},triggers:{type:Array,default:$(`triggers`)},showTriggers:{type:[Array,Function],default:$(`showTriggers`)},hideTriggers:{type:[Array,Function],default:$(`hideTriggers`)},popperTriggers:{type:Array,default:$(`popperTriggers`)},popperShowTriggers:{type:[Array,Function],default:$(`popperShowTriggers`)},popperHideTriggers:{type:[Array,Function],default:$(`popperHideTriggers`)},container:{type:[String,Object,kl,Boolean],default:$(`container`)},boundary:{type:[String,kl],default:$(`boundary`)},strategy:{type:String,validator:e=>[`absolute`,`fixed`].includes(e),default:$(`strategy`)},autoHide:{type:[Boolean,Function],default:$(`autoHide`)},handleResize:{type:Boolean,default:$(`handleResize`)},instantMove:{type:Boolean,default:$(`instantMove`)},eagerMount:{type:Boolean,default:$(`eagerMount`)},popperClass:{type:[String,Array,Object],default:$(`popperClass`)},computeTransformOrigin:{type:Boolean,default:$(`computeTransformOrigin`)},autoMinSize:{type:Boolean,default:$(`autoMinSize`)},autoSize:{type:[Boolean,String],default:$(`autoSize`)},autoMaxSize:{type:Boolean,default:$(`autoMaxSize`)},autoBoundaryMaxSize:{type:Boolean,default:$(`autoBoundaryMaxSize`)},preventOverflow:{type:Boolean,default:$(`preventOverflow`)},overflowPadding:{type:[Number,String],default:$(`overflowPadding`)},arrowPadding:{type:[Number,String],default:$(`arrowPadding`)},arrowOverflow:{type:Boolean,default:$(`arrowOverflow`)},flip:{type:Boolean,default:$(`flip`)},shift:{type:Boolean,default:$(`shift`)},shiftCrossAxis:{type:Boolean,default:$(`shiftCrossAxis`)},noAutoFocus:{type:Boolean,default:$(`noAutoFocus`)},disposeTimeout:{type:Number,default:$(`disposeTimeout`)}},emits:{show:()=>!0,hide:()=>!0,"update:shown":e=>!0,"apply-show":()=>!0,"apply-hide":()=>!0,"close-group":()=>!0,"close-directive":()=>!0,"auto-hide":()=>!0,resize:()=>!0},data(){return{isShown:!1,isMounted:!1,skipTransition:!1,classes:{showFrom:!1,showTo:!1,hideFrom:!1,hideTo:!0},result:{x:0,y:0,placement:``,strategy:this.strategy,arrow:{x:0,y:0,centerOffset:0},transformOrigin:null},randomId:`popper_${[Math.random(),Date.now()].map(e=>e.toString(36).substring(2,10)).join(`_`)}`,shownChildren:new Set,lastAutoHide:!0,pendingHide:!1,containsGlobalTarget:!1,isDisposed:!0,mouseDownContains:!1}},computed:{popperId(){return this.ariaId==null?this.randomId:this.ariaId},shouldMountContent(){return this.eagerMount||this.isMounted},slotData(){return{popperId:this.popperId,isShown:this.isShown,shouldMountContent:this.shouldMountContent,skipTransition:this.skipTransition,autoHide:typeof this.autoHide==`function`?this.lastAutoHide:this.autoHide,show:this.show,hide:this.hide,handleResize:this.handleResize,onResize:this.onResize,classes:{...this.classes,popperClass:this.popperClass},result:this.positioningDisabled?null:this.result,attrs:this.$attrs}},parentPopper(){return this[Al]?.parentPopper},hasPopperShowTriggerHover(){return this.popperTriggers?.includes(`hover`)||this.popperShowTriggers?.includes(`hover`)}},watch:{shown:`$_autoShowHide`,disabled(e){e?this.dispose():this.init()},async container(){this.isShown&&(this.$_ensureTeleport(),await this.$_computePosition())},triggers:{handler:`$_refreshListeners`,deep:!0},positioningDisabled:`$_refreshListeners`,...[`placement`,`distance`,`skidding`,`boundary`,`strategy`,`overflowPadding`,`arrowPadding`,`preventOverflow`,`shift`,`shiftCrossAxis`,`flip`].reduce((e,t)=>(e[t]=`$_computePosition`,e),{})},created(){this.autoMinSize&&console.warn('[floating-vue] `autoMinSize` option is deprecated. Use `autoSize="min"` instead.'),this.autoMaxSize&&console.warn("[floating-vue] `autoMaxSize` option is deprecated. Use `autoBoundaryMaxSize` instead.")},mounted(){this.init(),this.$_detachPopperNode()},activated(){this.$_autoShowHide()},deactivated(){this.hide()},beforeUnmount(){this.dispose()},methods:{show({event:e=null,skipDelay:t=!1,force:n=!1}={}){var r;(r=this.parentPopper)!=null&&r.lockedChild&&this.parentPopper.lockedChild!==this||(this.pendingHide=!1,(n||!this.disabled)&&(this.parentPopper?.lockedChild===this&&(this.parentPopper.lockedChild=null),this.$_scheduleShow(e,t),this.$emit(`show`),this.$_showFrameLocked=!0,requestAnimationFrame(()=>{this.$_showFrameLocked=!1})),this.$emit(`update:shown`,!0))},hide({event:e=null,skipDelay:t=!1}={}){if(!this.$_hideInProgress){if(this.shownChildren.size>0){this.pendingHide=!0;return}if(this.hasPopperShowTriggerHover&&this.$_isAimingPopper()){this.parentPopper&&(this.parentPopper.lockedChild=this,clearTimeout(this.parentPopper.lockedChildTimer),this.parentPopper.lockedChildTimer=setTimeout(()=>{this.parentPopper.lockedChild===this&&(this.parentPopper.lockedChild.hide({skipDelay:t}),this.parentPopper.lockedChild=null)},1e3));return}this.parentPopper?.lockedChild===this&&(this.parentPopper.lockedChild=null),this.pendingHide=!1,this.$_scheduleHide(e,t),this.$emit(`hide`),this.$emit(`update:shown`,!1)}},init(){this.isDisposed&&(this.isDisposed=!1,this.isMounted=!1,this.$_events=[],this.$_preventShow=!1,this.$_referenceNode=this.referenceNode?.call(this)??this.$el,this.$_targetNodes=this.targetNodes().filter(e=>e.nodeType===e.ELEMENT_NODE),this.$_popperNode=this.popperNode(),this.$_innerNode=this.$_popperNode.querySelector(`.v-popper__inner`),this.$_arrowNode=this.$_popperNode.querySelector(`.v-popper__arrow-container`),this.$_swapTargetAttrs(`title`,`data-original-title`),this.$_detachPopperNode(),this.triggers.length&&this.$_addEventListeners(),this.shown&&this.show())},dispose(){this.isDisposed||(this.isDisposed=!0,this.$_removeEventListeners(),this.hide({skipDelay:!0}),this.$_detachPopperNode(),this.isMounted=!1,this.isShown=!1,this.$_updateParentShownChildren(!1),this.$_swapTargetAttrs(`data-original-title`,`title`))},async onResize(){this.isShown&&(await this.$_computePosition(),this.$emit(`resize`))},async $_computePosition(){if(this.isDisposed||this.positioningDisabled)return;let e={strategy:this.strategy,middleware:[]};(this.distance||this.skidding)&&e.middleware.push(jc({mainAxis:this.distance,crossAxis:this.skidding}));let t=this.placement.startsWith(`auto`);if(t?e.middleware.push(Dc({alignment:this.placement.split(`-`)[1]??``})):e.placement=this.placement,this.preventOverflow&&(this.shift&&e.middleware.push(Mc({padding:this.overflowPadding,boundary:this.boundary,crossAxis:this.shiftCrossAxis})),!t&&this.flip&&e.middleware.push(Oc({padding:this.overflowPadding,boundary:this.boundary}))),e.middleware.push(Tc({element:this.$_arrowNode,padding:this.arrowPadding})),this.arrowOverflow&&e.middleware.push({name:`arrowOverflow`,fn:({placement:e,rects:t,middlewareData:n})=>{let r,{centerOffset:i}=n.arrow;return r=e.startsWith(`top`)||e.startsWith(`bottom`)?Math.abs(i)>t.reference.width/2:Math.abs(i)>t.reference.height/2,{data:{overflow:r}}}}),this.autoMinSize||this.autoSize){let t=this.autoSize?this.autoSize:this.autoMinSize?`min`:null;e.middleware.push({name:`autoSize`,fn:({rects:e,placement:n,middlewareData:r})=>{var i;if((i=r.autoSize)!=null&&i.skip)return{};let a,o;return n.startsWith(`top`)||n.startsWith(`bottom`)?a=e.reference.width:o=e.reference.height,this.$_innerNode.style[t===`min`?`minWidth`:t===`max`?`maxWidth`:`width`]=a==null?null:`${a}px`,this.$_innerNode.style[t===`min`?`minHeight`:t===`max`?`maxHeight`:`height`]=o==null?null:`${o}px`,{data:{skip:!0},reset:{rects:!0}}}})}(this.autoMaxSize||this.autoBoundaryMaxSize)&&(this.$_innerNode.style.maxWidth=null,this.$_innerNode.style.maxHeight=null,e.middleware.push(Nc({boundary:this.boundary,padding:this.overflowPadding,apply:({availableWidth:e,availableHeight:t})=>{this.$_innerNode.style.maxWidth=e==null?null:`${e}px`,this.$_innerNode.style.maxHeight=t==null?null:`${t}px`}})));let n=await pl(this.$_referenceNode,this.$_popperNode,e);Object.assign(this.result,{x:n.x,y:n.y,placement:n.placement,strategy:n.strategy,arrow:{...n.middlewareData.arrow,...n.middlewareData.arrowOverflow}})},$_scheduleShow(e,t=!1){if(this.$_updateParentShownChildren(!0),this.$_hideInProgress=!1,clearTimeout(this.$_scheduleTimer),El&&this.instantMove&&El.instantMove&&El!==this.parentPopper){El.$_applyHide(!0),this.$_applyShow(!0);return}t?this.$_applyShow():this.$_scheduleTimer=setTimeout(this.$_applyShow.bind(this),this.$_computeDelay(`show`))},$_scheduleHide(e,t=!1){if(this.shownChildren.size>0){this.pendingHide=!0;return}this.$_updateParentShownChildren(!1),this.$_hideInProgress=!0,clearTimeout(this.$_scheduleTimer),this.isShown&&(El=this),t?this.$_applyHide():this.$_scheduleTimer=setTimeout(this.$_applyHide.bind(this),this.$_computeDelay(`hide`))},$_computeDelay(e){let t=this.delay;return parseInt(t&&t[e]||t||0)},async $_applyShow(e=!1){clearTimeout(this.$_disposeTimer),clearTimeout(this.$_scheduleTimer),this.skipTransition=e,!this.isShown&&(this.$_ensureTeleport(),await wl(),await this.$_computePosition(),await this.$_applyShowEffect(),this.positioningDisabled||this.$_registerEventListeners([...sl(this.$_referenceNode),...sl(this.$_popperNode)],`scroll`,()=>{this.$_computePosition()}))},async $_applyShowEffect(){if(this.$_hideInProgress)return;if(this.computeTransformOrigin){let e=this.$_referenceNode.getBoundingClientRect(),t=this.$_popperNode.querySelector(`.v-popper__wrapper`),n=t.parentNode.getBoundingClientRect(),r=e.x+e.width/2-(n.left+t.offsetLeft),i=e.y+e.height/2-(n.top+t.offsetTop);this.result.transformOrigin=`${r}px ${i}px`}this.isShown=!0,this.$_applyAttrsToTarget({"aria-describedby":this.popperId,"data-popper-shown":``});let e=this.showGroup;if(e){let t;for(let n=0;n0){this.pendingHide=!0,this.$_hideInProgress=!1;return}if(clearTimeout(this.$_scheduleTimer),!this.isShown)return;this.skipTransition=e,Cl(Tl,this),Tl.length===0&&document.body.classList.remove(`v-popper--some-open`);for(let e of _l(this.theme)){let t=Ol(e);Cl(t,this),t.length===0&&document.body.classList.remove(`v-popper--some-open--${e}`)}El===this&&(El=null),this.isShown=!1,this.$_applyAttrsToTarget({"aria-describedby":void 0,"data-popper-shown":void 0}),clearTimeout(this.$_disposeTimer);let t=this.disposeTimeout;t!==null&&(this.$_disposeTimer=setTimeout(()=>{this.$_popperNode&&(this.$_detachPopperNode(),this.isMounted=!1)},t)),this.$_removeEventListeners(`scroll`),this.$emit(`apply-hide`),this.classes.showFrom=!1,this.classes.showTo=!1,this.classes.hideFrom=!0,this.classes.hideTo=!1,await wl(),this.classes.hideFrom=!1,this.classes.hideTo=!0},$_autoShowHide(){this.shown?this.show():this.hide()},$_ensureTeleport(){if(this.isDisposed)return;let e=this.container;if(typeof e==`string`?e=window.document.querySelector(e):e===!1&&(e=this.$_targetNodes[0].parentNode),!e)throw Error(`No container for popover: `+this.container);e.appendChild(this.$_popperNode),this.isMounted=!0},$_addEventListeners(){let e=e=>{this.isShown&&!this.$_hideInProgress||(e.usedByTooltip=!0,!this.$_preventShow&&this.show({event:e}))};this.$_registerTriggerListeners(this.$_targetNodes,xl,this.triggers,this.showTriggers,e),this.$_registerTriggerListeners([this.$_popperNode],xl,this.popperTriggers,this.popperShowTriggers,e);let t=e=>{e.usedByTooltip||this.hide({event:e})};this.$_registerTriggerListeners(this.$_targetNodes,Sl,this.triggers,this.hideTriggers,t),this.$_registerTriggerListeners([this.$_popperNode],Sl,this.popperTriggers,this.popperHideTriggers,t)},$_registerEventListeners(e,t,n){this.$_events.push({targetNodes:e,eventType:t,handler:n}),e.forEach(e=>e.addEventListener(t,n,vl?{passive:!0}:void 0))},$_registerTriggerListeners(e,t,n,r,i){let a=n;r!=null&&(a=typeof r==`function`?r(a):r),a.forEach(n=>{let r=t[n];r&&this.$_registerEventListeners(e,r,i)})},$_removeEventListeners(e){let t=[];this.$_events.forEach(n=>{let{targetNodes:r,eventType:i,handler:a}=n;!e||e===i?r.forEach(e=>e.removeEventListener(i,a)):t.push(n)}),this.$_events=t},$_refreshListeners(){this.isDisposed||(this.$_removeEventListeners(),this.$_addEventListeners())},$_handleGlobalClose(e,t=!1){this.$_showFrameLocked||(this.hide({event:e}),e.closePopover?this.$emit(`close-directive`):this.$emit(`auto-hide`),t&&(this.$_preventShow=!0,setTimeout(()=>{this.$_preventShow=!1},300)))},$_detachPopperNode(){this.$_popperNode.parentNode&&this.$_popperNode.parentNode.removeChild(this.$_popperNode)},$_swapTargetAttrs(e,t){for(let n of this.$_targetNodes){let r=n.getAttribute(e);r&&(n.removeAttribute(e),n.setAttribute(t,r))}},$_applyAttrsToTarget(e){for(let t of this.$_targetNodes)for(let n in e){let r=e[n];r==null?t.removeAttribute(n):t.setAttribute(n,r)}},$_updateParentShownChildren(e){let t=this.parentPopper;for(;t;)e?t.shownChildren.add(this.randomId):(t.shownChildren.delete(this.randomId),t.pendingHide&&t.hide()),t=t.parentPopper},$_isAimingPopper(){let e=this.$_referenceNode.getBoundingClientRect();if(Bl>=e.left&&Bl<=e.right&&Vl>=e.top&&Vl<=e.bottom){let e=this.$_popperNode.getBoundingClientRect(),t=Bl-Rl,n=Vl-zl,r=e.left+e.width/2-Rl+(e.top+e.height/2)-zl+e.width+e.height,i=Rl+t*r,a=zl+n*r;return Hl(Rl,zl,i,a,e.left,e.top,e.left,e.bottom)||Hl(Rl,zl,i,a,e.left,e.top,e.right,e.top)||Hl(Rl,zl,i,a,e.right,e.top,e.right,e.bottom)||Hl(Rl,zl,i,a,e.left,e.bottom,e.right,e.bottom)}return!1}},render(){return this.$slots.default(this.slotData)}});if(typeof document<`u`&&typeof window<`u`){if(yl){let e=!vl||{passive:!0,capture:!0};document.addEventListener(`touchstart`,e=>Ml(e,!0),e),document.addEventListener(`touchend`,e=>Nl(e,!0),e)}else window.addEventListener(`mousedown`,e=>Ml(e,!1),!0),window.addEventListener(`click`,e=>Nl(e,!1),!0);window.addEventListener(`resize`,Ll)}function Ml(e,t){if(ml.autoHideOnMousedown)Pl(e,t);else for(let t=0;t=0;r--){let i=Tl[r];try{let r=i.containsGlobalTarget=i.mouseDownContains||i.popperNode().contains(e.target);i.pendingHide=!1,requestAnimationFrame(()=>{if(i.pendingHide=!1,!n[i.randomId]&&Fl(i,r,e)){if(i.$_handleGlobalClose(e,t),!e.closeAllPopover&&e.closePopover&&r){let e=i.parentPopper;for(;e;)n[e.randomId]=!0,e=e.parentPopper;return}let a=i.parentPopper;for(;a&&Fl(a,a.containsGlobalTarget,e);)a.$_handleGlobalClose(e,t),a=a.parentPopper}})}catch{}}}function Fl(e,t,n){return n.closeAllPopover||n.closePopover&&t||Il(e,n)&&!t}function Il(e,t){if(typeof e.autoHide==`function`){let n=e.autoHide(t);return e.lastAutoHide=n,n}return e.autoHide}function Ll(){for(let e=0;e{Rl=Bl,zl=Vl,Bl=e.clientX,Vl=e.clientY},vl?{passive:!0}:void 0);function Hl(e,t,n,r,i,a,o,s){let c=((o-i)*(t-a)-(s-a)*(e-i))/((s-a)*(n-e)-(o-i)*(r-t)),l=((n-e)*(t-a)-(r-t)*(e-i))/((s-a)*(n-e)-(o-i)*(r-t));return c>=0&&c<=1&&l>=0&&l<=1}var Ul={extends:jl()},Wl=(e,t)=>{let n=e.__vccOpts||e;for(let[e,r]of t)n[e]=r;return n};function Gl(e,t,n,r,i,a){return G(),j(`div`,{ref:`reference`,class:V([`v-popper`,{"v-popper--shown":e.slotData.isShown}])},[Pe(e.$slots,`default`,Ue(_e(e.slotData)))],2)}var Kl=Wl(Ul,[[`render`,Gl]]);function ql(){var e=window.navigator.userAgent,t=e.indexOf(`MSIE `);if(t>0)return parseInt(e.substring(t+5,e.indexOf(`.`,t)),10);if(e.indexOf(`Trident/`)>0){var n=e.indexOf(`rv:`);return parseInt(e.substring(n+3,e.indexOf(`.`,n)),10)}var r=e.indexOf(`Edge/`);return r>0?parseInt(e.substring(r+5,e.indexOf(`.`,r)),10):-1}var Jl;function Yl(){Yl.init||(Yl.init=!0,Jl=ql()!==-1)}var Xl={name:`ResizeObserver`,props:{emitOnMount:{type:Boolean,default:!1},ignoreWidth:{type:Boolean,default:!1},ignoreHeight:{type:Boolean,default:!1}},emits:[`notify`],mounted(){Yl(),Ae(()=>{this._w=this.$el.offsetWidth,this._h=this.$el.offsetHeight,this.emitOnMount&&this.emitSize()});let e=document.createElement(`object`);this._resizeObject=e,e.setAttribute(`aria-hidden`,`true`),e.setAttribute(`tabindex`,-1),e.onload=this.addResizeHandlers,e.type=`text/html`,Jl&&this.$el.appendChild(e),e.data=`about:blank`,Jl||this.$el.appendChild(e)},beforeUnmount(){this.removeResizeHandlers()},methods:{compareAndNotify(){(!this.ignoreWidth&&this._w!==this.$el.offsetWidth||!this.ignoreHeight&&this._h!==this.$el.offsetHeight)&&(this._w=this.$el.offsetWidth,this._h=this.$el.offsetHeight,this.emitSize())},emitSize(){this.$emit(`notify`,{width:this._w,height:this._h})},addResizeHandlers(){this._resizeObject.contentDocument.defaultView.addEventListener(`resize`,this.compareAndNotify),this.compareAndNotify()},removeResizeHandlers(){this._resizeObject&&this._resizeObject.onload&&(!Jl&&this._resizeObject.contentDocument&&this._resizeObject.contentDocument.defaultView.removeEventListener(`resize`,this.compareAndNotify),this.$el.removeChild(this._resizeObject),this._resizeObject.onload=null,this._resizeObject=null)}}},Zl=et(`data-v-b329ee4c`);Ne(`data-v-b329ee4c`);var Ql={class:`resize-observer`,tabindex:`-1`};Ce(),Xl.render=Zl((e,t,n,r,i,a)=>(G(),F(`div`,Ql))),Xl.__scopeId=`data-v-b329ee4c`,Xl.__file=`src/components/ResizeObserver.vue`;var $l=(e=`theme`)=>({computed:{themeClass(){return gl(this[e])}}}),eu=J({name:`VPopperContent`,components:{ResizeObserver:Xl},mixins:[$l()],props:{popperId:String,theme:String,shown:Boolean,mounted:Boolean,skipTransition:Boolean,autoHide:Boolean,handleResize:Boolean,classes:Object,result:Object},emits:[`hide`,`resize`],methods:{toPx(e){return e!=null&&!isNaN(e)?`${e}px`:null}}}),tu=[`id`,`aria-hidden`,`tabindex`,`data-popper-placement`],nu={ref:`inner`,class:`v-popper__inner`},ru=[P(`div`,{class:`v-popper__arrow-outer`},null,-1),P(`div`,{class:`v-popper__arrow-inner`},null,-1)];function iu(e,t,n,r,i,a){let o=De(`ResizeObserver`);return G(),j(`div`,{id:e.popperId,ref:`popover`,class:V([`v-popper__popper`,[e.themeClass,e.classes.popperClass,{"v-popper__popper--shown":e.shown,"v-popper__popper--hidden":!e.shown,"v-popper__popper--show-from":e.classes.showFrom,"v-popper__popper--show-to":e.classes.showTo,"v-popper__popper--hide-from":e.classes.hideFrom,"v-popper__popper--hide-to":e.classes.hideTo,"v-popper__popper--skip-transition":e.skipTransition,"v-popper__popper--arrow-overflow":e.result&&e.result.arrow.overflow,"v-popper__popper--no-positioning":!e.result}]]),style:R(e.result?{position:e.result.strategy,transform:`translate3d(${Math.round(e.result.x)}px,${Math.round(e.result.y)}px,0)`}:void 0),"aria-hidden":e.shown?`false`:`true`,tabindex:e.autoHide?0:void 0,"data-popper-placement":e.result?e.result.placement:void 0,onKeyup:t[2]||=ye(t=>e.autoHide&&e.$emit(`hide`),[`esc`])},[P(`div`,{class:`v-popper__backdrop`,onClick:t[0]||=t=>e.autoHide&&e.$emit(`hide`)}),P(`div`,{class:`v-popper__wrapper`,style:R(e.result?{transformOrigin:e.result.transformOrigin}:void 0)},[P(`div`,nu,[e.mounted?(G(),j(H,{key:0},[P(`div`,null,[Pe(e.$slots,`default`)]),e.handleResize?(G(),F(o,{key:0,onNotify:t[1]||=t=>e.$emit(`resize`,t)})):A(``,!0)],64)):A(``,!0)],512),P(`div`,{ref:`arrow`,class:`v-popper__arrow-container`,style:R(e.result?{left:e.toPx(e.result.arrow.x),top:e.toPx(e.result.arrow.y)}:void 0)},ru,4)],4)],46,tu)}var au=Wl(eu,[[`render`,iu]]),ou={methods:{show(...e){return this.$refs.popper.show(...e)},hide(...e){return this.$refs.popper.hide(...e)},dispose(...e){return this.$refs.popper.dispose(...e)},onResize(...e){return this.$refs.popper.onResize(...e)}}},su=function(){};typeof window<`u`&&(su=window.Element);var cu=J({name:`VPopperWrapper`,components:{Popper:Kl,PopperContent:au},mixins:[ou,$l(`finalTheme`)],props:{theme:{type:String,default:null},referenceNode:{type:Function,default:null},shown:{type:Boolean,default:!1},showGroup:{type:String,default:null},ariaId:{default:null},disabled:{type:Boolean,default:void 0},positioningDisabled:{type:Boolean,default:void 0},placement:{type:String,default:void 0},delay:{type:[String,Number,Object],default:void 0},distance:{type:[Number,String],default:void 0},skidding:{type:[Number,String],default:void 0},triggers:{type:Array,default:void 0},showTriggers:{type:[Array,Function],default:void 0},hideTriggers:{type:[Array,Function],default:void 0},popperTriggers:{type:Array,default:void 0},popperShowTriggers:{type:[Array,Function],default:void 0},popperHideTriggers:{type:[Array,Function],default:void 0},container:{type:[String,Object,su,Boolean],default:void 0},boundary:{type:[String,su],default:void 0},strategy:{type:String,default:void 0},autoHide:{type:[Boolean,Function],default:void 0},handleResize:{type:Boolean,default:void 0},instantMove:{type:Boolean,default:void 0},eagerMount:{type:Boolean,default:void 0},popperClass:{type:[String,Array,Object],default:void 0},computeTransformOrigin:{type:Boolean,default:void 0},autoMinSize:{type:Boolean,default:void 0},autoSize:{type:[Boolean,String],default:void 0},autoMaxSize:{type:Boolean,default:void 0},autoBoundaryMaxSize:{type:Boolean,default:void 0},preventOverflow:{type:Boolean,default:void 0},overflowPadding:{type:[Number,String],default:void 0},arrowPadding:{type:[Number,String],default:void 0},arrowOverflow:{type:Boolean,default:void 0},flip:{type:Boolean,default:void 0},shift:{type:Boolean,default:void 0},shiftCrossAxis:{type:Boolean,default:void 0},noAutoFocus:{type:Boolean,default:void 0},disposeTimeout:{type:Number,default:void 0}},emits:{show:()=>!0,hide:()=>!0,"update:shown":e=>!0,"apply-show":()=>!0,"apply-hide":()=>!0,"close-group":()=>!0,"close-directive":()=>!0,"auto-hide":()=>!0,resize:()=>!0},computed:{finalTheme(){return this.theme??this.$options.vPopperTheme}},methods:{getTargetNodes(){return Array.from(this.$el.children).filter(e=>e!==this.$refs.popperContent.$el)}}});function lu(e,t,n,r,i,a){let o=De(`PopperContent`),s=De(`Popper`);return G(),F(s,Se({ref:`popper`},e.$props,{theme:e.finalTheme,"target-nodes":e.getTargetNodes,"popper-node":()=>e.$refs.popperContent.$el,class:[e.themeClass],onShow:t[0]||=()=>e.$emit(`show`),onHide:t[1]||=()=>e.$emit(`hide`),"onUpdate:shown":t[2]||=t=>e.$emit(`update:shown`,t),onApplyShow:t[3]||=()=>e.$emit(`apply-show`),onApplyHide:t[4]||=()=>e.$emit(`apply-hide`),onCloseGroup:t[5]||=()=>e.$emit(`close-group`),onCloseDirective:t[6]||=()=>e.$emit(`close-directive`),onAutoHide:t[7]||=()=>e.$emit(`auto-hide`),onResize:t[8]||=()=>e.$emit(`resize`)}),{default:z(({popperId:t,isShown:n,shouldMountContent:r,skipTransition:i,autoHide:a,show:s,hide:c,handleResize:l,onResize:u,classes:d,result:f})=>[Pe(e.$slots,`default`,{shown:n,show:s,hide:c}),I(o,{ref:`popperContent`,"popper-id":t,theme:e.finalTheme,shown:n,mounted:r,"skip-transition":i,"auto-hide":a,"handle-resize":l,classes:d,result:f,onHide:c,onResize:u},{default:z(()=>[Pe(e.$slots,`popper`,{shown:n,hide:c})]),_:2},1032,[`popper-id`,`theme`,`shown`,`mounted`,`skip-transition`,`auto-hide`,`handle-resize`,`classes`,`result`,`onHide`,`onResize`])]),_:3},16,[`theme`,`target-nodes`,`popper-node`,`class`])}var uu=Wl(cu,[[`render`,lu]]);({...uu}),{...uu},{...uu};var du=J({name:`VTooltipDirective`,components:{Popper:jl(),PopperContent:au},mixins:[ou],inheritAttrs:!1,props:{theme:{type:String,default:`tooltip`},html:{type:Boolean,default:e=>hl(e.theme,`html`)},content:{type:[String,Number,Function],default:null},loadingContent:{type:String,default:e=>hl(e.theme,`loadingContent`)},targetNodes:{type:Function,required:!0}},data(){return{asyncContent:null}},computed:{isContentAsync(){return typeof this.content==`function`},loading(){return this.isContentAsync&&this.asyncContent==null},finalContent(){return this.isContentAsync?this.loading?this.loadingContent:this.asyncContent:this.content}},watch:{content:{handler(){this.fetchContent(!0)},immediate:!0},async finalContent(){await this.$nextTick(),this.$refs.popper.onResize()}},created(){this.$_fetchId=0},methods:{fetchContent(e){if(typeof this.content==`function`&&this.$_isShown&&(e||!this.$_loading&&this.asyncContent==null)){this.asyncContent=null,this.$_loading=!0;let e=++this.$_fetchId,t=this.content(this);t.then?t.then(t=>this.onResult(e,t)):this.onResult(e,t)}},onResult(e,t){e===this.$_fetchId&&(this.$_loading=!1,this.asyncContent=t)},onShow(){this.$_isShown=!0,this.fetchContent()},onHide(){this.$_isShown=!1}}}),fu=[`innerHTML`],pu=[`textContent`];function mu(e,t,n,r,i,a){let o=De(`PopperContent`),s=De(`Popper`);return G(),F(s,Se({ref:`popper`},e.$attrs,{theme:e.theme,"target-nodes":e.targetNodes,"popper-node":()=>e.$refs.popperContent.$el,onApplyShow:e.onShow,onApplyHide:e.onHide}),{default:z(({popperId:t,isShown:n,shouldMountContent:r,skipTransition:i,autoHide:a,hide:s,handleResize:c,onResize:l,classes:u,result:d})=>[I(o,{ref:`popperContent`,class:V({"v-popper--tooltip-loading":e.loading}),"popper-id":t,theme:e.theme,shown:n,mounted:r,"skip-transition":i,"auto-hide":a,"handle-resize":c,classes:u,result:d,onHide:s,onResize:l},{default:z(()=>[e.html?(G(),j(`div`,{key:0,innerHTML:e.finalContent},null,8,fu)):(G(),j(`div`,{key:1,textContent:q(e.finalContent)},null,8,pu))]),_:2},1032,[`class`,`popper-id`,`theme`,`shown`,`mounted`,`skip-transition`,`auto-hide`,`handle-resize`,`classes`,`result`,`onHide`,`onResize`])]),_:1},16,[`theme`,`target-nodes`,`popper-node`,`onApplyShow`,`onApplyHide`])}var hu=Wl(du,[[`render`,mu]]),gu=`v-popper--has-tooltip`;function _u(e,t){let n=e.placement;if(!n&&t)for(let e of bl)t[e]&&(n=e);return n||=hl(e.theme||`tooltip`,`placement`),n}function vu(e,t,n){let r,i=typeof t;return r=i===`string`?{content:t}:t&&i===`object`?t:{content:!1},r.placement=_u(r,n),r.targetNodes=()=>[e],r.referenceNode=()=>e,r}var yu,bu,xu=0;function Su(){if(yu)return;bu=B([]),yu=Oe({name:`VTooltipDirectiveApp`,setup(){return{directives:bu}},render(){return this.directives.map(e=>je(hu,{...e.options,shown:e.shown||e.options.shown,key:e.id}))},devtools:{hide:!0}});let e=document.createElement(`div`);document.body.appendChild(e),yu.mount(e)}function Cu(e,t,n){Su();let r=B(vu(e,t,n)),i=B(!1),a={id:xu++,options:r,shown:i};return bu.value.push(a),e.classList&&e.classList.add(gu),e.$_popper={options:r,item:a,show(){i.value=!0},hide(){i.value=!1}}}function wu(e){if(e.$_popper){let t=bu.value.indexOf(e.$_popper.item);t!==-1&&bu.value.splice(t,1),delete e.$_popper,delete e.$_popperOldShown,delete e.$_popperMountTarget}e.classList&&e.classList.remove(gu)}function Tu(e,{value:t,modifiers:n}){let r=vu(e,t,n);if(!r.content||hl(r.theme||`tooltip`,`disabled`))wu(e);else{let i;e.$_popper?(i=e.$_popper,i.options.value=r):i=Cu(e,t,n),typeof t.shown<`u`&&t.shown!==e.$_popperOldShown&&(e.$_popperOldShown=t.shown,t.shown?i.show():i.hide())}}var Eu={beforeMount:Tu,updated:Tu,beforeUnmount(e){wu(e)}},Du=J({__name:`ActionIconButton`,props:{as:{},to:{},href:{},icon:{},tooltip:{},active:{type:Boolean},disabled:{type:Boolean},label:{},compact:{type:Boolean},activeClass:{}},setup(e){let t=e,n=N(()=>t.as?t.as:t.href!=null||t.to!=null?`a`:`button`),r=N(()=>n.value===`a`),i=N(()=>n.value===`button`),a=N(()=>t.compact?`btn-icon-compact`:`btn-icon`),o=N(()=>t.compact?`w-auto! h-auto! p-[0.25em]`:`w-auto! h-auto! p-[0.5em]`),s=N(()=>t.active?t.activeClass||`color-active bg-active op100`:``);return(t,c)=>Ze((G(),F(he(n.value),{type:i.value?`button`:void 0,class:V([a.value,o.value,s.value,{"pointer-events-none op-mute":e.disabled&&!i.value}]),href:r.value?e.href??e.to:void 0,to:e.as&&e.to!=null?e.to:void 0,disabled:i.value?e.disabled:void 0,"aria-disabled":e.disabled&&!i.value||void 0,"aria-label":e.label??e.tooltip,"aria-pressed":e.active||void 0},{default:z(()=>[e.icon?(G(),j(`span`,{key:0,class:V(e.icon),"aria-hidden":`true`},null,2)):A(``,!0),Pe(t.$slots,`default`),Pe(t.$slots,`badge`)]),_:3},8,[`type`,`class`,`href`,`to`,`disabled`,`aria-disabled`,`aria-label`,`aria-pressed`])),[[U(Eu),e.tooltip]])}}),Ou={class:`devframes-view-assets-error absolute inset-0 flex items-center justify-center overflow-auto bg-base p6`},ku={class:`w-full max-w-100 flex flex-col items-center gap-2 text-center`},Au={class:`text-xs color-faint`},ju={class:`font-mono`},Mu={class:`mt-2 w-full flex items-center gap-1 border border-base rounded bg-secondary py1 pl2.5 pr1`},Nu={class:`flex-1 truncate text-left text-xs font-mono color-base`},Pu={class:`w-full max-h-24 overflow-auto text-left text-xs font-mono color-faint break-words`},Fu=J({__name:`ViewAssetsError`,props:{error:{}},emits:[`retry`],setup(e,{emit:t}){let n=e,r=t,i=N(()=>`npm install ${n.error.package}@${n.error.version}`),a=B(!1),o;async function s(){try{await navigator.clipboard.writeText(i.value),a.value=!0,clearTimeout(o),o=setTimeout(()=>a.value=!1,1500)}catch{}}return(t,n)=>(G(),j(`div`,Ou,[P(`div`,ku,[n[5]||=P(`div`,{class:`i-ph:cloud-warning-duotone text-3xl color-faint`},null,-1),n[6]||=P(`div`,{class:`text-sm color-muted`},` Failed to load assets remotely `,-1),P(`div`,Au,[n[2]||=M(` This tool's UI is published as `,-1),P(`span`,ju,q(e.error.package)+`@`+q(e.error.version),1),n[3]||=M(`. Install it locally, or enable network access to continue. `,-1)]),P(`div`,Mu,[P(`code`,Nu,q(i.value),1),I(Du,{class:`text-sm`,icon:a.value?`i-ph:check`:`i-ph:copy`,label:a.value?`Copied`:`Copy install command`,onClick:n[0]||=e=>s()},null,8,[`icon`,`label`])]),P(`div`,Pu,q(e.error.reason),1),I(ma,{class:`mt-2`,onClick:n[1]||=e=>r(`retry`)},{default:z(()=>[...n[4]||=[M(` Retry `,-1)]]),_:1})])]))}}),Iu={class:`devframes-view-iframe-loading absolute inset-0 flex flex-col items-center justify-center gap-2`},Lu=J({__name:`ViewIframeLoading`,setup(e){return(e,t)=>(G(),j(`div`,Iu,[...t[0]||=[P(`div`,{class:`i-ph:circle-notch-duotone animate-spin text-3xl color-faint`},null,-1),P(`div`,{class:`text-sm color-muted`},` Loading… `,-1)]]))}}),Ru={class:`w-full h-full flex flex-col`},zu={key:1,class:`flex items-center gap-1 px2 py1 rounded text-xs bg-amber/10 text-amber border border-amber/20 shrink-0`,title:`Cross-origin iframe`},Bu={class:`flex-1 flex items-center h-7 px-2.5 rounded bg-gray/5 border border-transparent hover:border-gray/10 focus-within:border-gray/15 transition-colors`},Vu=[`value`,`readonly`],Hu={key:0,class:`i-ph-circle-notch text-sm op40 ml-2 shrink-0 animate-spin`},Uu=40,Wu=J({__name:`ViewIframe`,props:{context:{},entry:{},panes:{},iframeStyle:{}},setup(e){let t=e,n=oe(t.context),r=N(()=>t.context.panel.store.mode===`edge`),i=N(()=>typeof t.entry.addressBar==`object`?t.entry.addressBar:void 0),a=N(()=>n.value.showIframeAddressBar||!!t.entry.addressBar),o=B(!0),s=B(!1),c=B(!1),l=B(null),u=N(()=>!l.value&&(o.value||s.value)),d=K(`viewFrame`),f=K(`urlInput`),p=B(t.entry.url),m=B(t.entry.url),h=B(!1),g=N(()=>t.entry.frameId??t.entry.id),_=N(()=>t.panes.get(g.value)?.iframe),v=N(()=>{try{return window.location.origin}catch{return``}}),y=N(()=>{try{return new URL(p.value).origin!==v.value}catch{return!0}}),b=N(()=>i.value?.back??!y.value),x=N(()=>i.value?.reload??!y.value),S=N(()=>i.value?.openExternal??!1),C=N(()=>{let e=re(p.value);if(y.value)return e;try{let t=new URL(e);return t.pathname+t.search+t.hash}catch{return e}});function w(e){let t=e.data;typeof t!=`object`||!t||t.type!==ce||e.source===_.value?.contentWindow&&(l.value={type:ce,package:String(t.package??``),version:String(t.version??``),reason:String(t.reason??``)})}function T(e){let t=_.value;if(!t)return;l.value=null;let n=e.trim();n&&!/^https?:\/\//i.test(n)&&(n=n.startsWith(`/`)?`${window.location.origin}${n}`:`http://${n}`),p.value=n,m.value=n,t.src=n,s.value=!0}function E(){h.value=!1,m.value!==p.value&&T(m.value)}function ee(){h.value=!0,m.value=p.value,Ae(()=>{f.value?.select()})}function te(){h.value=!1,m.value=p.value}function D(e){e.key===`Escape`&&(h.value=!1,m.value=p.value,f.value?.blur())}function ne(){try{_.value?.contentWindow?.history.back()}catch{}}function ie(){let e=_.value;if(!e)return;l.value=null,s.value=!0;let t=p.value;e.src=``,e.src=t,p.value=t,m.value=t}function ae(){try{let e=new URL(re(p.value));(e.protocol===`http:`||e.protocol===`https:`)&&window.open(e.href,`_blank`,`noopener,noreferrer`)}catch{}}let se,le;return Me(()=>{let e=t.panes.has(g.value),n=t.context.panel.consumeBootRoute?.(t.entry.id)??t.entry.url;!e&&n!==p.value&&(p.value=n,m.value=n);let i=t.panes.ensure(g.value,{src:n,style:{boxShadow:`none`,outline:`none`}}),u=i.iframe;le=ot({iframe:u,initial:p.value,onChange:e=>{p.value=e}}),e||(s.value=!0);let f=t.context.panel.session;$e(()=>{t.context.docks.selectedId===t.entry.id&&(f.selectedDockRoute=p.value)}),se=()=>{s.value=!1},u.addEventListener(`load`,se);let h=t.context.docks.getStateById(t.entry.id);h&&(h.domElements.iframe=u),$e(()=>{Object.assign(u.style,t.iframeStyle),a.value&&!r.value?(u.style.borderTopLeftRadius=`0px`,u.style.borderTopRightRadius=`0px`):(u.style.borderTopLeftRadius=``,u.style.borderTopRightRadius=``),r.value&&(u.style.borderRadius=`0px`,u.style.border=`none`)}),$e(()=>{c.value&&(l.value||s.value?i.hide():i.show())}),window.addEventListener(`message`,w),i.mount(d.value),o.value=!1,c.value=!0,Ae(()=>{i.update()})}),xe(()=>{window.removeEventListener(`message`,w),le?.(),le=void 0;let e=t.panes.get(g.value);e&&se&&e.iframe?.removeEventListener(`load`,se),e&&e.target===d.value&&e.unmount()}),(e,t)=>(G(),j(`div`,Ru,[a.value?(G(),j(`div`,{key:0,class:`flex-none px-2 w-full flex items-center gap-1 color-base border-base border-b`,style:R({height:`${Uu}px`})},[b.value?(G(),j(`button`,{key:0,class:`w-7 h-7 flex items-center justify-center rounded hover:bg-gray/15 transition-colors shrink-0`,title:`Back`,onClick:ne},[...t[1]||=[P(`div`,{class:`i-ph-caret-left op60 w-4.5 h-4.5`},null,-1)]])):A(``,!0),y.value?(G(),j(`div`,zu,[...t[2]||=[P(`div`,{class:`i-ph-globe text-sm`},null,-1),P(`span`,null,`Cross-Origin`,-1)]])):A(``,!0),x.value?(G(),j(`button`,{key:2,class:`w-7 h-7 flex items-center justify-center rounded hover:bg-gray/15 transition-colors shrink-0`,title:`Reload`,onClick:ie},[...t[3]||=[P(`div`,{class:`i-ph-arrow-clockwise op60 w-4.5 h-4.5`},null,-1)]])):A(``,!0),P(`div`,Bu,[P(`input`,{ref:`urlInput`,value:h.value?m.value:C.value,type:`text`,class:`flex-1 bg-transparent outline-none text-sm font-mono`,placeholder:`Enter URL...`,readonly:y.value,onInput:t[0]||=e=>m.value=e.target.value,onFocus:ee,onBlur:te,onKeydown:[D,ye(E,[`enter`])]},null,40,Vu),s.value?(G(),j(`div`,Hu)):A(``,!0)]),S.value?(G(),j(`button`,{key:3,class:`w-7 h-7 flex items-center justify-center rounded hover:bg-gray/15 transition-colors shrink-0`,title:`Open externally`,onClick:ae},[...t[4]||=[P(`div`,{class:`i-ph-arrow-square-out-duotone op60 w-4.5 h-4.5`},null,-1)]])):A(``,!0)],4)):A(``,!0),P(`div`,{ref_key:`viewFrame`,ref:d,class:`devframes-view-iframe relative w-full h-full flex-1 items-center justify-center`},[u.value?(G(),F(Lu,{key:0})):A(``,!0),l.value?(G(),F(Fu,{key:1,error:l.value,onRetry:ie},null,8,[`error`])):A(``,!0)],512)]))}}),Gu={class:`flex flex-col gap-4 items-center justify-center h-full relative`},Ku={class:`text-2xl font-bold`},qu={key:0,class:`flex flex-col gap-1 max-w-full w-64 items-start`},Ju=[`disabled`,`title`],Yu=[`value`],Xu={class:`min-h-16 max-w-full flex flex-col gap-2 items-center`},Zu={key:0,class:`max-w-full text-sm text-red-600 dark:text-red-400 text-center text-balance`},Qu={key:1,class:`inline-flex items-center gap-2 text-sm color-base op70`},$u={key:0,class:`i-ph-dots-three-outline-duotone animate-pulse flex-none`},ed={class:`truncate`},td=J({__name:`ViewLauncher`,props:{context:{},entry:{}},setup(e){let t=e,n=N(()=>t.entry.launcher.roots),r=B(n.value?.[0]?.value);Xe(n,e=>{let t=e?.[0];if(!t){r.value=void 0;return}e.some(e=>e.value===r.value)||(r.value=t.value)},{immediate:!0});function i(){let e=t.entry.launcher.command;e&&t.context.commands.execute(e,n.value?.length?{root:r.value}:void 0)}let a=N(()=>t.entry.launcher.status||`idle`),o=N(()=>t.entry.launcher.digest),s=N(()=>t.entry.launcher.terminalSessionId);function c(){s.value&&t.context.rpc.call(Ee.rpc.docksActivate,{dockId:p,params:{sessionId:s.value}})}let l={error:`i-ph-arrow-clockwise-duotone`,idle:`i-ph-rocket-launch-duotone`,loading:`i-ph-spinner-gap-duotone animate-spin`,success:`i-ph:check`},u=N(()=>t.entry.launcher.error),d=N(()=>a.value===`idle`?t.entry.launcher.buttonStart||`Launch`:a.value===`loading`?t.entry.launcher.buttonLoading||`Loading...`:a.value===`error`?`Retry`:a.value===`success`?`Success`:`UNKNOWN STATUS: ${a.value}`),f=N(()=>a.value===`idle`||a.value===`error`);return(t,p)=>(G(),j(`div`,Gu,[I(Ui,{icon:e.entry.launcher.icon||e.entry.icon,class:`w-10 h-10`},null,8,[`icon`]),P(`h1`,Ku,q(e.entry.launcher.title),1),P(`p`,null,q(e.entry.launcher.description),1),n.value?.length?(G(),j(`label`,qu,[p[1]||=P(`span`,{class:`text-xs op60`},`Launch root`,-1),Ze(P(`select`,{"onUpdate:modelValue":p[0]||=e=>r.value=e,disabled:a.value===`loading`,title:n.value.find(e=>e.value===r.value)?.description,class:`w-full px3 py2 text-sm rounded-lg bg-base color-base border border-base outline-none transition-all focus-visible:ring-3 focus-visible:ring-primary-500/30 disabled:op50 disabled:pointer-events-none`},[(G(!0),j(H,null,L(n.value,e=>(G(),j(`option`,{key:e.value,value:e.value},q(e.label),9,Yu))),128))],8,Ju),[[it,r.value]])])):A(``,!0),I(ma,{variant:`primary`,class:V([`min-w-40 justify-center`,a.value===`error`?`bg-error! border-error!`:``]),icon:l[a.value],loading:a.value===`loading`,disabled:!f.value,onClick:i},{default:z(()=>[M(q(d.value),1)]),_:1},8,[`class`,`icon`,`loading`,`disabled`]),P(`div`,Xu,[a.value===`error`&&u.value?(G(),j(`p`,Zu,q(u.value),1)):o.value?(G(),j(`div`,Qu,[a.value===`loading`?(G(),j(`div`,$u)):A(``,!0),P(`span`,ed,q(o.value),1)])):A(``,!0),s.value?(G(),F(ma,{key:2,variant:`text`,size:`sm`,icon:`i-ph-arrow-square-out-duotone`,onClick:c},{default:z(()=>[...p[2]||=[M(` View in Terminal `,-1)]]),_:1})):A(``,!0)])]))}}),nd={key:2},rd=J({__name:`ViewEntry`,props:{context:{},entry:{},panes:{},iframeStyle:{},divStyle:{}},setup(e){return(t,n)=>(G(),F(ge,null,{fallback:z(()=>[...n[0]||=[P(`div`,null,` Loading... `,-1)]]),default:z(()=>[e.entry.type===`~builtin`?(G(),j(H,{key:0},[e.entry.id===`~settings`?(G(),F(zs,{key:0,context:e.context,entry:e.entry},null,8,[`context`,`entry`])):e.entry.id===`~client-auth-notice`?(G(),F(_o,{key:1,context:e.context},null,8,[`context`])):(G(),j(`div`,nd,` Unknown builtin entry: `+q(e.entry),1))],64)):e.entry.type===`action`?(G(),j(H,{key:1},[],64)):e.entry.type===`iframe`?(G(),F(Wu,{key:2,context:e.context,entry:e.entry,panes:e.panes,"iframe-style":e.iframeStyle},null,8,[`context`,`entry`,`panes`,`iframe-style`])):e.entry.type===`custom-render`?(G(),F(Bs,{key:3,context:e.context,entry:e.entry,panes:e.panes,"div-style":e.divStyle},null,8,[`context`,`entry`,`panes`,`div-style`])):e.entry.type===`launcher`?(G(),F(td,{key:4,context:e.context,entry:e.entry},null,8,[`context`,`entry`])):(G(),F(qs,{key:5,context:e.context,entry:e.entry},null,8,[`context`,`entry`]))]),_:1}))}}),id=J({__name:`ColorSchemeRoot`,setup(e){return(e,t)=>(G(),j(`div`,{class:V([`devframes-color-root`,U(Be)?`dark`:`light`]),style:R({display:`contents`,colorScheme:U(Be)?`dark`:`light`})},[Pe(e.$slots,`default`)],6))}});function ad(e){return je(`button`,{class:`flex items-center gap-2 px3 py1.5 rounded hover:bg-active transition text-left`,onClick:e.action},[je(`div`,{class:`${e.icon} text-base op60`}),je(`span`,e.label)])}function od(e,t){let n=e.docks.settings,r=t.id;n.mutate(e=>{e.docksHidden.includes(r)||(e.docksHidden=[...e.docksHidden,r])}),e.docks.selected?.id===r&&e.docks.switchEntry(null),le(null)}function sd(e,t){let n=e.docks.getStateById(t.id)?.domElements.iframe;if(!n){le(null);return}let r=n.src;n.src=``,n.src=r,le(null)}function cd(e,t){return t.id!==`~settings`&&e.docks.entries.some(e=>e.id===t.id)}function ld(e){return e.type===`iframe`}function ud(e){let{context:t,entry:n,el:r,gap:i=6}=e,a=t.panel.store.mode===`edge`,o=[{label:`Hide`,icon:`i-ph-eye-slash-duotone`,action:()=>od(t,n),visible:cd(t,n)},{label:`Refresh`,icon:`i-ph-arrow-clockwise-duotone`,action:()=>sd(t,n),visible:ld(n)},{label:a?`Float Mode`:`Edge Mode`,icon:a?`i-ph-arrows-out-duotone`:`i-ph-square-half-bottom-duotone`,action:()=>{if(a){let e=t.panel.store;switch(e.position){case`bottom`:e.left=50,e.top=100;break;case`top`:e.left=50,e.top=0;break;case`left`:e.left=0,e.top=50;break;case`right`:e.left=100,e.top=50}e.mode=`float`}else t.panel.store.mode=`edge`;le(null)},visible:t.clientType===`embedded`},{label:`Popup`,icon:`i-ph-arrow-square-out-duotone`,action:()=>{le(null),se(t)},visible:ue()&&!ie().value&&t.clientType===`embedded`}].filter(e=>e.visible);o.length!==0&&le({el:r,gap:i,content:()=>je(`div`,{class:`flex flex-col text-sm min-w-36 mx--1`},o.map(ad))})}var dd=[`aria-label`],fd=J({__name:`DockEntry`,props:{context:{},dock:{},isAction:{type:Boolean},isSelected:{type:Boolean},isDimmed:{type:Boolean},isVertical:{type:Boolean},badge:{},badgeVariant:{},tooltip:{type:Boolean,default:!0}},setup(e){let t=e,n={info:{bg:`#3b82f6`,fg:`#eff6ff`},success:{bg:`#22c55e`,fg:`#f0fdf4`},warning:{bg:`#f59e0b`,fg:`#fffbeb`},danger:{bg:`#ef4444`,fg:`#fef2f2`}},r=N(()=>{if(!t.badgeVariant||t.badgeVariant==="default")return;let{bg:e,fg:r}=n[t.badgeVariant];return{backgroundColor:e,color:r}}),i=K(`button`);function a(){t.tooltip&&i.value&&w({content:t.dock.title,el:i.value})}function o(){t.tooltip&&w(null)}function s(e){if(!i.value||t.dock.id===`overflow`)return;e.preventDefault(),o();let n=t.context.docks.entries.find(e=>e.id===t.dock.id);n&&ud({context:t.context,entry:n,el:i.value,gap:6})}return Fe(`pointerdown`,()=>{t.tooltip&&w(null)}),(t,n)=>(G(),j(`div`,{key:e.dock.id,class:`relative group devframes-dock-entry`,onPointerenter:a,onPointerleave:o,onContextmenu:s},[P(`button`,{ref_key:`button`,ref:i,"aria-label":e.dock.title,class:V([[e.isVertical?`rotate-270`:``,e.isDimmed?`op50 saturate-0`:``,e.isSelected?`scale-120 text-primary`:``,e.isAction?`bg-[#8881] hover:bg-[#8882] rounded-full`:`rounded-xl`],`flex items-center justify-center p1.5 hover:bg-[#8881] hover:scale-110 transition-all duration-300 relative outline-none`])},[I(Ui,{icon:e.dock.icon,class:`w-5 h-5 select-none`},null,8,[`icon`]),e.badge?(G(),j(`div`,{key:0,class:V([`absolute top-0.5 right-0 text-0.6em px-1 rounded-full shadow`,r.value?``:`bg-gray-500 text-white`]),style:R(r.value)},q(e.badge),7)):A(``,!0)],10,dd)],32))}});function pd(e){return e?{"--devframe-primary":e}:void 0}var md={class:`truncate`},hd={key:0,class:`border-t border-base mx--2 my1`},gd=[`onClick`],_d={class:`truncate flex-1`},vd={key:0,class:`bg-gray-6 text-white text-0.6em px-1 rounded-full shadow`},yd={key:0,class:`px2 py1.5 op50 text-sm italic`},bd=J({__name:`DockGroupPopover`,props:{context:{},group:{},members:{},selectedId:{}},emits:[`select`],setup(e,{emit:t}){let n=e,r=t,i=N(()=>n.members.every(([,e])=>e.length===0));return(t,n)=>(G(),j(`div`,{class:V([`flex flex-col gap-0.5 min-w-44 max-w-64`,e.group.accentColor?`devframes-accent-scope`:``]),style:R(U(pd)(e.group.accentColor)),onMousemove:n[0]||=Te(()=>{},[`stop`])},[P(`div`,{class:V([`flex items-center gap-1.5 px4 mx--2 pt2 pb2.5 font-bold text-2.75 uppercase tracking-wide border-b border-base`,e.group.accentColor?`color-active`:``])},[I(Ui,{icon:e.group.icon,class:`w-4.5 h-4.5`},null,8,[`icon`]),P(`span`,md,q(e.group.title),1)],2),(G(!0),j(H,null,L(e.members,([t,n],i)=>(G(),j(H,{key:t},[i>0&&n.length?(G(),j(`div`,hd)):A(``,!0),(G(!0),j(H,null,L(n,t=>(G(),j(`button`,{key:t.id,class:V([`flex items-center gap-2 w-full px2 py1.5 rounded text-sm text-left transition`,e.selectedId===t.id?`text-primary bg-active`:`op80 hover:op100 hover:bg-active`]),onClick:e=>r(`select`,t)},[I(Ui,{icon:t.icon,class:`w-4.5 h-4.5 flex-none`},null,8,[`icon`]),P(`span`,_d,q(t.title),1),t.badge?(G(),j(`div`,vd,q(t.badge),1)):A(``,!0)],10,gd))),128))],64))),128)),i.value?(G(),j(`div`,yd,` No tools yet `)):A(``,!0)],38))}}),xd=J({__name:`DockGroupButton`,props:{context:{},group:{},isVertical:{type:Boolean},selected:{},dimInactive:{type:Boolean,default:!0}},emits:[`select`],setup(e,{emit:t}){let r=e,i=t,a=oe(r.context),o=N(()=>n(r.context.docks.entries,r.group.id,a.value,{whenContext:r.context.when.context})),s=N(()=>v(r.context.docks.entries,r.group.id,a.value,{whenContext:r.context.when.context})),l=N(()=>{let e=r.selected?.id;return!!e&&o.value.some(t=>t.id===e)}),u=K(`groupButton`),d=B(!1),f=ne();function p(){u.value&&(d.value=!0,S({el:u.value,content:()=>je(bd,{context:r.context,group:r.group,members:s.value,selectedId:r.selected?.id??null,onSelect:e=>{i(`select`,e),m()}})}))}function m(){d.value=!1,S(null)}function h(){d.value?m():p()}Ie(()=>f.value,e=>{d.value=e?.el===u.value},{debounce:1e3});function g(){if(l.value){m(),i(`select`,void 0);return}let e=c(r.context.docks.entries,r.group,r.context.panel.session.groupLastChildIds?.[r.group.id],r.context.when.context);if(e){m(),i(`select`,e);return}h()}return(t,n)=>(G(),j(`div`,{ref_key:`groupButton`,ref:u,class:V(e.group.accentColor?`devframes-accent-scope`:``),style:R(U(pd)(e.group.accentColor))},[I(fd,{context:e.context,dock:e.group,"is-vertical":e.isVertical,"is-selected":l.value||d.value,"is-dimmed":e.dimInactive&&e.selected?!l.value:!1,badge:e.group.badge,"badge-variant":e.group.badgeVariant,onClick:g},null,8,[`context`,`dock`,`is-vertical`,`is-selected`,`is-dimmed`,`badge`,`badge-variant`])],6))}}),Sd=J({__name:`DockEntries`,props:{context:{},entries:{},selected:{},isVertical:{type:Boolean},dimInactive:{type:Boolean,default:!0}},emits:[`select`],setup(t,{emit:r}){let i=t,a=r,{selected:s,isVertical:c,entries:l}=W(i),u=oe(i.context);function d(t){return!(t.type===`group`&&n(i.context.docks.entries,t.id,u.value,{whenContext:i.context.when.context}).length===0&&!o(i.context.docks.entries,t.id,t.defaultChildId,i.context.when.context)||t.when&&!e(t.when,i.context.when.context)||t.visibility&&!e(t.visibility,i.context.when.context))}function f(e){s.value?.id===e.id?a(`select`,void 0):a(`select`,e)}return(e,n)=>(G(!0),j(H,null,L(U(l),e=>(G(),j(H,{key:e.id},[d(e)?(G(),j(H,{key:0},[e.type===`group`?(G(),F(xd,{key:0,context:t.context,group:e,"is-vertical":U(c),selected:U(s),"dim-inactive":t.dimInactive,onSelect:n[0]||=e=>a(`select`,e)},null,8,[`context`,`group`,`is-vertical`,`selected`,`dim-inactive`])):(G(),F(fd,{key:1,context:t.context,dock:e,"is-action":e.type===`action`,"is-selected":U(s)?.id===e.id,"is-dimmed":t.dimInactive&&U(s)?U(s).id!==e.id:!1,"is-vertical":U(c),badge:e.badge,"badge-variant":e.badgeVariant,onClick:t=>f(e)},null,8,[`context`,`dock`,`is-action`,`is-selected`,`is-dimmed`,`is-vertical`,`badge`,`badge-variant`,`onClick`]))],64)):A(``,!0)],64))),128))}}),Cd=J({__name:`DockEntriesWithCategories`,props:{context:{},groups:{},selected:{},isVertical:{type:Boolean},rotate:{type:Boolean,default:!0},dimInactive:{type:Boolean,default:!0}},emits:[`select`],setup(e,{emit:t}){let n=t;return(t,r)=>(G(!0),j(H,null,L(e.groups,([i,a],o)=>(G(),j(H,{key:i},[o>0?Pe(t.$slots,`separator`,{category:i,index:o,isVertical:e.isVertical},()=>[P(`div`,{class:V([`border-base m1`,e.isVertical&&!e.rotate?`w-20px h-px border-b-1.5`:`h-20px w-px border-r-1.5`])},null,2)],void 0,0):A(``,!0),I(Sd,{context:e.context,entries:a,"is-vertical":e.isVertical&&e.rotate,selected:e.selected,"dim-inactive":e.dimInactive,onSelect:r[0]||=e=>n(`select`,e)},null,8,[`context`,`entries`,`is-vertical`,`selected`,`dim-inactive`])],64))),128))}}),wd={key:0,class:`w-8 h-px border-t border-base my0.5`},Td=[`aria-label`,`onPointerenter`,`onClick`],Ed={key:0,class:`absolute top-0.5 right-0.5 bg-gray-6 text-white text-0.6em px-0.5 rounded-full shadow leading-none`},Dd={class:`absolute top-0.5 right-0.5 bg-gray-6 text-white text-0.6em px-0.5 rounded-full shadow leading-none`},Od=34,kd=34,Ad=7,jd=53,Md=J({__name:`DockGroupSidebar`,props:{context:{},group:{},selectedId:{}},setup(e){let t=e,n=oe(t.context),r=N(()=>v(t.context.docks.entries,t.group.id,n.value,{whenContext:t.context.when.context})),i=K(`sidebar`),o=B(0);Xe(i,(e,t,n)=>{if(!e){o.value=0;return}let r=e.ownerDocument?.defaultView?.ResizeObserver??globalThis.ResizeObserver;if(!r)return;let i=new r(([e])=>{e&&(o.value=e.borderBoxSize?.[0]?.blockSize??e.contentRect.height)});i.observe(e),n(()=>i.disconnect())},{immediate:!0,flush:`post`});let s=N(()=>r.value.reduce((e,[,t])=>e+t.length,0)),c=N(()=>a({availableHeight:o.value,reservedHeight:jd,itemHeight:Od,dividerHeight:Ad,moreButtonHeight:kd,dividerCount:Math.max(0,r.value.filter(([,e])=>e.length).length-1),totalItems:s.value})),u=N(()=>l(r.value,c.value)),d=N(()=>u.value.visible),f=N(()=>u.value.overflow),p=N(()=>f.value.reduce((e,[,t])=>e+t.length,0)),m=N(()=>p.value>0),h=N(()=>p.value>9?`9+`:p.value.toString()),g=N(()=>!!t.selectedId&&f.value.some(([,e])=>e.some(e=>e.id===t.selectedId)));function _(e){t.context.docks.switchEntry(e)}function y(e,t){w({content:t,el:e.currentTarget})}function b(){w(null)}let x=K(`moreButton`),S=B(!1),C=T();function E(){x.value&&(S.value=!0,O({el:x.value,placement:`right`,content:()=>je(bd,{context:t.context,group:t.group,members:f.value,selectedId:t.selectedId,onSelect:e=>{_(e.id),ee()}})}))}function ee(){S.value=!1,O(null)}function te(){S.value?ee():E()}Ie(()=>C.value,e=>{S.value=e?.el===x.value},{debounce:1e3});let D=N(()=>g.value||S.value);return(t,n)=>(G(),j(`div`,{ref_key:`sidebar`,ref:i,class:V([`devframes-group-sidebar flex flex-col flex-none w-12 h-full border-r border-base of-y-hidden of-x-hidden select-none py1.5 items-center gap-0.5`,e.group.accentColor?`devframes-accent-scope`:``]),style:R(U(pd)(e.group.accentColor))},[P(`div`,{class:V([`flex items-center justify-center w-8 h-8 op60`,e.group.accentColor?`color-active`:``]),onPointerenter:n[0]||=t=>y(t,e.group.title),onPointerleave:b},[I(Ui,{icon:e.group.icon,class:`w-5 h-5 flex-none`},null,8,[`icon`])],34),n[2]||=P(`div`,{class:`w-8 h-px border-t border-base my0.5`},null,-1),(G(!0),j(H,null,L(d.value,([t,n],r)=>(G(),j(H,{key:t},[r>0&&n.length?(G(),j(`div`,wd)):A(``,!0),(G(!0),j(H,null,L(n,t=>(G(),j(`button`,{key:t.id,"aria-label":t.title,class:V([`relative flex items-center justify-center w-8 h-8 rounded-lg transition`,e.selectedId===t.id?`text-primary bg-active`:`op60 hover:op100 hover:bg-active`]),onPointerenter:e=>y(e,t.title),onPointerleave:b,onPointerdown:b,onClick:e=>_(t.id)},[I(Ui,{icon:t.icon,class:`w-5 h-5 flex-none`},null,8,[`icon`]),t.badge?(G(),j(`div`,Ed,q(t.badge),1)):A(``,!0)],42,Td))),128))],64))),128)),m.value?(G(),j(`button`,{key:0,ref_key:`moreButton`,ref:x,"aria-label":`Show more`,class:V([`relative flex items-center justify-center w-8 h-8 rounded-lg transition mt-auto flex-none`,D.value?`text-primary bg-active`:`op60 hover:op100 hover:bg-active`]),onPointerenter:n[1]||=e=>y(e,`Show more`),onPointerleave:b,onPointerdown:b,onClick:te},[I(Ui,{icon:`ph:dots-three-circle-duotone`,class:`w-5 h-5 flex-none`}),P(`div`,Dd,q(h.value),1)],34)):A(``,!0)],6))}}),Nd={key:0,class:`h-screen w-screen of-hidden`},Pd={key:1,class:`h-screen w-screen of-hidden grid cols-[max-content_1fr]`},Fd={class:`border-r border-base flex flex-col min-h-0`},Id={class:`p2 border-b border-base flex`},Ld={class:`transition duration-200 p2 of-y-auto`},Rd={class:`min-h-0 flex`},zd={class:`relative flex-1 min-w-0 min-h-0`},Bd=ve(J({__name:`DockStandalone`,props:{context:{}},setup(e){let t=e.context,n=K(`viewsContainer`),r=ft(n,t.panel,()=>dt(t.docks.selected)),i=pt(t,e=>{e||t.docks.switchEntry(null)});Xe(()=>t.docks.entries,()=>{t.docks.selectedId||=t.docks.entries[0]?.id??null},{immediate:!0});let a=N(()=>t.docks.groupedEntries),o=N(()=>me(t.docks.entries,t.docks.selected));function s(e){e&&t.docks.switchEntry(e)}return(e,c)=>(G(),F(id,null,{default:z(()=>[U(i)?(G(),j(`div`,Pd,[P(`div`,Fd,[P(`div`,Id,[I(ur,{class:`w-7 h-7 ma`})]),P(`div`,Ld,[I(Cd,{context:U(t),groups:a.value,"is-vertical":!1,selected:U(t).docks.selected,"dim-inactive":!1,onSelect:c[0]||=e=>s(e?.id)},{separator:z(()=>[...c[1]||=[P(`div`,{class:`border-base border-b w-full my-2`},null,-1)]]),_:1},8,[`context`,`groups`,`selected`])])]),P(`div`,Rd,[o.value?(G(),F(Md,{key:0,context:U(t),group:o.value,"selected-id":U(t).docks.selected?.id??null},null,8,[`context`,`group`,`selected-id`])):A(``,!0),P(`div`,zd,[U(t).docks.selected&&U(r)?(G(),F(rd,{key:U(t).docks.selected.id,entry:U(t).docks.selected,context:U(t),panes:U(r)},null,8,[`entry`,`context`,`panes`])):A(``,!0),P(`div`,{id:`devframes-views-container`,ref_key:`viewsContainer`,ref:n,class:`pointer-events-auto`},null,512)])])])):(G(),j(`div`,Nd,[I(_o,{context:U(t)},null,8,[`context`])])),I(ao),I(fa,{context:U(t)},null,8,[`context`]),I(qa,{context:U(t)},null,8,[`context`]),I(Sa)]),_:1}))}}),{shadowRoot:!0,styles:[ct]});customElements.get(`devframes-dock-standalone`)||customElements.define(`devframes-dock-standalone`,Bd);export{Bd as DockStandalone}; diff --git a/patches/README.md b/patches/README.md new file mode 100644 index 0000000..4a075f4 --- /dev/null +++ b/patches/README.md @@ -0,0 +1,9 @@ +# Temporary dependency patches + +`@devframes/hub-ui@0.9.10` backports [Devframe PR #375](https://github.com/devframes/devframe/pull/375), commit `6df25f3a`, to the pinned embedded and standalone browser bundles. It removes toasts when their messages disappear and preserves surviving entries during full reconciliation. It does not change notification behavior for updated messages. + +The patch changes only the corresponding message-feed logic in the published assets. The standalone asset is minified on one line, so its textual patch is large despite the small behavior change. No dependency versions or unrelated generated assets are changed. + +Remove the patch and its `patchedDependencies` entry when a compatible Devframe upgrade includes the fix. The Vite/DevTools upgrade remains separate. `tests/e2e/devframe-notifications.test.ts` exercises both installed browser bundles through the real hub, including updates, dismissal, intentional resurfacing, and incremental/full removal. + +pnpm patches apply to the consuming workspace; they are not inherited by applications installing published CDB packages. A host on hub-ui 0.9.10 needs this patch to receive the removal fix. Hosts remain responsible for their own dependency installation. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f273606..ab5692f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -60,6 +60,9 @@ catalogs: '@devframes/hub': specifier: 0.9.10 version: 0.9.10 + '@devframes/hub-ui': + specifier: 0.9.10 + version: 0.9.10 '@devframes/json-render': specifier: 0.9.10 version: 0.9.10 @@ -136,10 +139,19 @@ overrides: '@vitejs/devtools-kit>@devframes/hub': 0.9.10 '@vitejs/devtools-kit>@devframes/json-render': 0.9.10 +patchedDependencies: + '@devframes/hub-ui@0.9.10': 52f9ce54ff28e6ee4b64e20e555b085f562f9ca932e4459a9032ff910dfce087 + importers: .: devDependencies: + '@devframes/hub': + specifier: catalog:devtools + version: 0.9.10(crossws@0.4.12(srvx@1.0.3))(devframe@0.9.10(@modelcontextprotocol/client@2.0.0)(cac@7.0.0)(srvx@1.0.3)) + '@devframes/hub-ui': + specifier: catalog:devtools + version: 0.9.10(patch_hash=52f9ce54ff28e6ee4b64e20e555b085f562f9ca932e4459a9032ff910dfce087)(@devframes/hub@0.9.10(crossws@0.4.12(srvx@1.0.3))(devframe@0.9.10(@modelcontextprotocol/client@2.0.0)(cac@7.0.0)(srvx@1.0.3)))(@devframes/json-render@0.9.10(@devframes/hub@0.9.10(crossws@0.4.12(srvx@1.0.3))(devframe@0.9.10(@modelcontextprotocol/client@2.0.0)(cac@7.0.0)(srvx@1.0.3)))(devframe@0.9.10(@modelcontextprotocol/client@2.0.0)(cac@7.0.0)(srvx@1.0.3)))(devframe@0.9.10(@modelcontextprotocol/client@2.0.0)(cac@7.0.0)(srvx@1.0.3)) '@dvcol/cdb': specifier: workspace:* version: link:packages/core @@ -271,7 +283,7 @@ importers: version: 2.0.0 '@vitejs/devtools-kit': specifier: catalog:devtools - version: 0.6.1(@modelcontextprotocol/client@2.0.0)(cac@7.0.0)(srvx@1.0.3)(vite@8.1.5(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0)) + version: 0.6.1(@modelcontextprotocol/client@2.0.0)(cac@7.0.0)(crossws@0.4.12(srvx@1.0.3))(srvx@1.0.3)(vite@8.1.5(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0)) devframe: specifier: catalog:devtools version: 0.9.10(@modelcontextprotocol/client@2.0.0)(cac@7.0.0)(srvx@1.0.3) @@ -417,7 +429,7 @@ importers: version: 0.9.10(crossws@0.4.12(srvx@1.0.3))(devframe@0.9.10(@modelcontextprotocol/client@2.0.0)(cac@7.0.0)(srvx@1.0.3)) '@devframes/json-render': specifier: catalog:devtools - version: 0.9.10(@devframes/hub@0.9.10(devframe@0.9.10(@modelcontextprotocol/client@2.0.0)(cac@7.0.0)(srvx@1.0.3)))(devframe@0.9.10(@modelcontextprotocol/client@2.0.0)(cac@7.0.0)(srvx@1.0.3)) + version: 0.9.10(@devframes/hub@0.9.10(crossws@0.4.12(srvx@1.0.3))(devframe@0.9.10(@modelcontextprotocol/client@2.0.0)(cac@7.0.0)(srvx@1.0.3)))(devframe@0.9.10(@modelcontextprotocol/client@2.0.0)(cac@7.0.0)(srvx@1.0.3)) '@devframes/json-render-ui': specifier: catalog:devtools version: 0.9.10(@devframes/hub@0.9.10(crossws@0.4.12(srvx@1.0.3))(devframe@0.9.10(@modelcontextprotocol/client@2.0.0)(cac@7.0.0)(srvx@1.0.3)))(devframe@0.9.10(@modelcontextprotocol/client@2.0.0)(cac@7.0.0)(srvx@1.0.3)) @@ -656,6 +668,16 @@ packages: resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} engines: {node: '>=20.19.0'} + '@devframes/hub-ui@0.9.10': + resolution: {integrity: sha512-lO0J6UV7bwtLJv/+TrGry6LqAe4BucOj53JAmXX+kQumSPvTfjKfVvFHEk/Hiegxpz+M9zlSRMWzJ/wtpl0o4g==} + peerDependencies: + '@devframes/hub': 0.9.10 + '@devframes/json-render': 0.9.10 + devframe: 0.9.10 + peerDependenciesMeta: + '@devframes/json-render': + optional: true + '@devframes/hub@0.9.10': resolution: {integrity: sha512-BFjO0iTEIazdNJbqvJkixV6d7DdUGaCpIHEsAZob2Lc59usOa8g4+jslRepbLhIraXm7SNUzP3w8Li9OBFHcdw==} peerDependencies: @@ -3895,6 +3917,13 @@ snapshots: '@csstools/css-tokenizer@4.0.0': {} + '@devframes/hub-ui@0.9.10(patch_hash=52f9ce54ff28e6ee4b64e20e555b085f562f9ca932e4459a9032ff910dfce087)(@devframes/hub@0.9.10(crossws@0.4.12(srvx@1.0.3))(devframe@0.9.10(@modelcontextprotocol/client@2.0.0)(cac@7.0.0)(srvx@1.0.3)))(@devframes/json-render@0.9.10(@devframes/hub@0.9.10(crossws@0.4.12(srvx@1.0.3))(devframe@0.9.10(@modelcontextprotocol/client@2.0.0)(cac@7.0.0)(srvx@1.0.3)))(devframe@0.9.10(@modelcontextprotocol/client@2.0.0)(cac@7.0.0)(srvx@1.0.3)))(devframe@0.9.10(@modelcontextprotocol/client@2.0.0)(cac@7.0.0)(srvx@1.0.3))': + dependencies: + '@devframes/hub': 0.9.10(crossws@0.4.12(srvx@1.0.3))(devframe@0.9.10(@modelcontextprotocol/client@2.0.0)(cac@7.0.0)(srvx@1.0.3)) + devframe: 0.9.10(@modelcontextprotocol/client@2.0.0)(cac@7.0.0)(srvx@1.0.3) + optionalDependencies: + '@devframes/json-render': 0.9.10(@devframes/hub@0.9.10(crossws@0.4.12(srvx@1.0.3))(devframe@0.9.10(@modelcontextprotocol/client@2.0.0)(cac@7.0.0)(srvx@1.0.3)))(devframe@0.9.10(@modelcontextprotocol/client@2.0.0)(cac@7.0.0)(srvx@1.0.3)) + '@devframes/hub@0.9.10(crossws@0.4.12(srvx@1.0.3))(devframe@0.9.10(@modelcontextprotocol/client@2.0.0)(cac@7.0.0)(srvx@1.0.3))': dependencies: '@standard-schema/spec': 1.1.0 @@ -3915,7 +3944,7 @@ snapshots: '@devframes/hub': 0.9.10(crossws@0.4.12(srvx@1.0.3))(devframe@0.9.10(@modelcontextprotocol/client@2.0.0)(cac@7.0.0)(srvx@1.0.3)) devframe: 0.9.10(@modelcontextprotocol/client@2.0.0)(cac@7.0.0)(srvx@1.0.3) - '@devframes/json-render@0.9.10(@devframes/hub@0.9.10(devframe@0.9.10(@modelcontextprotocol/client@2.0.0)(cac@7.0.0)(srvx@1.0.3)))(devframe@0.9.10(@modelcontextprotocol/client@2.0.0)(cac@7.0.0)(srvx@1.0.3))': + '@devframes/json-render@0.9.10(@devframes/hub@0.9.10(crossws@0.4.12(srvx@1.0.3))(devframe@0.9.10(@modelcontextprotocol/client@2.0.0)(cac@7.0.0)(srvx@1.0.3)))(devframe@0.9.10(@modelcontextprotocol/client@2.0.0)(cac@7.0.0)(srvx@1.0.3))': dependencies: '@json-render/core': 0.20.0 '@standard-schema/spec': 1.1.0 @@ -4657,10 +4686,10 @@ snapshots: '@typescript-eslint/types': 8.65.0 eslint-visitor-keys: 5.0.1 - '@vitejs/devtools-kit@0.6.1(@modelcontextprotocol/client@2.0.0)(cac@7.0.0)(srvx@1.0.3)(vite@8.1.5(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0))': + '@vitejs/devtools-kit@0.6.1(@modelcontextprotocol/client@2.0.0)(cac@7.0.0)(crossws@0.4.12(srvx@1.0.3))(srvx@1.0.3)(vite@8.1.5(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0))': dependencies: '@devframes/hub': 0.9.10(crossws@0.4.12(srvx@1.0.3))(devframe@0.9.10(@modelcontextprotocol/client@2.0.0)(cac@7.0.0)(srvx@1.0.3)) - '@devframes/json-render': 0.9.10(@devframes/hub@0.9.10(devframe@0.9.10(@modelcontextprotocol/client@2.0.0)(cac@7.0.0)(srvx@1.0.3)))(devframe@0.9.10(@modelcontextprotocol/client@2.0.0)(cac@7.0.0)(srvx@1.0.3)) + '@devframes/json-render': 0.9.10(@devframes/hub@0.9.10(crossws@0.4.12(srvx@1.0.3))(devframe@0.9.10(@modelcontextprotocol/client@2.0.0)(cac@7.0.0)(srvx@1.0.3)))(devframe@0.9.10(@modelcontextprotocol/client@2.0.0)(cac@7.0.0)(srvx@1.0.3)) devframe: 0.9.10(@modelcontextprotocol/client@2.0.0)(cac@7.0.0)(srvx@1.0.3) local-pkg: 1.2.1 mlly: 1.8.2 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 602b8e8..7911204 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -16,6 +16,8 @@ packages: overrides: '@vitejs/devtools-kit>@devframes/hub': 0.9.10 '@vitejs/devtools-kit>@devframes/json-render': 0.9.10 +patchedDependencies: + '@devframes/hub-ui@0.9.10': patches/@devframes__hub-ui@0.9.10.patch catalog: '@modelcontextprotocol/client': 2.0.0 '@modelcontextprotocol/node': 2.0.0 @@ -38,6 +40,7 @@ catalogs: turbo: 2.10.8 devtools: '@devframes/hub': 0.9.10 + '@devframes/hub-ui': 0.9.10 '@devframes/json-render': 0.9.10 '@devframes/json-render-ui': 0.9.10 '@vitejs/devtools-kit': 0.6.1 diff --git a/tests/e2e/devframe-native.test.ts b/tests/e2e/devframe-native.test.ts index b79db4e..cdcac55 100644 --- a/tests/e2e/devframe-native.test.ts +++ b/tests/e2e/devframe-native.test.ts @@ -199,4 +199,4 @@ it('runs native MCP actions through Chrome extension and shared Devframe RPC acr } finally { for (const cleanup of cleanups.reverse()) await cleanup(); } -}, process.env.CDB_LIFECYCLE_DIAGNOSTICS_OUTPUT === undefined ? (process.env.CDB_DEVFRAME_BENCHMARK_OUTPUT === undefined ? 90_000 : 240_000) : 600_000); +}, process.env.CDB_LIFECYCLE_DIAGNOSTICS_OUTPUT === undefined ? (process.env.CDB_DEVFRAME_BENCHMARK_OUTPUT === undefined ? 90_000 : 600_000) : 600_000); diff --git a/tests/e2e/devframe-notifications.test.ts b/tests/e2e/devframe-notifications.test.ts new file mode 100644 index 0000000..1f4e2c2 --- /dev/null +++ b/tests/e2e/devframe-notifications.test.ts @@ -0,0 +1,64 @@ +import { mkdtemp, rm } from 'node:fs/promises'; +import { createServer } from 'node:http'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +import { createUi } from '@devframes/hub-ui'; +import { initHub } from '@devframes/hub/initiate'; +import { chromium } from 'playwright'; +import { expect, it } from 'vitest'; + +it.each(['embedded', 'standalone'] as const)('updates and removes notifications in the patched %s browser assets', async (mode) => { + expect.assertions(15); + const directory = await mkdtemp(join(tmpdir(), 'cdb-hub-notifications-')); + const server = createServer(); + let origin = ''; + let full = false; + let reads = 0; + const hub = initHub({ base: '/hub/', cwd: directory, origin: () => origin, auth: false, server, ui: createUi(), configure(context) { + context.rpc.register({ name: 'devframes:plugin:messages:list', type: 'query', handler: (since?: number | null) => { + reads += 1; + return context.messages.listSince(full ? null : since); + } }); + } }); + server.on('request', (request, response) => { + if (request.url === '/app') { + response.setHeader('content-type', 'text/html'); + response.end('

Public CDB notification host

'); + } else hub.nodeMiddleware(request, response); + }); + const browser = await chromium.launch({ channel: 'chromium', headless: true }); + try { + await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)); + const address = server.address(); + if (address === null || typeof address === 'string') throw new Error('Missing fixture address.'); + origin = `http://127.0.0.1:${address.port}`; + await hub.ready; + const context = await hub.context; + const page = await browser.newPage(); + await page.goto(`${origin}${mode === 'embedded' ? '/app' : '/hub/'}`); + await expect.poll(() => reads).toBeGreaterThan(0); + for (const reconcileFull of [false, true]) { + full = reconcileFull; + const message = await context.messages.info('CDB approval fixture', { notify: true, autoDismiss: false, description: 'One approved tab' }); + await expect.poll(async () => page.getByText('One approved tab', { exact: true }).count()).toBe(1); + await message.update({ description: 'Two approved tabs' }); + await expect.poll(async () => page.getByText('Two approved tabs', { exact: true }).count()).toBe(1); + await page.locator('.bg-toast-glass').getByRole('button').click(); + await expect.poll(async () => page.getByText('CDB approval fixture', { exact: true }).count()).toBe(0); + const previousReads = reads; + await context.messages.info('Feed refresh', { notify: false }); + await expect.poll(() => reads).toBeGreaterThan(previousReads); + expect(await page.getByText('CDB approval fixture', { exact: true }).count()).toBe(0); + await message.update({ description: 'Three approved tabs' }); + await expect.poll(async () => page.getByText('Three approved tabs', { exact: true }).count()).toBe(1); + await message.dismiss(); + await expect.poll(async () => page.getByText('CDB approval fixture', { exact: true }).count()).toBe(0); + } + } finally { + await browser.close(); + await hub.close(); + await new Promise(resolve => server.close(() => resolve())); + await rm(directory, { recursive: true, force: true }); + } +}, 30_000); diff --git a/tests/e2e/fixtures/workflow-benchmark.ts b/tests/e2e/fixtures/workflow-benchmark.ts index f2b2d45..f77aeed 100644 --- a/tests/e2e/fixtures/workflow-benchmark.ts +++ b/tests/e2e/fixtures/workflow-benchmark.ts @@ -6,59 +6,111 @@ import { writeFile } from 'node:fs/promises'; import { cpus, loadavg } from 'node:os'; import { styleText } from 'node:util'; -/** Measures the same public workflow over the embedding test's real provider transport. */ -export async function benchmarkAgentWorkflow(agent: Client, chromiumVersion: string, page: Page): Promise { - if (process.env.CDB_DEVFRAME_BENCHMARK_OUTPUT === undefined) return; - const samples = Number(process.env.CDB_DEVFRAME_BENCHMARK_SAMPLES ?? 30); - const initialLoad = loadavg(); +const referencePattern = /\[ref=(e\d+)\]/u; + +interface BenchmarkWorker { + chrome: { debugger: { sendCommand: (...parameters: unknown[]) => Promise } }; + cdbBenchmarkCommands: { method: string; milliseconds: number }[]; + restoreCdbBenchmarkCommands: () => void; +} + +/** Runs identical agent workflows over either real extension transport. */ +export async function benchmarkAgentWorkflow(agent: Client, chromiumVersion: string, page: Page, transport: 'devframe' | 'websocket' = 'devframe'): Promise { + const output = transport === 'devframe' ? process.env.CDB_DEVFRAME_BENCHMARK_OUTPUT : process.env.CDB_WEBSOCKET_BENCHMARK_OUTPUT; + if (output === undefined) return; + const samples = Number(process.env.CDB_WORKFLOW_BENCHMARK_SAMPLES ?? 30); if (!Number.isInteger(samples) || samples < 5 || samples > 50) throw new Error('Benchmark samples must be between 5 and 50.'); + const initialLoad = loadavg(); const actionMetrics: unknown[] = []; + const snapshotMetrics: unknown[] = []; const diagnostics = channel('cdb.mcp.action'); + const snapshotDiagnostics = channel('cdb.mcp.snapshot'); + const collectSnapshot = (measurement: unknown): void => { + snapshotMetrics.push(measurement); + }; const collect = (measurement: unknown): void => { actionMetrics.push(measurement); }; + const measurements: { selector: string; name: string; milliseconds: number; argumentsCharacters: number; responseCharacters: number; error: boolean }[] = []; + const summaries: Record = {}; + const percentile95 = (values: number[]): number | undefined => values.sort((left, right) => left - right)[Math.ceil(values.length * 0.95) - 1]; + const worker = page.context().serviceWorkers()[0]; + if (worker === undefined) throw new Error('The benchmark extension worker is unavailable.'); + await worker.evaluate(() => { + const workerGlobal = globalThis as unknown as BenchmarkWorker; + const sendCommand = workerGlobal.chrome.debugger.sendCommand; + workerGlobal.cdbBenchmarkCommands = []; + workerGlobal.restoreCdbBenchmarkCommands = () => { + workerGlobal.chrome.debugger.sendCommand = sendCommand; + }; + workerGlobal.chrome.debugger.sendCommand = async (...parameters) => { + const started = performance.now(); + try { + return await sendCommand.apply(workerGlobal.chrome.debugger, parameters); + } finally { + workerGlobal.cdbBenchmarkCommands.push({ method: String(parameters[1]), milliseconds: performance.now() - started }); + } + }; + }); diagnostics.subscribe(collect); + snapshotDiagnostics.subscribe(collectSnapshot); try { - const measurements: { name: string; milliseconds: number; argumentsCharacters: number; responseCharacters: number }[] = []; - const locator = (role: string, value: string): { role: string; name: { match: 'exact'; value: string } } => ({ role, name: { match: 'exact', value } }); - const fill = { targetRef: 't1', locator: locator('textbox', 'Deep value'), text: 'Benchmark value' }; - const click = { targetRef: 't1', locator: locator('button', 'Save deep value') }; - const batch = { targetRef: 't1', observe: true, actions: [{ action: 'fill', ...fill }, { action: 'click', ...click }].map(({ targetRef: _targetRef, ...action }) => action) }; - async function call(name: string, argumentsValue: Record, record = true, blocked = false): Promise { - const started = performance.now(); - const result = await agent.callTool({ name, arguments: argumentsValue }); - if (blocked ? !JSON.stringify(result).includes('MCP_ELEMENT_COVERED') : result.isError) throw new Error(`Benchmark ${name} failed: ${JSON.stringify(result)}`); - if (record) measurements.push({ name: blocked ? `${name}:blocked` : name, milliseconds: performance.now() - started, argumentsCharacters: JSON.stringify(argumentsValue).length, responseCharacters: JSON.stringify(result).length }); - } - await call('browser.fill', fill, false); - await call('browser.click', click, false); - await call('browser.batch', batch, false); - for (let sample = 0; sample < samples; sample += 1) { - await call('browser.fill', fill); - await call('browser.click', click); - await call('browser.snapshot', { targetRef: 't1' }); - await call('browser.batch', batch); - } - await page.locator('#toggle-overlay').click(); - try { - for (let sample = 0; sample < samples; sample += 1) await call('browser.click', click, true, true); - } finally { - await page.locator('#parent-overlay').evaluate(element => element.remove()); - } - const workflows = Array.from({ length: samples }, (_unused, index) => measurements.slice(index * 4, index * 4 + 3).reduce((total, measurement) => total + measurement.milliseconds, 0)); - const percentile95 = (values: number[]): number | undefined => values.sort((left, right) => left - right)[Math.ceil(values.length * 0.95) - 1]; - const summaries = Object.fromEntries(['browser.fill', 'browser.click', 'browser.snapshot', 'browser.batch', 'browser.click:blocked'].map(name => [name, { p95Milliseconds: percentile95(measurements.filter(measurement => measurement.name === name).map(measurement => measurement.milliseconds)) }])); - - const catalogue = await agent.listTools(); - const report = { environment: { chromiumVersion, node: process.version, architecture: process.arch, operatingSystem: process.platform, processor: cpus()[0]?.model, initialLoad, finalLoad: loadavg() }, transport: 'MCP → broker → authenticated Devframe RPC → Chrome extension → chrome.debugger', fixture: 'normal, closed shadow roots, three cross-origin frame levels', samples, individualWorkflowP95Milliseconds: percentile95(workflows), summaries, measurements, actionMetrics, catalogueCharacters: JSON.stringify(catalogue).length, tokenizerEstimate: 'Serialized JSON characters / 4; not measured tokenizer usage' }; - await writeFile(process.env.CDB_DEVFRAME_BENCHMARK_OUTPUT, `${JSON.stringify(report, null, 2)}\n`); - console.info(styleText('cyan', '📊 [devframe-benchmark]'), summaries); - if (process.env.CDB_DEVFRAME_PERFORMANCE_GATE === '1') { - for (const [name, maximumMilliseconds] of [['browser.fill', 500], ['browser.click', 500], ['browser.snapshot', 1_000]] as const) { - if ((summaries[name]?.p95Milliseconds ?? Infinity) > maximumMilliseconds) throw new Error(`${name} p95 exceeded ${maximumMilliseconds} ms.`); + for (const selector of ['ref', 'locator'] as const) { + async function call(name: string, argumentsValue: Record, record = true, blocked = false): Promise { + const started = performance.now(); + const result = await agent.callTool({ name, arguments: argumentsValue }); + const text = result.content.filter(item => item.type === 'text').map(item => item.text).join('\n'); + if (record) measurements.push({ selector, name: blocked ? `${name}:blocked` : name, milliseconds: performance.now() - started, argumentsCharacters: JSON.stringify(argumentsValue).length, responseCharacters: JSON.stringify(result).length, error: result.isError === true }); + if (blocked ? !text.includes('MCP_ELEMENT_COVERED') : result.isError) throw new Error(`Benchmark ${name} failed: ${text}`); + return text; } + let snapshot = await call('browser.snapshot', { targetRef: 't1' }, false); + const locate = (role: string, name: string): Record => { + if (selector === 'locator') return { locator: { role, name: { match: 'exact', value: name } } }; + const reference = snapshot.split('\n').find(line => line.includes(`${role} "${name}"`))?.match(referencePattern)?.[1]; + if (reference === undefined) throw new Error(`Missing benchmark reference for ${name}.`); + return { ref: reference }; + }; + const actions = (): { fill: Record; click: Record } => ({ + fill: { ...locate('textbox', 'Deep value'), text: 'Benchmark value' }, + click: { ...locate('button', 'Save deep value') }, + }); + const workflows: number[] = []; + for (let sample = -5; sample < samples; sample += 1) { + const started = performance.now(); + const individual = actions(); + await call('browser.fill', { targetRef: 't1', ...individual.fill }, sample >= 0); + await call('browser.click', { targetRef: 't1', ...individual.click }, sample >= 0); + snapshot = await call('browser.snapshot', { targetRef: 't1' }, sample >= 0); + if (sample >= 0) workflows.push(performance.now() - started); + const next = actions(); + const batch = { targetRef: 't1', observe: true, actions: [{ action: 'fill', ...next.fill }, { action: 'click', ...next.click }] }; + snapshot = (JSON.parse(await call('browser.batch', batch, sample >= 0)) as { observation: string }).observation; + } + await page.locator('#toggle-overlay').click(); + try { + for (let sample = -5; sample < samples; sample += 1) await call('browser.click', { targetRef: 't1', ...actions().click }, sample >= 0, true); + } finally { + await page.locator('#parent-overlay').evaluate(element => element.remove()); + } + for (const name of ['browser.fill', 'browser.click', 'browser.snapshot', 'browser.batch', 'browser.click:blocked']) summaries[`${selector}:${name}`] = { p95Milliseconds: percentile95(measurements.filter(measurement => measurement.selector === selector && measurement.name === name).map(measurement => measurement.milliseconds)) }; + summaries[`${selector}:individual-workflow`] = { p95Milliseconds: percentile95(workflows) }; } } finally { diagnostics.unsubscribe(collect); + snapshotDiagnostics.unsubscribe(collectSnapshot); + const chromeCommands = await worker.evaluate(() => { + const workerGlobal = globalThis as unknown as BenchmarkWorker; + workerGlobal.restoreCdbBenchmarkCommands(); + return workerGlobal.cdbBenchmarkCommands; + }).catch(() => undefined); + const report = { environment: { chromiumVersion, node: process.version, architecture: process.arch, operatingSystem: process.platform, processor: cpus()[0]?.model, initialLoad, finalLoad: loadavg() }, transport, fixture: 'normal, closed shadow roots, three cross-origin frame levels', warmups: 5, samples, summaries, measurements, actionMetrics, snapshotMetrics, chromeCommands, tokenizerEstimate: 'Serialized JSON characters / 4; not measured tokenizer usage' }; + await writeFile(output, `${JSON.stringify(report, null, 2)}\n`); + console.info(styleText('cyan', '📊 [workflow-benchmark]'), transport, summaries); + } + if (process.env.CDB_WORKFLOW_PERFORMANCE_GATE === '1') { + for (const selector of ['ref', 'locator']) for (const [name, maximumMilliseconds] of [['browser.fill', 500], ['browser.click', 500], ['browser.snapshot', 1_000]] as const) { + if ((summaries[`${selector}:${name}`]?.p95Milliseconds ?? Infinity) > maximumMilliseconds) throw new Error(`${selector}:${name} p95 exceeded ${maximumMilliseconds} ms.`); + } } } diff --git a/tests/e2e/native-agent-benchmark.test.ts b/tests/e2e/native-agent-benchmark.test.ts index 21942c3..daba7a2 100644 --- a/tests/e2e/native-agent-benchmark.test.ts +++ b/tests/e2e/native-agent-benchmark.test.ts @@ -8,6 +8,7 @@ import { afterEach, expect, it } from 'vitest'; import { deepDomProfiles } from './fixtures/deep-dom-page.js'; import { createNativeMcpHarness, toolText } from './fixtures/native-mcp-harness.js'; +import { benchmarkAgentWorkflow } from './fixtures/workflow-benchmark.js'; interface CallMeasurement { readonly argumentCharacters: number; @@ -195,3 +196,10 @@ it.runIf(process.env.CDB_NATIVE_BENCHMARK === '1')('measures the public native a expect(process.env.CDB_NATIVE_PERFORMANCE_GATE === '1' ? action95 ?? Infinity : 0).toBeLessThanOrEqual(500); expect(process.env.CDB_NATIVE_PERFORMANCE_GATE === '1' ? snapshot95 ?? Infinity : 0).toBeLessThanOrEqual(1_000); }, 180_000); + +it.runIf(process.env.CDB_WEBSOCKET_BENCHMARK_OUTPUT !== undefined)('measures the same workflow over the WebSocket provider transport', async () => { + expect.assertions(1); + harness = await createNativeMcpHarness({ profile: 'normal', frames: 'cross-origin', shadow: 'closed' }); + await benchmarkAgentWorkflow(harness.mcpClient, harness.context.browser()?.version() ?? 'unavailable', harness.page, 'websocket'); + expect(await harness.page.getByRole('status').textContent()).toBe('Saved: Benchmark value'); +}, 600_000); From c51630a2df439927378f09930f857926a66208cc Mon Sep 17 00:00:00 2001 From: dvcolomban Date: Sat, 12 Sep 2026 14:49:26 +0200 Subject: [PATCH 5/6] test: move browser diagnostics to manual CI --- .github/workflows/benchmark.yml | 79 + docs/benchmarks/2026-09-08-latency.csv | 1201 --------- docs/benchmarks/2026-09-08-latency.md | 2 +- docs/benchmarks/2026-09-09-aligned.csv | 2380 ----------------- docs/benchmarks/2026-09-09-aligned.md | 6 +- .../2026-09-09-followup-lifecycle.csv | 25 - docs/benchmarks/2026-09-09-latency.csv | 241 -- docs/benchmarks/2026-09-09-lifecycle.csv | 221 -- docs/benchmarks/2026-09-09-lifecycle.md | 4 +- tests/e2e/README.md | 12 +- tests/e2e/fixtures/report-validation.ts | 40 + tests/e2e/fixtures/workflow-benchmark.ts | 16 +- tests/e2e/workflow-report.test.ts | 49 + 13 files changed, 190 insertions(+), 4086 deletions(-) create mode 100644 .github/workflows/benchmark.yml delete mode 100644 docs/benchmarks/2026-09-08-latency.csv delete mode 100644 docs/benchmarks/2026-09-09-aligned.csv delete mode 100644 docs/benchmarks/2026-09-09-followup-lifecycle.csv delete mode 100644 docs/benchmarks/2026-09-09-latency.csv delete mode 100644 docs/benchmarks/2026-09-09-lifecycle.csv create mode 100644 tests/e2e/fixtures/report-validation.ts create mode 100644 tests/e2e/workflow-report.test.ts diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml new file mode 100644 index 0000000..b59f2a7 --- /dev/null +++ b/.github/workflows/benchmark.yml @@ -0,0 +1,79 @@ +name: Browser diagnostics + +on: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: browser-diagnostics-${{ github.ref }} + cancel-in-progress: false + +jobs: + diagnostics: + name: Sequential browser measurements + runs-on: ubuntu-latest + timeout-minutes: 45 + env: + NODE_OPTIONS: --max-old-space-size=4096 + CDB_WORKFLOW_BENCHMARK_SAMPLES: '30' + CDB_WORKFLOW_PERFORMANCE_GATE: '1' + steps: + - name: 📥 Checkout selected revision + uses: actions/checkout@v7 + + - name: 🏗️ Set up workspace + uses: ./.github/actions/setup-workspace + with: + install-chromium: 'true' + + - name: 🔨 Build before measuring + run: pnpm exec turbo run build --concurrency=1 + + - name: 🖥️ Record runner environment + run: | + git rev-parse HEAD + node --version + pnpm exec playwright --version + uname -a + lscpu + uptime + echo 'Linux CI baseline. Compare only matching fixtures, browser versions and runner environments.' >> "$GITHUB_STEP_SUMMARY" + git rev-parse HEAD >> "$GITHUB_STEP_SUMMARY" + + # Keep transports and lifecycle work sequential, including after a failed gate. + - name: 📊 Measure WebSocket workflows + id: websocket + continue-on-error: true + env: + CDB_WEBSOCKET_BENCHMARK_OUTPUT: ${{ runner.temp }}/cdb-websocket.json + run: pnpm exec vitest run --project extension-e2e --maxWorkers=1 tests/e2e/native-agent-benchmark.test.ts + + - name: 📊 Measure Devframe workflows + id: devframe + if: ${{ !cancelled() && steps.websocket.outcome != 'skipped' }} + continue-on-error: true + env: + CDB_DEVFRAME_BENCHMARK_OUTPUT: ${{ runner.temp }}/cdb-devframe.json + run: pnpm exec vitest run --project extension-e2e --maxWorkers=1 tests/e2e/devframe-native.test.ts + + - name: 🔄 Exercise 30 lifecycle cycles + id: lifecycle + if: ${{ !cancelled() && steps.websocket.outcome != 'skipped' }} + continue-on-error: true + env: + CDB_LIFECYCLE_DIAGNOSTICS_OUTPUT: ${{ runner.temp }}/cdb-lifecycle.json + run: pnpm exec vitest run --project extension-e2e --maxWorkers=1 tests/e2e/devframe-native.test.ts + + - name: 📝 Report complete and partial results + if: always() + run: node tests/e2e/fixtures/report-validation.ts "$RUNNER_TEMP/cdb-websocket.json" "$RUNNER_TEMP/cdb-devframe.json" "$RUNNER_TEMP/cdb-lifecycle.json" + + - name: ✅ Require all diagnostic suites to pass + if: ${{ !cancelled() }} + env: + WEBSOCKET_OUTCOME: ${{ steps.websocket.outcome }} + DEVFRAME_OUTCOME: ${{ steps.devframe.outcome }} + LIFECYCLE_OUTCOME: ${{ steps.lifecycle.outcome }} + run: test "$WEBSOCKET_OUTCOME/$DEVFRAME_OUTCOME/$LIFECYCLE_OUTCOME" = success/success/success diff --git a/docs/benchmarks/2026-09-08-latency.csv b/docs/benchmarks/2026-09-08-latency.csv deleted file mode 100644 index bc6cb74..0000000 --- a/docs/benchmarks/2026-09-08-latency.csv +++ /dev/null @@ -1,1201 +0,0 @@ -run,tool,milliseconds,argument_characters,response_characters -devframe-before,browser.fill,665.914,118,56 -devframe-before,browser.click,642.185,97,57 -devframe-before,browser.snapshot,584.808,18,452 -devframe-before,browser.batch,1858.653,261,605 -devframe-before,browser.fill,655.935,118,56 -devframe-before,browser.click,667.392,97,57 -devframe-before,browser.snapshot,581.378,18,456 -devframe-before,browser.batch,1834.693,261,605 -devframe-before,browser.fill,650.006,118,56 -devframe-before,browser.click,651.773,97,57 -devframe-before,browser.snapshot,606.705,18,456 -devframe-before,browser.batch,1874.813,261,605 -devframe-before,browser.fill,650.686,118,56 -devframe-before,browser.click,658.995,97,57 -devframe-before,browser.snapshot,565.023,18,456 -devframe-before,browser.batch,1868.965,261,605 -devframe-before,browser.fill,701.848,118,56 -devframe-before,browser.click,784.83,97,57 -devframe-before,browser.snapshot,628.898,18,456 -devframe-before,browser.batch,2370.466,261,605 -devframe-before,browser.fill,1422.107,118,56 -devframe-before,browser.click,1199.71,97,57 -devframe-before,browser.snapshot,1102.314,18,456 -devframe-before,browser.batch,2795.679,261,605 -devframe-before,browser.fill,740.032,118,56 -devframe-before,browser.click,651.123,97,57 -devframe-before,browser.snapshot,551.803,18,456 -devframe-before,browser.batch,1856.037,261,605 -devframe-before,browser.fill,647.157,118,56 -devframe-before,browser.click,665.51,97,57 -devframe-before,browser.snapshot,607.204,18,456 -devframe-before,browser.batch,1927.29,261,605 -devframe-before,browser.fill,682.502,118,56 -devframe-before,browser.click,658.755,97,57 -devframe-before,browser.snapshot,591.102,18,456 -devframe-before,browser.batch,1914.958,261,605 -devframe-before,browser.fill,719.497,118,56 -devframe-before,browser.click,701.231,97,57 -devframe-before,browser.snapshot,930.418,18,457 -devframe-before,browser.batch,1855.581,261,610 -devframe-before,browser.fill,652.525,118,56 -devframe-before,browser.click,650.04,97,57 -devframe-before,browser.snapshot,558.767,18,461 -devframe-before,browser.batch,1807.236,261,610 -devframe-before,browser.fill,639.931,118,56 -devframe-before,browser.click,635.772,97,57 -devframe-before,browser.snapshot,574.127,18,461 -devframe-before,browser.batch,1808.154,261,610 -devframe-before,browser.fill,649.482,118,56 -devframe-before,browser.click,668.471,97,57 -devframe-before,browser.snapshot,565.056,18,461 -devframe-before,browser.batch,1827.637,261,610 -devframe-before,browser.fill,709.049,118,56 -devframe-before,browser.click,812.586,97,57 -devframe-before,browser.snapshot,692.224,18,461 -devframe-before,browser.batch,1882.954,261,610 -devframe-before,browser.fill,670.176,118,56 -devframe-before,browser.click,674.767,97,57 -devframe-before,browser.snapshot,579.444,18,461 -devframe-before,browser.batch,1798.696,261,610 -devframe-before,browser.fill,650.399,118,56 -devframe-before,browser.click,649.178,97,57 -devframe-before,browser.snapshot,576.029,18,461 -devframe-before,browser.batch,1800.113,261,610 -devframe-before,browser.fill,661.626,118,56 -devframe-before,browser.click,635.029,97,57 -devframe-before,browser.snapshot,570.537,18,461 -devframe-before,browser.batch,1822.384,261,610 -devframe-before,browser.fill,636.096,118,56 -devframe-before,browser.click,636.827,97,57 -devframe-before,browser.snapshot,579.231,18,461 -devframe-before,browser.batch,1804.186,261,610 -devframe-before,browser.fill,665.651,118,56 -devframe-before,browser.click,644.665,97,57 -devframe-before,browser.snapshot,557.239,18,461 -devframe-before,browser.batch,1907.898,261,610 -devframe-before,browser.fill,659.914,118,56 -devframe-before,browser.click,668.325,97,57 -devframe-before,browser.snapshot,568.779,18,461 -devframe-before,browser.batch,1811.382,261,610 -devframe-before,browser.fill,635.574,118,56 -devframe-before,browser.click,648.259,97,57 -devframe-before,browser.snapshot,599.448,18,461 -devframe-before,browser.batch,1817.285,261,610 -devframe-before,browser.fill,650.087,118,56 -devframe-before,browser.click,702.499,97,57 -devframe-before,browser.snapshot,556.187,18,461 -devframe-before,browser.batch,1836.006,261,610 -devframe-before,browser.fill,645.846,118,56 -devframe-before,browser.click,681.035,97,57 -devframe-before,browser.snapshot,617.855,18,461 -devframe-before,browser.batch,2031.881,261,610 -devframe-before,browser.fill,793.003,118,56 -devframe-before,browser.click,717.571,97,57 -devframe-before,browser.snapshot,760.318,18,461 -devframe-before,browser.batch,2161.035,261,610 -devframe-before,browser.fill,779.759,118,56 -devframe-before,browser.click,706.716,97,57 -devframe-before,browser.snapshot,605.787,18,461 -devframe-before,browser.batch,1877.26,261,610 -devframe-before,browser.fill,643.858,118,56 -devframe-before,browser.click,630.375,97,57 -devframe-before,browser.snapshot,576.75,18,461 -devframe-before,browser.batch,1807.053,261,610 -devframe-before,browser.fill,648.588,118,56 -devframe-before,browser.click,635.006,97,57 -devframe-before,browser.snapshot,554.175,18,461 -devframe-before,browser.batch,1791.192,261,610 -devframe-before,browser.fill,649.989,118,56 -devframe-before,browser.click,641.189,97,57 -devframe-before,browser.snapshot,565.795,18,461 -devframe-before,browser.batch,1798.55,261,610 -devframe-before,browser.fill,743.351,118,56 -devframe-before,browser.click,647.954,97,57 -devframe-before,browser.snapshot,562.255,18,461 -devframe-before,browser.batch,1803.327,261,610 -devframe-before,browser.fill,637.416,118,56 -devframe-before,browser.click,674.908,97,57 -devframe-before,browser.snapshot,605.911,18,461 -devframe-before,browser.batch,1978.871,261,610 -devframe-final,browser.fill,365.665,118,56 -devframe-final,browser.click,426.405,97,57 -devframe-final,browser.snapshot,856.457,18,452 -devframe-final,browser.batch,1174.22,261,605 -devframe-final,browser.fill,412.736,118,56 -devframe-final,browser.click,415.409,97,57 -devframe-final,browser.snapshot,683.858,18,456 -devframe-final,browser.batch,1337.171,261,605 -devframe-final,browser.fill,418.923,118,56 -devframe-final,browser.click,424.173,97,57 -devframe-final,browser.snapshot,610.804,18,456 -devframe-final,browser.batch,1314.38,261,605 -devframe-final,browser.fill,374.73,118,56 -devframe-final,browser.click,400.033,97,57 -devframe-final,browser.snapshot,633.617,18,456 -devframe-final,browser.batch,1277.608,261,605 -devframe-final,browser.fill,363.072,118,56 -devframe-final,browser.click,353.837,97,57 -devframe-final,browser.snapshot,641.252,18,456 -devframe-final,browser.batch,1223.789,261,605 -devframe-final,browser.fill,378.88,118,56 -devframe-final,browser.click,335.317,97,57 -devframe-final,browser.snapshot,745.524,18,456 -devframe-final,browser.batch,1229.394,261,605 -devframe-final,browser.fill,320.707,118,56 -devframe-final,browser.click,1033.579,97,57 -devframe-final,browser.snapshot,1693.964,18,456 -devframe-final,browser.batch,1187.673,261,605 -devframe-final,browser.fill,433.147,118,56 -devframe-final,browser.click,382.342,97,57 -devframe-final,browser.snapshot,592.484,18,456 -devframe-final,browser.batch,1417.023,261,605 -devframe-final,browser.fill,351.966,118,56 -devframe-final,browser.click,396.387,97,57 -devframe-final,browser.snapshot,675.106,18,456 -devframe-final,browser.batch,1299.463,261,605 -devframe-final,browser.fill,386.744,118,56 -devframe-final,browser.click,401.219,97,57 -devframe-final,browser.snapshot,634.678,18,457 -devframe-final,browser.batch,1423.854,261,610 -devframe-final,browser.fill,369.354,118,56 -devframe-final,browser.click,418.955,97,57 -devframe-final,browser.snapshot,615.119,18,461 -devframe-final,browser.batch,1324.838,261,610 -devframe-final,browser.fill,410.813,118,56 -devframe-final,browser.click,363.263,97,57 -devframe-final,browser.snapshot,667.424,18,461 -devframe-final,browser.batch,1227.627,261,610 -devframe-final,browser.fill,385.919,118,56 -devframe-final,browser.click,414.459,97,57 -devframe-final,browser.snapshot,648.423,18,461 -devframe-final,browser.batch,1231.09,261,610 -devframe-final,browser.fill,366.375,118,56 -devframe-final,browser.click,311.194,97,57 -devframe-final,browser.snapshot,645.656,18,461 -devframe-final,browser.batch,1201.598,261,610 -devframe-final,browser.fill,365.5,118,56 -devframe-final,browser.click,395.054,97,57 -devframe-final,browser.snapshot,591.751,18,461 -devframe-final,browser.batch,1332.65,261,610 -devframe-final,browser.fill,300.009,118,56 -devframe-final,browser.click,317.512,97,57 -devframe-final,browser.snapshot,934.723,18,461 -devframe-final,browser.batch,1895.619,261,610 -devframe-final,browser.fill,340.665,118,56 -devframe-final,browser.click,293.87,97,57 -devframe-final,browser.snapshot,731.419,18,461 -devframe-final,browser.batch,1222.864,261,610 -devframe-final,browser.fill,397.495,118,56 -devframe-final,browser.click,429.829,97,57 -devframe-final,browser.snapshot,660.538,18,461 -devframe-final,browser.batch,1381.368,261,610 -devframe-final,browser.fill,407.527,118,56 -devframe-final,browser.click,329.529,97,57 -devframe-final,browser.snapshot,738.213,18,461 -devframe-final,browser.batch,1184.988,261,610 -devframe-final,browser.fill,376.748,118,56 -devframe-final,browser.click,387.433,97,57 -devframe-final,browser.snapshot,598.81,18,461 -devframe-final,browser.batch,1331.361,261,610 -devframe-final,browser.fill,214.33,118,56 -devframe-final,browser.click,414.329,97,57 -devframe-final,browser.snapshot,671.419,18,461 -devframe-final,browser.batch,1306.107,261,610 -devframe-final,browser.fill,418.951,118,56 -devframe-final,browser.click,413.182,97,57 -devframe-final,browser.snapshot,625.544,18,461 -devframe-final,browser.batch,1255.71,261,610 -devframe-final,browser.fill,325.483,118,56 -devframe-final,browser.click,407.23,97,57 -devframe-final,browser.snapshot,578.919,18,461 -devframe-final,browser.batch,1973.472,261,610 -devframe-final,browser.fill,807.452,118,56 -devframe-final,browser.click,445.087,97,57 -devframe-final,browser.snapshot,872.154,18,461 -devframe-final,browser.batch,1235.236,261,610 -devframe-final,browser.fill,354.445,118,56 -devframe-final,browser.click,271.557,97,57 -devframe-final,browser.snapshot,706.537,18,461 -devframe-final,browser.batch,1338.806,261,610 -devframe-final,browser.fill,290.277,118,56 -devframe-final,browser.click,298.019,97,57 -devframe-final,browser.snapshot,692.617,18,461 -devframe-final,browser.batch,1262.667,261,610 -devframe-final,browser.fill,429.212,118,56 -devframe-final,browser.click,360.508,97,57 -devframe-final,browser.snapshot,638.915,18,461 -devframe-final,browser.batch,1230.06,261,610 -devframe-final,browser.fill,379.49,118,56 -devframe-final,browser.click,387.013,97,57 -devframe-final,browser.snapshot,677.798,18,461 -devframe-final,browser.batch,1190.268,261,610 -devframe-final,browser.fill,340.557,118,56 -devframe-final,browser.click,338.321,97,57 -devframe-final,browser.snapshot,905.419,18,461 -devframe-final,browser.batch,1827.455,261,610 -devframe-final,browser.fill,364.02,118,56 -devframe-final,browser.click,425.151,97,57 -devframe-final,browser.snapshot,641.693,18,461 -devframe-final,browser.batch,1304.13,261,610 -devframe-final,browser.click:blocked,2063.87,97,197 -devframe-final,browser.click:blocked,2001.917,97,197 -devframe-final,browser.click:blocked,2002.676,97,197 -devframe-final,browser.click:blocked,2267.219,97,197 -devframe-final,browser.click:blocked,2026.696,97,197 -devframe-final,browser.click:blocked,2022.197,97,197 -devframe-final,browser.click:blocked,2003.358,97,197 -devframe-final,browser.click:blocked,2000.568,97,197 -devframe-final,browser.click:blocked,2002.436,97,197 -devframe-final,browser.click:blocked,2002.609,97,197 -devframe-final,browser.click:blocked,2002.927,97,197 -devframe-final,browser.click:blocked,2010.17,97,197 -devframe-final,browser.click:blocked,2010.846,97,197 -devframe-final,browser.click:blocked,2130.67,97,197 -devframe-final,browser.click:blocked,2001.124,97,197 -devframe-final,browser.click:blocked,2022.038,97,197 -devframe-final,browser.click:blocked,2002.463,97,197 -devframe-final,browser.click:blocked,2001.258,97,197 -devframe-final,browser.click:blocked,1999.786,97,197 -devframe-final,browser.click:blocked,2001.313,97,197 -devframe-final,browser.click:blocked,2003.145,97,197 -devframe-final,browser.click:blocked,2034.358,97,197 -devframe-final,browser.click:blocked,2003.095,97,197 -devframe-final,browser.click:blocked,2004.31,97,197 -devframe-final,browser.click:blocked,2101.897,97,197 -devframe-final,browser.click:blocked,2002.523,97,197 -devframe-final,browser.click:blocked,2000.263,97,197 -devframe-final,browser.click:blocked,2002.981,97,197 -devframe-final,browser.click:blocked,2047.744,97,197 -devframe-final,browser.click:blocked,2000.552,97,197 -websocket-before,browser.snapshot,669.488,18,461 -websocket-before,browser.fill,107.358,63,56 -websocket-before,browser.click,98.717,30,57 -websocket-before,browser.snapshot,648.694,18,465 -websocket-before,browser.fill,104.245,64,56 -websocket-before,browser.click,101.4,30,57 -websocket-before,browser.snapshot,634.346,18,465 -websocket-before,browser.fill,107.394,64,56 -websocket-before,browser.click,95.947,30,57 -websocket-before,browser.snapshot,645.912,18,465 -websocket-before,browser.fill,99.365,64,56 -websocket-before,browser.click,95.047,30,57 -websocket-before,browser.snapshot,628.92,18,465 -websocket-before,browser.fill,103.562,64,56 -websocket-before,browser.click,93.565,30,57 -websocket-before,browser.snapshot,586.077,18,465 -websocket-before,browser.fill,102.316,64,56 -websocket-before,browser.click,91.576,30,57 -websocket-before,browser.snapshot,587.977,18,465 -websocket-before,browser.fill,99.923,64,56 -websocket-before,browser.click,93.552,30,57 -websocket-before,browser.snapshot,591.653,18,465 -websocket-before,browser.fill,104.3,64,56 -websocket-before,browser.click,91.37,30,57 -websocket-before,browser.snapshot,586.024,18,465 -websocket-before,browser.fill,97.763,64,56 -websocket-before,browser.click,91.134,30,57 -websocket-before,browser.snapshot,602.56,18,465 -websocket-before,browser.fill,100.16,64,56 -websocket-before,browser.click,92.146,30,57 -websocket-before,browser.snapshot,617.31,18,465 -websocket-before,browser.fill,99.967,64,56 -websocket-before,browser.click,92.836,30,57 -websocket-before,browser.snapshot,641.543,18,465 -websocket-before,browser.fill,103.994,64,56 -websocket-before,browser.click,93.359,30,57 -websocket-before,browser.snapshot,685.116,18,465 -websocket-before,browser.fill,100.713,64,56 -websocket-before,browser.click,97.235,30,57 -websocket-before,browser.snapshot,736.993,18,465 -websocket-before,browser.fill,174.623,64,56 -websocket-before,browser.click,107.565,30,57 -websocket-before,browser.snapshot,664.391,18,465 -websocket-before,browser.fill,123.743,64,56 -websocket-before,browser.click,126.464,30,57 -websocket-before,browser.snapshot,651.708,18,465 -websocket-before,browser.fill,121.566,64,56 -websocket-before,browser.click,104.143,30,57 -websocket-before,browser.snapshot,652.87,18,465 -websocket-before,browser.fill,108.478,64,56 -websocket-before,browser.click,105.452,30,57 -websocket-before,browser.snapshot,664.924,18,465 -websocket-before,browser.fill,108.928,64,56 -websocket-before,browser.click,106.008,30,57 -websocket-before,browser.snapshot,644.588,18,466 -websocket-before,browser.fill,104.343,64,56 -websocket-before,browser.click,91.799,31,57 -websocket-before,browser.snapshot,578.418,18,470 -websocket-before,browser.fill,107.828,65,56 -websocket-before,browser.click,97.319,31,57 -websocket-before,browser.snapshot,588.974,18,470 -websocket-before,browser.fill,98.441,65,56 -websocket-before,browser.click,89.004,31,57 -websocket-before,browser.snapshot,579.904,18,470 -websocket-before,browser.fill,100.574,65,56 -websocket-before,browser.click,88.702,31,57 -websocket-before,browser.snapshot,582.565,18,470 -websocket-before,browser.fill,108.908,65,56 -websocket-before,browser.click,94.634,31,57 -websocket-before,browser.snapshot,577.285,18,470 -websocket-before,browser.fill,98.635,65,56 -websocket-before,browser.click,90.2,31,57 -websocket-before,browser.snapshot,585.437,18,470 -websocket-before,browser.fill,100.935,65,56 -websocket-before,browser.click,89.988,31,57 -websocket-before,browser.snapshot,586.583,18,470 -websocket-before,browser.fill,100.97,65,56 -websocket-before,browser.click,91.652,31,57 -websocket-before,browser.snapshot,594.287,18,470 -websocket-before,browser.fill,101.197,65,56 -websocket-before,browser.click,92.215,31,57 -websocket-before,browser.snapshot,614.849,18,470 -websocket-before,browser.fill,98.747,65,56 -websocket-before,browser.click,89.656,31,57 -websocket-before,browser.snapshot,618.94,18,470 -websocket-before,browser.fill,101.782,65,56 -websocket-before,browser.click,92.166,31,57 -websocket-before,browser.snapshot,729.736,18,470 -websocket-before,browser.fill,121.325,65,56 -websocket-before,browser.click,112.373,31,57 -websocket-intermediate,browser.snapshot,608.936,18,461 -websocket-intermediate,browser.fill,103.588,63,56 -websocket-intermediate,browser.click,98.135,30,57 -websocket-intermediate,browser.snapshot,594.151,18,465 -websocket-intermediate,browser.fill,100.639,64,56 -websocket-intermediate,browser.click,89.73,30,57 -websocket-intermediate,browser.snapshot,603.3,18,465 -websocket-intermediate,browser.fill,106.552,64,56 -websocket-intermediate,browser.click,100.377,30,57 -websocket-intermediate,browser.snapshot,588.123,18,465 -websocket-intermediate,browser.fill,101.041,64,56 -websocket-intermediate,browser.click,92.876,30,57 -websocket-intermediate,browser.snapshot,590.964,18,465 -websocket-intermediate,browser.fill,98.536,64,56 -websocket-intermediate,browser.click,91.955,30,57 -websocket-intermediate,browser.snapshot,584.465,18,465 -websocket-intermediate,browser.fill,98.874,64,56 -websocket-intermediate,browser.click,91.799,30,57 -websocket-intermediate,browser.snapshot,587.74,18,465 -websocket-intermediate,browser.fill,96.755,64,56 -websocket-intermediate,browser.click,91.935,30,57 -websocket-intermediate,browser.snapshot,590.873,18,465 -websocket-intermediate,browser.fill,99.99,64,56 -websocket-intermediate,browser.click,90.84,30,57 -websocket-intermediate,browser.snapshot,584.226,18,465 -websocket-intermediate,browser.fill,98.558,64,56 -websocket-intermediate,browser.click,89.69,30,57 -websocket-intermediate,browser.snapshot,600.072,18,465 -websocket-intermediate,browser.fill,100.185,64,56 -websocket-intermediate,browser.click,88.941,30,57 -websocket-intermediate,browser.snapshot,589.91,18,465 -websocket-intermediate,browser.fill,97.86,64,56 -websocket-intermediate,browser.click,90.107,30,57 -websocket-intermediate,browser.snapshot,583.771,18,465 -websocket-intermediate,browser.fill,103.508,64,56 -websocket-intermediate,browser.click,99.841,30,57 -websocket-intermediate,browser.snapshot,594.442,18,465 -websocket-intermediate,browser.fill,106.894,64,56 -websocket-intermediate,browser.click,92.404,30,57 -websocket-intermediate,browser.snapshot,594.619,18,465 -websocket-intermediate,browser.fill,95.915,64,56 -websocket-intermediate,browser.click,89.827,30,57 -websocket-intermediate,browser.snapshot,598.961,18,465 -websocket-intermediate,browser.fill,128.0,64,56 -websocket-intermediate,browser.click,95.073,30,57 -websocket-intermediate,browser.snapshot,629.819,18,465 -websocket-intermediate,browser.fill,120.957,64,56 -websocket-intermediate,browser.click,119.898,30,57 -websocket-intermediate,browser.snapshot,662.34,18,465 -websocket-intermediate,browser.fill,96.935,64,56 -websocket-intermediate,browser.click,91.573,30,57 -websocket-intermediate,browser.snapshot,629.71,18,465 -websocket-intermediate,browser.fill,98.913,64,56 -websocket-intermediate,browser.click,88.664,30,57 -websocket-intermediate,browser.snapshot,609.257,18,466 -websocket-intermediate,browser.fill,97.821,64,56 -websocket-intermediate,browser.click,90.31,31,57 -websocket-intermediate,browser.snapshot,612.338,18,470 -websocket-intermediate,browser.fill,101.064,65,56 -websocket-intermediate,browser.click,93.912,31,57 -websocket-intermediate,browser.snapshot,657.423,18,470 -websocket-intermediate,browser.fill,102.622,65,56 -websocket-intermediate,browser.click,92.293,31,57 -websocket-intermediate,browser.snapshot,637.513,18,470 -websocket-intermediate,browser.fill,96.8,65,56 -websocket-intermediate,browser.click,87.089,31,57 -websocket-intermediate,browser.snapshot,640.743,18,470 -websocket-intermediate,browser.fill,104.685,65,56 -websocket-intermediate,browser.click,98.108,31,57 -websocket-intermediate,browser.snapshot,606.495,18,470 -websocket-intermediate,browser.fill,99.052,65,56 -websocket-intermediate,browser.click,87.919,31,57 -websocket-intermediate,browser.snapshot,608.234,18,470 -websocket-intermediate,browser.fill,97.604,65,56 -websocket-intermediate,browser.click,90.315,31,57 -websocket-intermediate,browser.snapshot,583.847,18,470 -websocket-intermediate,browser.fill,98.28,65,56 -websocket-intermediate,browser.click,89.727,31,57 -websocket-intermediate,browser.snapshot,598.92,18,470 -websocket-intermediate,browser.fill,98.668,65,56 -websocket-intermediate,browser.click,91.766,31,57 -websocket-intermediate,browser.snapshot,593.021,18,470 -websocket-intermediate,browser.fill,97.96,65,56 -websocket-intermediate,browser.click,90.371,31,57 -websocket-intermediate,browser.snapshot,637.53,18,470 -websocket-intermediate,browser.fill,99.277,65,56 -websocket-intermediate,browser.click,90.054,31,57 -websocket-intermediate,browser.snapshot,602.92,18,470 -websocket-intermediate,browser.fill,102.884,65,56 -websocket-intermediate,browser.click,90.275,31,57 -websocket-final,browser.snapshot,664.745,18,461 -websocket-final,browser.fill,105.674,63,56 -websocket-final,browser.click,94.057,30,57 -websocket-final,browser.snapshot,624.873,18,465 -websocket-final,browser.fill,101.705,64,56 -websocket-final,browser.click,94.87,30,57 -websocket-final,browser.snapshot,629.386,18,465 -websocket-final,browser.fill,100.401,64,56 -websocket-final,browser.click,90.052,30,57 -websocket-final,browser.snapshot,615.24,18,465 -websocket-final,browser.fill,101.273,64,56 -websocket-final,browser.click,125.812,30,57 -websocket-final,browser.snapshot,652.561,18,465 -websocket-final,browser.fill,98.598,64,56 -websocket-final,browser.click,93.108,30,57 -websocket-final,browser.snapshot,615.302,18,465 -websocket-final,browser.fill,97.775,64,56 -websocket-final,browser.click,93.095,30,57 -websocket-final,browser.snapshot,631.475,18,465 -websocket-final,browser.fill,104.903,64,56 -websocket-final,browser.click,93.588,30,57 -websocket-final,browser.snapshot,628.054,18,465 -websocket-final,browser.fill,98.46,64,56 -websocket-final,browser.click,91.984,30,57 -websocket-final,browser.snapshot,599.163,18,465 -websocket-final,browser.fill,99.428,64,56 -websocket-final,browser.click,90.135,30,57 -websocket-final,browser.snapshot,600.259,18,465 -websocket-final,browser.fill,100.452,64,56 -websocket-final,browser.click,91.013,30,57 -websocket-final,browser.snapshot,604.582,18,465 -websocket-final,browser.fill,99.086,64,56 -websocket-final,browser.click,94.497,30,57 -websocket-final,browser.snapshot,626.666,18,465 -websocket-final,browser.fill,99.997,64,56 -websocket-final,browser.click,94.632,30,57 -websocket-final,browser.snapshot,625.802,18,465 -websocket-final,browser.fill,99.643,64,56 -websocket-final,browser.click,90.132,30,57 -websocket-final,browser.snapshot,612.176,18,465 -websocket-final,browser.fill,97.543,64,56 -websocket-final,browser.click,90.904,30,57 -websocket-final,browser.snapshot,609.463,18,465 -websocket-final,browser.fill,97.819,64,56 -websocket-final,browser.click,90.599,30,57 -websocket-final,browser.snapshot,611.608,18,465 -websocket-final,browser.fill,107.402,64,56 -websocket-final,browser.click,98.54,30,57 -websocket-final,browser.snapshot,1157.887,18,465 -websocket-final,browser.fill,225.363,64,56 -websocket-final,browser.click,169.617,30,57 -websocket-final,browser.snapshot,1418.676,18,465 -websocket-final,browser.fill,538.377,64,56 -websocket-final,browser.click,272.395,30,57 -websocket-final,browser.snapshot,1261.849,18,466 -websocket-final,browser.fill,186.248,64,56 -websocket-final,browser.click,161.84,31,57 -websocket-final,browser.snapshot,1940.601,18,470 -websocket-final,browser.fill,235.379,65,56 -websocket-final,browser.click,291.711,31,57 -websocket-final,browser.snapshot,1876.544,18,470 -websocket-final,browser.fill,400.25,65,56 -websocket-final,browser.click,149.508,31,57 -websocket-final,browser.snapshot,1871.929,18,470 -websocket-final,browser.fill,211.004,65,56 -websocket-final,browser.click,104.619,31,57 -websocket-final,browser.snapshot,1205.999,18,470 -websocket-final,browser.fill,169.634,65,56 -websocket-final,browser.click,142.133,31,57 -websocket-final,browser.snapshot,1651.754,18,470 -websocket-final,browser.fill,168.634,65,56 -websocket-final,browser.click,114.817,31,57 -websocket-final,browser.snapshot,669.3,18,470 -websocket-final,browser.fill,106.903,65,56 -websocket-final,browser.click,91.782,31,57 -websocket-final,browser.snapshot,736.557,18,470 -websocket-final,browser.fill,185.907,65,56 -websocket-final,browser.click,256.563,31,57 -websocket-final,browser.snapshot,1579.727,18,470 -websocket-final,browser.fill,234.339,65,56 -websocket-final,browser.click,355.891,31,57 -websocket-final,browser.snapshot,970.507,18,470 -websocket-final,browser.fill,104.105,65,56 -websocket-final,browser.click,96.146,31,57 -websocket-final,browser.snapshot,617.855,18,470 -websocket-final,browser.fill,99.622,65,56 -websocket-final,browser.click,96.835,31,57 -websocket-final,browser.snapshot,596.08,18,470 -websocket-final,browser.fill,97.688,65,56 -websocket-final,browser.click,90.159,31,57 -websocket-validation-validation,browser.snapshot,1182.863,18,461 -websocket-validation-validation,browser.fill,114.374,63,56 -websocket-validation-validation,browser.click,111.908,30,57 -websocket-validation-validation,browser.snapshot,700.686,18,465 -websocket-validation-validation,browser.fill,149.448,64,56 -websocket-validation-validation,browser.click,147.34,30,57 -websocket-validation-validation,browser.snapshot,740.642,18,465 -websocket-validation-validation,browser.fill,124.896,64,56 -websocket-validation-validation,browser.click,104.523,30,57 -websocket-validation-validation,browser.snapshot,661.463,18,465 -websocket-validation-validation,browser.fill,105.836,64,56 -websocket-validation-validation,browser.click,103.252,30,57 -websocket-validation-validation,browser.snapshot,662.27,18,465 -websocket-validation-validation,browser.fill,117.871,64,56 -websocket-validation-validation,browser.click,105.21,30,57 -websocket-validation-validation,browser.snapshot,751.368,18,465 -websocket-validation-validation,browser.fill,143.406,64,56 -websocket-validation-validation,browser.click,102.786,30,57 -websocket-validation-validation,browser.snapshot,807.09,18,465 -websocket-validation-validation,browser.fill,103.368,64,56 -websocket-validation-validation,browser.click,97.203,30,57 -websocket-validation-validation,browser.snapshot,651.861,18,465 -websocket-validation-validation,browser.fill,108.163,64,56 -websocket-validation-validation,browser.click,98.198,30,57 -websocket-validation-validation,browser.snapshot,620.001,18,465 -websocket-validation-validation,browser.fill,127.217,64,56 -websocket-validation-validation,browser.click,97.072,30,57 -websocket-validation-validation,browser.snapshot,627.498,18,465 -websocket-validation-validation,browser.fill,99.381,64,56 -websocket-validation-validation,browser.click,96.191,30,57 -websocket-validation-validation,browser.snapshot,663.068,18,465 -websocket-validation-validation,browser.fill,172.38,64,56 -websocket-validation-validation,browser.click,123.333,30,57 -websocket-validation-validation,browser.snapshot,664.126,18,465 -websocket-validation-validation,browser.fill,126.783,64,56 -websocket-validation-validation,browser.click,101.684,30,57 -websocket-validation-validation,browser.snapshot,649.343,18,465 -websocket-validation-validation,browser.fill,112.777,64,56 -websocket-validation-validation,browser.click,107.175,30,57 -websocket-validation-validation,browser.snapshot,1017.371,18,465 -websocket-validation-validation,browser.fill,128.802,64,56 -websocket-validation-validation,browser.click,101.155,30,57 -websocket-validation-validation,browser.snapshot,771.506,18,465 -websocket-validation-validation,browser.fill,103.969,64,56 -websocket-validation-validation,browser.click,95.207,30,57 -websocket-validation-validation,browser.snapshot,648.612,18,465 -websocket-validation-validation,browser.fill,103.409,64,56 -websocket-validation-validation,browser.click,97.664,30,57 -websocket-validation-validation,browser.snapshot,732.147,18,465 -websocket-validation-validation,browser.fill,180.856,64,56 -websocket-validation-validation,browser.click,169.003,30,57 -websocket-validation-validation,browser.snapshot,838.509,18,465 -websocket-validation-validation,browser.fill,107.561,64,56 -websocket-validation-validation,browser.click,96.55,30,57 -websocket-validation-validation,browser.snapshot,672.017,18,466 -websocket-validation-validation,browser.fill,136.635,64,56 -websocket-validation-validation,browser.click,103.038,31,57 -websocket-validation-validation,browser.snapshot,698.326,18,470 -websocket-validation-validation,browser.fill,209.049,65,56 -websocket-validation-validation,browser.click,134.582,31,57 -websocket-validation-validation,browser.snapshot,666.06,18,470 -websocket-validation-validation,browser.fill,299.16,65,56 -websocket-validation-validation,browser.click,113.401,31,57 -websocket-validation-validation,browser.snapshot,750.514,18,470 -websocket-validation-validation,browser.fill,212.186,65,56 -websocket-validation-validation,browser.click,118.544,31,57 -websocket-validation-validation,browser.snapshot,1289.783,18,470 -websocket-validation-validation,browser.fill,119.847,65,56 -websocket-validation-validation,browser.click,127.474,31,57 -websocket-validation-validation,browser.snapshot,622.654,18,470 -websocket-validation-validation,browser.fill,134.033,65,56 -websocket-validation-validation,browser.click,92.134,31,57 -websocket-validation-validation,browser.snapshot,601.283,18,470 -websocket-validation-validation,browser.fill,130.64,65,56 -websocket-validation-validation,browser.click,99.477,31,57 -websocket-validation-validation,browser.snapshot,601.336,18,470 -websocket-validation-validation,browser.fill,103.909,65,56 -websocket-validation-validation,browser.click,102.546,31,57 -websocket-validation-validation,browser.snapshot,704.16,18,470 -websocket-validation-validation,browser.fill,103.194,65,56 -websocket-validation-validation,browser.click,92.169,31,57 -websocket-validation-validation,browser.snapshot,637.63,18,470 -websocket-validation-validation,browser.fill,109.829,65,56 -websocket-validation-validation,browser.click,99.667,31,57 -websocket-validation-validation,browser.snapshot,639.144,18,470 -websocket-validation-validation,browser.fill,106.618,65,56 -websocket-validation-validation,browser.click,92.268,31,57 -websocket-validation-validation,browser.snapshot,643.621,18,470 -websocket-validation-validation,browser.fill,135.937,65,56 -websocket-validation-validation,browser.click,96.254,31,57 -websocket-validation-profile,browser.snapshot,671.33,18,461 -websocket-validation-profile,browser.fill,107.326,63,56 -websocket-validation-profile,browser.click,92.857,30,57 -websocket-validation-profile,browser.snapshot,627.918,18,465 -websocket-validation-profile,browser.fill,109.345,64,56 -websocket-validation-profile,browser.click,103.387,30,57 -websocket-validation-profile,browser.snapshot,877.921,18,465 -websocket-validation-profile,browser.fill,118.389,64,56 -websocket-validation-profile,browser.click,92.472,30,57 -websocket-validation-profile,browser.snapshot,641.717,18,465 -websocket-validation-profile,browser.fill,127.872,64,56 -websocket-validation-profile,browser.click,94.146,30,57 -websocket-validation-profile,browser.snapshot,614.898,18,465 -websocket-validation-profile,browser.fill,106.751,64,56 -websocket-validation-profile,browser.click,94.677,30,57 -websocket-validation-profile,browser.snapshot,673.926,18,465 -websocket-validation-profile,browser.fill,109.068,64,56 -websocket-validation-profile,browser.click,93.791,30,57 -websocket-validation-profile,browser.snapshot,647.503,18,465 -websocket-validation-profile,browser.fill,99.951,64,56 -websocket-validation-profile,browser.click,90.514,30,57 -websocket-validation-profile,browser.snapshot,607.553,18,465 -websocket-validation-profile,browser.fill,98.977,64,56 -websocket-validation-profile,browser.click,89.501,30,57 -websocket-validation-profile,browser.snapshot,602.448,18,465 -websocket-validation-profile,browser.fill,95.923,64,56 -websocket-validation-profile,browser.click,91.362,30,57 -websocket-validation-profile,browser.snapshot,603.778,18,465 -websocket-validation-profile,browser.fill,102.73,64,56 -websocket-validation-profile,browser.click,90.205,30,57 -websocket-validation-profile,browser.snapshot,640.702,18,465 -websocket-validation-profile,browser.fill,100.103,64,56 -websocket-validation-profile,browser.click,95.62,30,57 -websocket-validation-profile,browser.snapshot,693.598,18,465 -websocket-validation-profile,browser.fill,130.695,64,56 -websocket-validation-profile,browser.click,107.693,30,57 -websocket-validation-profile,browser.snapshot,637.21,18,465 -websocket-validation-profile,browser.fill,96.849,64,56 -websocket-validation-profile,browser.click,89.902,30,57 -websocket-validation-profile,browser.snapshot,632.534,18,465 -websocket-validation-profile,browser.fill,99.77,64,56 -websocket-validation-profile,browser.click,92.564,30,57 -websocket-validation-profile,browser.snapshot,692.932,18,465 -websocket-validation-profile,browser.fill,115.088,64,56 -websocket-validation-profile,browser.click,136.562,30,57 -websocket-validation-profile,browser.snapshot,649.236,18,465 -websocket-validation-profile,browser.fill,101.412,64,56 -websocket-validation-profile,browser.click,117.074,30,57 -websocket-validation-profile,browser.snapshot,625.749,18,465 -websocket-validation-profile,browser.fill,105.055,64,56 -websocket-validation-profile,browser.click,91.302,30,57 -websocket-validation-profile,browser.snapshot,621.003,18,465 -websocket-validation-profile,browser.fill,111.529,64,56 -websocket-validation-profile,browser.click,89.884,30,57 -websocket-validation-profile,browser.snapshot,791.033,18,466 -websocket-validation-profile,browser.fill,113.189,64,56 -websocket-validation-profile,browser.click,90.055,31,57 -websocket-validation-profile,browser.snapshot,604.588,18,470 -websocket-validation-profile,browser.fill,100.27,65,56 -websocket-validation-profile,browser.click,101.367,31,57 -websocket-validation-profile,browser.snapshot,627.667,18,470 -websocket-validation-profile,browser.fill,124.06,65,56 -websocket-validation-profile,browser.click,109.44,31,57 -websocket-validation-profile,browser.snapshot,631.615,18,470 -websocket-validation-profile,browser.fill,104.461,65,56 -websocket-validation-profile,browser.click,92.172,31,57 -websocket-validation-profile,browser.snapshot,617.246,18,470 -websocket-validation-profile,browser.fill,97.765,65,56 -websocket-validation-profile,browser.click,89.745,31,57 -websocket-validation-profile,browser.snapshot,593.626,18,470 -websocket-validation-profile,browser.fill,98.506,65,56 -websocket-validation-profile,browser.click,101.134,31,57 -websocket-validation-profile,browser.snapshot,654.203,18,470 -websocket-validation-profile,browser.fill,107.819,65,56 -websocket-validation-profile,browser.click,92.541,31,57 -websocket-validation-profile,browser.snapshot,700.239,18,470 -websocket-validation-profile,browser.fill,123.17,65,56 -websocket-validation-profile,browser.click,97.625,31,57 -websocket-validation-profile,browser.snapshot,674.439,18,470 -websocket-validation-profile,browser.fill,117.989,65,56 -websocket-validation-profile,browser.click,92.777,31,57 -websocket-validation-profile,browser.snapshot,629.703,18,470 -websocket-validation-profile,browser.fill,100.347,65,56 -websocket-validation-profile,browser.click,99.607,31,57 -websocket-validation-profile,browser.snapshot,616.109,18,470 -websocket-validation-profile,browser.fill,97.113,65,56 -websocket-validation-profile,browser.click,88.521,31,57 -websocket-validation-profile,browser.snapshot,602.571,18,470 -websocket-validation-profile,browser.fill,98.539,65,56 -websocket-validation-profile,browser.click,92.199,31,57 -websocket-validation-repeat,browser.snapshot,689.002,18,461 -websocket-validation-repeat,browser.fill,103.37,63,56 -websocket-validation-repeat,browser.click,92.261,30,57 -websocket-validation-repeat,browser.snapshot,642.689,18,465 -websocket-validation-repeat,browser.fill,101.016,64,56 -websocket-validation-repeat,browser.click,121.676,30,57 -websocket-validation-repeat,browser.snapshot,879.289,18,465 -websocket-validation-repeat,browser.fill,102.015,64,56 -websocket-validation-repeat,browser.click,93.136,30,57 -websocket-validation-repeat,browser.snapshot,701.735,18,465 -websocket-validation-repeat,browser.fill,99.535,64,56 -websocket-validation-repeat,browser.click,91.496,30,57 -websocket-validation-repeat,browser.snapshot,624.829,18,465 -websocket-validation-repeat,browser.fill,105.156,64,56 -websocket-validation-repeat,browser.click,93.65,30,57 -websocket-validation-repeat,browser.snapshot,677.641,18,465 -websocket-validation-repeat,browser.fill,100.008,64,56 -websocket-validation-repeat,browser.click,123.94,30,57 -websocket-validation-repeat,browser.snapshot,690.193,18,465 -websocket-validation-repeat,browser.fill,110.545,64,56 -websocket-validation-repeat,browser.click,93.448,30,57 -websocket-validation-repeat,browser.snapshot,646.878,18,465 -websocket-validation-repeat,browser.fill,100.988,64,56 -websocket-validation-repeat,browser.click,92.194,30,57 -websocket-validation-repeat,browser.snapshot,635.47,18,465 -websocket-validation-repeat,browser.fill,101.433,64,56 -websocket-validation-repeat,browser.click,91.104,30,57 -websocket-validation-repeat,browser.snapshot,632.336,18,465 -websocket-validation-repeat,browser.fill,97.851,64,56 -websocket-validation-repeat,browser.click,90.533,30,57 -websocket-validation-repeat,browser.snapshot,601.599,18,465 -websocket-validation-repeat,browser.fill,97.693,64,56 -websocket-validation-repeat,browser.click,90.213,30,57 -websocket-validation-repeat,browser.snapshot,627.206,18,465 -websocket-validation-repeat,browser.fill,108.616,64,56 -websocket-validation-repeat,browser.click,92.066,30,57 -websocket-validation-repeat,browser.snapshot,650.785,18,465 -websocket-validation-repeat,browser.fill,98.612,64,56 -websocket-validation-repeat,browser.click,91.203,30,57 -websocket-validation-repeat,browser.snapshot,632.07,18,465 -websocket-validation-repeat,browser.fill,94.721,64,56 -websocket-validation-repeat,browser.click,87.656,30,57 -websocket-validation-repeat,browser.snapshot,617.111,18,465 -websocket-validation-repeat,browser.fill,101.577,64,56 -websocket-validation-repeat,browser.click,96.785,30,57 -websocket-validation-repeat,browser.snapshot,615.053,18,465 -websocket-validation-repeat,browser.fill,99.802,64,56 -websocket-validation-repeat,browser.click,90.256,30,57 -websocket-validation-repeat,browser.snapshot,608.767,18,465 -websocket-validation-repeat,browser.fill,100.073,64,56 -websocket-validation-repeat,browser.click,92.157,30,57 -websocket-validation-repeat,browser.snapshot,635.634,18,465 -websocket-validation-repeat,browser.fill,100.064,64,56 -websocket-validation-repeat,browser.click,90.31,30,57 -websocket-validation-repeat,browser.snapshot,614.558,18,466 -websocket-validation-repeat,browser.fill,96.245,64,56 -websocket-validation-repeat,browser.click,91.895,31,57 -websocket-validation-repeat,browser.snapshot,629.242,18,470 -websocket-validation-repeat,browser.fill,97.473,65,56 -websocket-validation-repeat,browser.click,90.146,31,57 -websocket-validation-repeat,browser.snapshot,615.515,18,470 -websocket-validation-repeat,browser.fill,96.739,65,56 -websocket-validation-repeat,browser.click,89.612,31,57 -websocket-validation-repeat,browser.snapshot,645.705,18,470 -websocket-validation-repeat,browser.fill,111.366,65,56 -websocket-validation-repeat,browser.click,93.142,31,57 -websocket-validation-repeat,browser.snapshot,644.829,18,470 -websocket-validation-repeat,browser.fill,97.446,65,56 -websocket-validation-repeat,browser.click,90.334,31,57 -websocket-validation-repeat,browser.snapshot,613.952,18,470 -websocket-validation-repeat,browser.fill,96.737,65,56 -websocket-validation-repeat,browser.click,91.277,31,57 -websocket-validation-repeat,browser.snapshot,623.556,18,470 -websocket-validation-repeat,browser.fill,97.326,65,56 -websocket-validation-repeat,browser.click,88.265,31,57 -websocket-validation-repeat,browser.snapshot,645.855,18,470 -websocket-validation-repeat,browser.fill,95.207,65,56 -websocket-validation-repeat,browser.click,89.18,31,57 -websocket-validation-repeat,browser.snapshot,679.68,18,470 -websocket-validation-repeat,browser.fill,104.128,65,56 -websocket-validation-repeat,browser.click,94.426,31,57 -websocket-validation-repeat,browser.snapshot,646.129,18,470 -websocket-validation-repeat,browser.fill,97.047,65,56 -websocket-validation-repeat,browser.click,87.523,31,57 -websocket-validation-repeat,browser.snapshot,620.01,18,470 -websocket-validation-repeat,browser.fill,97.241,65,56 -websocket-validation-repeat,browser.click,90.514,31,57 -websocket-validation-repeat,browser.snapshot,634.757,18,470 -websocket-validation-repeat,browser.fill,97.123,65,56 -websocket-validation-repeat,browser.click,91.166,31,57 -websocket-validation-final,browser.snapshot,609.194,18,461 -websocket-validation-final,browser.fill,100.946,63,56 -websocket-validation-final,browser.click,94.02,30,57 -websocket-validation-final,browser.snapshot,605.819,18,465 -websocket-validation-final,browser.fill,99.498,64,56 -websocket-validation-final,browser.click,91.49,30,57 -websocket-validation-final,browser.snapshot,602.858,18,465 -websocket-validation-final,browser.fill,98.328,64,56 -websocket-validation-final,browser.click,90.682,30,57 -websocket-validation-final,browser.snapshot,587.73,18,465 -websocket-validation-final,browser.fill,111.221,64,56 -websocket-validation-final,browser.click,91.895,30,57 -websocket-validation-final,browser.snapshot,627.105,18,465 -websocket-validation-final,browser.fill,101.407,64,56 -websocket-validation-final,browser.click,90.306,30,57 -websocket-validation-final,browser.snapshot,634.053,18,465 -websocket-validation-final,browser.fill,100.227,64,56 -websocket-validation-final,browser.click,96.394,30,57 -websocket-validation-final,browser.snapshot,636.025,18,465 -websocket-validation-final,browser.fill,99.626,64,56 -websocket-validation-final,browser.click,91.48,30,57 -websocket-validation-final,browser.snapshot,580.655,18,465 -websocket-validation-final,browser.fill,95.952,64,56 -websocket-validation-final,browser.click,90.212,30,57 -websocket-validation-final,browser.snapshot,612.975,18,465 -websocket-validation-final,browser.fill,105.298,64,56 -websocket-validation-final,browser.click,91.447,30,57 -websocket-validation-final,browser.snapshot,603.648,18,465 -websocket-validation-final,browser.fill,99.626,64,56 -websocket-validation-final,browser.click,89.335,30,57 -websocket-validation-final,browser.snapshot,596.111,18,465 -websocket-validation-final,browser.fill,94.555,64,56 -websocket-validation-final,browser.click,87.943,30,57 -websocket-validation-final,browser.snapshot,588.854,18,465 -websocket-validation-final,browser.fill,98.507,64,56 -websocket-validation-final,browser.click,91.054,30,57 -websocket-validation-final,browser.snapshot,588.151,18,465 -websocket-validation-final,browser.fill,94.622,64,56 -websocket-validation-final,browser.click,89.397,30,57 -websocket-validation-final,browser.snapshot,611.732,18,465 -websocket-validation-final,browser.fill,95.015,64,56 -websocket-validation-final,browser.click,85.09,30,57 -websocket-validation-final,browser.snapshot,625.254,18,465 -websocket-validation-final,browser.fill,98.26,64,56 -websocket-validation-final,browser.click,88.361,30,57 -websocket-validation-final,browser.snapshot,626.148,18,465 -websocket-validation-final,browser.fill,95.427,64,56 -websocket-validation-final,browser.click,89.357,30,57 -websocket-validation-final,browser.snapshot,614.862,18,465 -websocket-validation-final,browser.fill,97.836,64,56 -websocket-validation-final,browser.click,89.167,30,57 -websocket-validation-final,browser.snapshot,620.83,18,465 -websocket-validation-final,browser.fill,93.296,64,56 -websocket-validation-final,browser.click,86.383,30,57 -websocket-validation-final,browser.snapshot,711.974,18,466 -websocket-validation-final,browser.fill,131.279,64,56 -websocket-validation-final,browser.click,106.474,31,57 -websocket-validation-final,browser.snapshot,671.268,18,470 -websocket-validation-final,browser.fill,146.599,65,56 -websocket-validation-final,browser.click,102.216,31,57 -websocket-validation-final,browser.snapshot,693.128,18,470 -websocket-validation-final,browser.fill,109.694,65,56 -websocket-validation-final,browser.click,97.09,31,57 -websocket-validation-final,browser.snapshot,841.208,18,470 -websocket-validation-final,browser.fill,98.365,65,56 -websocket-validation-final,browser.click,99.011,31,57 -websocket-validation-final,browser.snapshot,634.851,18,470 -websocket-validation-final,browser.fill,99.78,65,56 -websocket-validation-final,browser.click,92.592,31,57 -websocket-validation-final,browser.snapshot,697.512,18,470 -websocket-validation-final,browser.fill,130.975,65,56 -websocket-validation-final,browser.click,92.891,31,57 -websocket-validation-final,browser.snapshot,681.105,18,470 -websocket-validation-final,browser.fill,105.034,65,56 -websocket-validation-final,browser.click,104.193,31,57 -websocket-validation-final,browser.snapshot,613.601,18,470 -websocket-validation-final,browser.fill,103.605,65,56 -websocket-validation-final,browser.click,108.998,31,57 -websocket-validation-final,browser.snapshot,708.145,18,470 -websocket-validation-final,browser.fill,98.159,65,56 -websocket-validation-final,browser.click,92.185,31,57 -websocket-validation-final,browser.snapshot,692.54,18,470 -websocket-validation-final,browser.fill,108.658,65,56 -websocket-validation-final,browser.click,111.316,31,57 -websocket-validation-final,browser.snapshot,670.807,18,470 -websocket-validation-final,browser.fill,101.042,65,56 -websocket-validation-final,browser.click,91.422,31,57 -websocket-validation-final,browser.snapshot,660.968,18,470 -websocket-validation-final,browser.fill,100.358,65,56 -websocket-validation-final,browser.click,95.742,31,57 -devframe-validation-validation,browser.fill,340.097,118,56 -devframe-validation-validation,browser.click,292.802,97,57 -devframe-validation-validation,browser.snapshot,632.067,18,452 -devframe-validation-validation,browser.batch,1186.612,261,605 -devframe-validation-validation,browser.fill,383.29,118,56 -devframe-validation-validation,browser.click,317.468,97,57 -devframe-validation-validation,browser.snapshot,608.528,18,456 -devframe-validation-validation,browser.batch,1281.218,261,605 -devframe-validation-validation,browser.fill,387.383,118,56 -devframe-validation-validation,browser.click,266.776,97,57 -devframe-validation-validation,browser.snapshot,671.853,18,456 -devframe-validation-validation,browser.batch,1310.824,261,605 -devframe-validation-validation,browser.fill,325.141,118,56 -devframe-validation-validation,browser.click,342.566,97,57 -devframe-validation-validation,browser.snapshot,593.86,18,456 -devframe-validation-validation,browser.batch,1265.797,261,605 -devframe-validation-validation,browser.fill,407.939,118,56 -devframe-validation-validation,browser.click,320.753,97,57 -devframe-validation-validation,browser.snapshot,598.263,18,456 -devframe-validation-validation,browser.batch,1317.02,261,605 -devframe-validation-validation,browser.fill,397.547,118,56 -devframe-validation-validation,browser.click,305.95,97,57 -devframe-validation-validation,browser.snapshot,613.472,18,456 -devframe-validation-validation,browser.batch,1341.585,261,605 -devframe-validation-validation,browser.fill,278.219,118,56 -devframe-validation-validation,browser.click,275.59,97,57 -devframe-validation-validation,browser.snapshot,612.154,18,456 -devframe-validation-validation,browser.batch,1224.37,261,605 -devframe-validation-validation,browser.fill,383.537,118,56 -devframe-validation-validation,browser.click,330.406,97,57 -devframe-validation-validation,browser.snapshot,609.647,18,456 -devframe-validation-validation,browser.batch,1165.287,261,605 -devframe-validation-validation,browser.fill,429.27,118,56 -devframe-validation-validation,browser.click,363.652,97,57 -devframe-validation-validation,browser.snapshot,633.367,18,456 -devframe-validation-validation,browser.batch,1259.317,261,605 -devframe-validation-validation,browser.fill,338.517,118,56 -devframe-validation-validation,browser.click,288.884,97,57 -devframe-validation-validation,browser.snapshot,667.674,18,457 -devframe-validation-validation,browser.batch,1207.499,261,610 -devframe-validation-validation,browser.fill,371.855,118,56 -devframe-validation-validation,browser.click,331.782,97,57 -devframe-validation-validation,browser.snapshot,752.394,18,461 -devframe-validation-validation,browser.batch,1076.647,261,610 -devframe-validation-validation,browser.fill,426.872,118,56 -devframe-validation-validation,browser.click,411.911,97,57 -devframe-validation-validation,browser.snapshot,738.02,18,461 -devframe-validation-validation,browser.batch,1041.727,261,610 -devframe-validation-validation,browser.fill,370.228,118,56 -devframe-validation-validation,browser.click,361.279,97,57 -devframe-validation-validation,browser.snapshot,563.757,18,461 -devframe-validation-validation,browser.batch,1283.173,261,610 -devframe-validation-validation,browser.fill,319.799,118,56 -devframe-validation-validation,browser.click,378.422,97,57 -devframe-validation-validation,browser.snapshot,666.585,18,461 -devframe-validation-validation,browser.batch,1152.538,261,610 -devframe-validation-validation,browser.fill,308.421,118,56 -devframe-validation-validation,browser.click,332.601,97,57 -devframe-validation-validation,browser.snapshot,592.524,18,461 -devframe-validation-validation,browser.batch,1288.587,261,610 -devframe-validation-validation,browser.fill,401.692,118,56 -devframe-validation-validation,browser.click,422.479,97,57 -devframe-validation-validation,browser.snapshot,613.033,18,461 -devframe-validation-validation,browser.batch,1320.636,261,610 -devframe-validation-validation,browser.fill,369.6,118,56 -devframe-validation-validation,browser.click,347.167,97,57 -devframe-validation-validation,browser.snapshot,583.995,18,461 -devframe-validation-validation,browser.batch,1166.723,261,610 -devframe-validation-validation,browser.fill,361.417,118,56 -devframe-validation-validation,browser.click,406.104,97,57 -devframe-validation-validation,browser.snapshot,614.672,18,461 -devframe-validation-validation,browser.batch,1284.271,261,610 -devframe-validation-validation,browser.fill,333.812,118,56 -devframe-validation-validation,browser.click,326.341,97,57 -devframe-validation-validation,browser.snapshot,673.167,18,461 -devframe-validation-validation,browser.batch,1239.59,261,610 -devframe-validation-validation,browser.fill,351.025,118,56 -devframe-validation-validation,browser.click,342.679,97,57 -devframe-validation-validation,browser.snapshot,590.104,18,461 -devframe-validation-validation,browser.batch,1642.983,261,610 -devframe-validation-validation,browser.fill,323.922,118,56 -devframe-validation-validation,browser.click,298.95,97,57 -devframe-validation-validation,browser.snapshot,583.05,18,461 -devframe-validation-validation,browser.batch,1359.275,261,610 -devframe-validation-validation,browser.fill,355.355,118,56 -devframe-validation-validation,browser.click,297.134,97,57 -devframe-validation-validation,browser.snapshot,628.134,18,461 -devframe-validation-validation,browser.batch,1149.58,261,610 -devframe-validation-validation,browser.fill,372.307,118,56 -devframe-validation-validation,browser.click,356.803,97,57 -devframe-validation-validation,browser.snapshot,588.413,18,461 -devframe-validation-validation,browser.batch,1121.753,261,610 -devframe-validation-validation,browser.fill,347.528,118,56 -devframe-validation-validation,browser.click,275.648,97,57 -devframe-validation-validation,browser.snapshot,581.245,18,461 -devframe-validation-validation,browser.batch,1269.639,261,610 -devframe-validation-validation,browser.fill,335.269,118,56 -devframe-validation-validation,browser.click,409.154,97,57 -devframe-validation-validation,browser.snapshot,658.343,18,461 -devframe-validation-validation,browser.batch,1427.485,261,610 -devframe-validation-validation,browser.fill,298.9,118,56 -devframe-validation-validation,browser.click,306.002,97,57 -devframe-validation-validation,browser.snapshot,664.216,18,461 -devframe-validation-validation,browser.batch,1245.854,261,610 -devframe-validation-validation,browser.fill,374.639,118,56 -devframe-validation-validation,browser.click,329.531,97,57 -devframe-validation-validation,browser.snapshot,822.374,18,461 -devframe-validation-validation,browser.batch,1109.942,261,610 -devframe-validation-validation,browser.fill,335.336,118,56 -devframe-validation-validation,browser.click,425.028,97,57 -devframe-validation-validation,browser.snapshot,597.196,18,461 -devframe-validation-validation,browser.batch,1226.926,261,610 -devframe-validation-validation,browser.fill,380.779,118,56 -devframe-validation-validation,browser.click,389.456,97,57 -devframe-validation-validation,browser.snapshot,603.882,18,461 -devframe-validation-validation,browser.batch,1306.936,261,610 -devframe-validation-validation,browser.fill,426.236,118,56 -devframe-validation-validation,browser.click,337.037,97,57 -devframe-validation-validation,browser.snapshot,588.49,18,461 -devframe-validation-validation,browser.batch,1303.217,261,610 -devframe-validation-validation,browser.click:blocked,2002.87,97,197 -devframe-validation-validation,browser.click:blocked,2001.755,97,197 -devframe-validation-validation,browser.click:blocked,2001.964,97,197 -devframe-validation-validation,browser.click:blocked,2002.237,97,197 -devframe-validation-validation,browser.click:blocked,2000.503,97,197 -devframe-validation-validation,browser.click:blocked,2002.435,97,197 -devframe-validation-validation,browser.click:blocked,2000.854,97,197 -devframe-validation-validation,browser.click:blocked,2000.979,97,197 -devframe-validation-validation,browser.click:blocked,2002.857,97,197 -devframe-validation-validation,browser.click:blocked,2001.402,97,197 -devframe-validation-validation,browser.click:blocked,2002.378,97,197 -devframe-validation-validation,browser.click:blocked,2003.021,97,197 -devframe-validation-validation,browser.click:blocked,2024.507,97,197 -devframe-validation-validation,browser.click:blocked,2000.284,97,197 -devframe-validation-validation,browser.click:blocked,2001.654,97,197 -devframe-validation-validation,browser.click:blocked,2002.467,97,197 -devframe-validation-validation,browser.click:blocked,2000.535,97,197 -devframe-validation-validation,browser.click:blocked,2002.296,97,197 -devframe-validation-validation,browser.click:blocked,2013.454,97,197 -devframe-validation-validation,browser.click:blocked,2001.919,97,197 -devframe-validation-validation,browser.click:blocked,2030.31,97,197 -devframe-validation-validation,browser.click:blocked,2003.089,97,197 -devframe-validation-validation,browser.click:blocked,2001.517,97,197 -devframe-validation-validation,browser.click:blocked,2056.295,97,197 -devframe-validation-validation,browser.click:blocked,2000.825,97,197 -devframe-validation-validation,browser.click:blocked,2001.54,97,197 -devframe-validation-validation,browser.click:blocked,2001.676,97,197 -devframe-validation-validation,browser.click:blocked,2001.146,97,197 -devframe-validation-validation,browser.click:blocked,2059.054,97,197 -devframe-validation-validation,browser.click:blocked,2001.048,97,197 -devframe-validation-final,browser.fill,334.525,118,56 -devframe-validation-final,browser.click,319.495,97,57 -devframe-validation-final,browser.snapshot,587.946,18,452 -devframe-validation-final,browser.batch,1076.829,261,605 -devframe-validation-final,browser.fill,286.73,118,56 -devframe-validation-final,browser.click,288.48,97,57 -devframe-validation-final,browser.snapshot,592.177,18,456 -devframe-validation-final,browser.batch,1079.096,261,605 -devframe-validation-final,browser.fill,273.888,118,56 -devframe-validation-final,browser.click,312.221,97,57 -devframe-validation-final,browser.snapshot,611.19,18,456 -devframe-validation-final,browser.batch,1099.432,261,605 -devframe-validation-final,browser.fill,297.266,118,56 -devframe-validation-final,browser.click,263.375,97,57 -devframe-validation-final,browser.snapshot,645.465,18,456 -devframe-validation-final,browser.batch,1196.065,261,605 -devframe-validation-final,browser.fill,333.928,118,56 -devframe-validation-final,browser.click,306.397,97,57 -devframe-validation-final,browser.snapshot,695.502,18,456 -devframe-validation-final,browser.batch,1358.076,261,605 -devframe-validation-final,browser.fill,362.02,118,56 -devframe-validation-final,browser.click,347.609,97,57 -devframe-validation-final,browser.snapshot,624.534,18,456 -devframe-validation-final,browser.batch,1092.474,261,605 -devframe-validation-final,browser.fill,311.23,118,56 -devframe-validation-final,browser.click,274.077,97,57 -devframe-validation-final,browser.snapshot,590.014,18,456 -devframe-validation-final,browser.batch,1100.753,261,605 -devframe-validation-final,browser.fill,284.211,118,56 -devframe-validation-final,browser.click,305.432,97,57 -devframe-validation-final,browser.snapshot,633.038,18,456 -devframe-validation-final,browser.batch,1169.554,261,605 -devframe-validation-final,browser.fill,443.609,118,56 -devframe-validation-final,browser.click,401.158,97,57 -devframe-validation-final,browser.snapshot,851.832,18,456 -devframe-validation-final,browser.batch,1134.599,261,605 -devframe-validation-final,browser.fill,306.488,118,56 -devframe-validation-final,browser.click,279.557,97,57 -devframe-validation-final,browser.snapshot,711.726,18,457 -devframe-validation-final,browser.batch,1240.372,261,610 -devframe-validation-final,browser.fill,370.35,118,56 -devframe-validation-final,browser.click,291.191,97,57 -devframe-validation-final,browser.snapshot,655.373,18,461 -devframe-validation-final,browser.batch,1242.185,261,610 -devframe-validation-final,browser.fill,287.914,118,56 -devframe-validation-final,browser.click,325.876,97,57 -devframe-validation-final,browser.snapshot,623.183,18,461 -devframe-validation-final,browser.batch,1184.104,261,610 -devframe-validation-final,browser.fill,323.203,118,56 -devframe-validation-final,browser.click,341.68,97,57 -devframe-validation-final,browser.snapshot,642.44,18,461 -devframe-validation-final,browser.batch,1077.787,261,610 -devframe-validation-final,browser.fill,313.542,118,56 -devframe-validation-final,browser.click,268.053,97,57 -devframe-validation-final,browser.snapshot,606.167,18,461 -devframe-validation-final,browser.batch,1199.638,261,610 -devframe-validation-final,browser.fill,270.38,118,56 -devframe-validation-final,browser.click,289.297,97,57 -devframe-validation-final,browser.snapshot,627.305,18,461 -devframe-validation-final,browser.batch,1159.873,261,610 -devframe-validation-final,browser.fill,327.307,118,56 -devframe-validation-final,browser.click,283.73,97,57 -devframe-validation-final,browser.snapshot,672.138,18,461 -devframe-validation-final,browser.batch,1088.664,261,610 -devframe-validation-final,browser.fill,317.571,118,56 -devframe-validation-final,browser.click,279.713,97,57 -devframe-validation-final,browser.snapshot,635.194,18,461 -devframe-validation-final,browser.batch,1116.706,261,610 -devframe-validation-final,browser.fill,307.252,118,56 -devframe-validation-final,browser.click,345.578,97,57 -devframe-validation-final,browser.snapshot,625.879,18,461 -devframe-validation-final,browser.batch,1116.924,261,610 -devframe-validation-final,browser.fill,271.904,118,56 -devframe-validation-final,browser.click,310.017,97,57 -devframe-validation-final,browser.snapshot,553.541,18,461 -devframe-validation-final,browser.batch,1072.454,261,610 -devframe-validation-final,browser.fill,288.724,118,56 -devframe-validation-final,browser.click,299.014,97,57 -devframe-validation-final,browser.snapshot,583.66,18,461 -devframe-validation-final,browser.batch,1118.419,261,610 -devframe-validation-final,browser.fill,308.042,118,56 -devframe-validation-final,browser.click,296.496,97,57 -devframe-validation-final,browser.snapshot,606.585,18,461 -devframe-validation-final,browser.batch,1021.443,261,610 -devframe-validation-final,browser.fill,300.439,118,56 -devframe-validation-final,browser.click,273.459,97,57 -devframe-validation-final,browser.snapshot,655.412,18,461 -devframe-validation-final,browser.batch,1094.311,261,610 -devframe-validation-final,browser.fill,282.885,118,56 -devframe-validation-final,browser.click,324.183,97,57 -devframe-validation-final,browser.snapshot,994.853,18,461 -devframe-validation-final,browser.batch,2307.807,261,610 -devframe-validation-final,browser.fill,389.936,118,56 -devframe-validation-final,browser.click,537.056,97,57 -devframe-validation-final,browser.snapshot,1387.117,18,461 -devframe-validation-final,browser.batch,1814.76,261,610 -devframe-validation-final,browser.fill,790.579,118,56 -devframe-validation-final,browser.click,263.882,97,57 -devframe-validation-final,browser.snapshot,627.45,18,461 -devframe-validation-final,browser.batch,1196.755,261,610 -devframe-validation-final,browser.fill,302.862,118,56 -devframe-validation-final,browser.click,299.992,97,57 -devframe-validation-final,browser.snapshot,770.81,18,461 -devframe-validation-final,browser.batch,1209.729,261,610 -devframe-validation-final,browser.fill,313.774,118,56 -devframe-validation-final,browser.click,242.085,97,57 -devframe-validation-final,browser.snapshot,586.381,18,461 -devframe-validation-final,browser.batch,1101.918,261,610 -devframe-validation-final,browser.fill,318.166,118,56 -devframe-validation-final,browser.click,329.305,97,57 -devframe-validation-final,browser.snapshot,587.955,18,461 -devframe-validation-final,browser.batch,1055.779,261,610 -devframe-validation-final,browser.fill,281.699,118,56 -devframe-validation-final,browser.click,279.296,97,57 -devframe-validation-final,browser.snapshot,567.7,18,461 -devframe-validation-final,browser.batch,1056.206,261,610 -devframe-validation-final,browser.fill,326.245,118,56 -devframe-validation-final,browser.click,300.213,97,57 -devframe-validation-final,browser.snapshot,619.546,18,461 -devframe-validation-final,browser.batch,1288.024,261,610 -devframe-validation-final,browser.click:blocked,2001.495,97,197 -devframe-validation-final,browser.click:blocked,2002.566,97,197 -devframe-validation-final,browser.click:blocked,2002.679,97,197 -devframe-validation-final,browser.click:blocked,2001.582,97,197 -devframe-validation-final,browser.click:blocked,2002.806,97,197 -devframe-validation-final,browser.click:blocked,2001.927,97,197 -devframe-validation-final,browser.click:blocked,2002.821,97,197 -devframe-validation-final,browser.click:blocked,2002.086,97,197 -devframe-validation-final,browser.click:blocked,2001.052,97,197 -devframe-validation-final,browser.click:blocked,2003.153,97,197 -devframe-validation-final,browser.click:blocked,2039.416,97,197 -devframe-validation-final,browser.click:blocked,2000.925,97,197 -devframe-validation-final,browser.click:blocked,2002.806,97,197 -devframe-validation-final,browser.click:blocked,2001.481,97,197 -devframe-validation-final,browser.click:blocked,2000.973,97,197 -devframe-validation-final,browser.click:blocked,2001.387,97,197 -devframe-validation-final,browser.click:blocked,2002.231,97,197 -devframe-validation-final,browser.click:blocked,2002.097,97,197 -devframe-validation-final,browser.click:blocked,2001.894,97,197 -devframe-validation-final,browser.click:blocked,2002.91,97,197 -devframe-validation-final,browser.click:blocked,2001.65,97,197 -devframe-validation-final,browser.click:blocked,2001.797,97,197 -devframe-validation-final,browser.click:blocked,2000.906,97,197 -devframe-validation-final,browser.click:blocked,2002.241,97,197 -devframe-validation-final,browser.click:blocked,2010.802,97,197 -devframe-validation-final,browser.click:blocked,2002.194,97,197 -devframe-validation-final,browser.click:blocked,2002.11,97,197 -devframe-validation-final,browser.click:blocked,2001.157,97,197 -devframe-validation-final,browser.click:blocked,2000.562,97,197 -devframe-validation-final,browser.click:blocked,2001.68,97,197 diff --git a/docs/benchmarks/2026-09-08-latency.md b/docs/benchmarks/2026-09-08-latency.md index 80eaba1..7b32c30 100644 --- a/docs/benchmarks/2026-09-08-latency.md +++ b/docs/benchmarks/2026-09-08-latency.md @@ -1,6 +1,6 @@ # Native action deadlines and discovery measurements -Recorded on 8 September 2026 with Chromium 151.0.7922.34, Node 24.20.0, macOS arm64, Apple M3 Pro (11 logical processors). The normal public fixture contains 10,000 DOM nodes, 200 levels, 20 closed shadow roots and three cross-origin frame levels. Each reported scenario has 30 warm samples. Browser startup, fixture loading and model time are excluded; CDB processing and readiness checks are included. [Raw samples](./2026-09-08-latency.csv) retain the failed performance run as well as successful runs. +Recorded on 8 September 2026 with Chromium 151.0.7922.34, Node 24.20.0, macOS arm64, Apple M3 Pro (11 logical processors). The normal public fixture contains 10,000 DOM nodes, 200 levels, 20 closed shadow roots and three cross-origin frame levels. Each reported scenario has 30 warm samples. Browser startup, fixture loading and model time are excluded; CDB processing and readiness checks are included. This summary includes failed and successful runs. ## Devframe RPC workflow diff --git a/docs/benchmarks/2026-09-09-aligned.csv b/docs/benchmarks/2026-09-09-aligned.csv deleted file mode 100644 index 6e0ca0e..0000000 --- a/docs/benchmarks/2026-09-09-aligned.csv +++ /dev/null @@ -1,2380 +0,0 @@ -run,transport,selector,operation,milliseconds,arguments_characters,response_characters,error -cdb-aligned-websocket-before-run2,websocket,ref,browser.fill,100.850,55,56,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.click,96.317,30,57,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.snapshot,652.538,18,456,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.batch,799.477,131,605,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.fill,117.591,55,56,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.click,132.298,30,57,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.snapshot,619.824,18,456,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.batch,993.632,131,605,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.fill,103.954,55,56,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.click,97.728,30,57,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.snapshot,605.533,18,456,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.batch,742.676,131,605,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.fill,95.391,55,56,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.click,88.826,30,57,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.snapshot,570.927,18,456,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.batch,767.012,131,605,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.fill,102.022,55,56,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.click,87.729,30,57,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.snapshot,605.491,18,457,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.batch,824.714,132,610,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.fill,190.978,56,56,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.click,95.694,31,57,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.snapshot,744.312,18,461,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.batch,834.009,133,610,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.fill,112.083,56,56,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.click,92.218,31,57,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.snapshot,621.944,18,461,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.batch,1073.002,133,610,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.fill,122.802,56,56,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.click,186.925,31,57,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.snapshot,727.573,18,461,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.batch,862.100,133,610,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.fill,108.047,56,56,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.click,99.181,31,57,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.snapshot,746.659,18,461,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.batch,953.051,133,610,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.fill,155.394,56,56,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.click,108.964,31,57,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.snapshot,647.160,18,461,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.batch,908.220,133,610,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.fill,105.692,56,56,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.click,101.530,31,57,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.snapshot,624.866,18,461,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.batch,796.727,133,610,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.fill,102.733,56,56,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.click,104.418,31,57,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.snapshot,654.086,18,461,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.batch,838.757,133,610,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.fill,154.159,56,56,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.click,98.820,31,57,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.snapshot,598.025,18,461,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.batch,817.824,133,610,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.fill,159.905,56,56,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.click,113.951,31,57,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.snapshot,731.802,18,461,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.batch,1255.345,133,610,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.fill,101.783,56,56,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.click,93.759,31,57,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.snapshot,640.061,18,461,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.batch,807.659,133,610,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.fill,112.377,56,56,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.click,89.560,31,57,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.snapshot,599.714,18,461,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.batch,1085.676,133,610,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.fill,115.044,56,56,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.click,110.727,31,57,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.snapshot,601.245,18,461,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.batch,747.312,133,610,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.fill,98.809,56,56,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.click,91.286,31,57,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.snapshot,636.386,18,461,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.batch,780.098,133,610,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.fill,107.297,56,56,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.click,98.998,31,57,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.snapshot,752.409,18,461,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.batch,1009.203,133,610,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.fill,110.325,56,56,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.click,94.237,31,57,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.snapshot,650.834,18,461,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.batch,809.465,133,610,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.fill,101.151,56,56,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.click,91.906,31,57,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.snapshot,600.580,18,461,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.batch,899.548,133,610,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.fill,110.537,56,56,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.click,94.465,31,57,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.snapshot,630.301,18,461,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.batch,1096.836,133,610,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.fill,100.919,56,56,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.click,105.728,31,57,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.snapshot,621.696,18,461,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.batch,791.732,133,610,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.fill,115.694,56,56,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.click,95.919,31,57,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.snapshot,680.596,18,461,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.batch,829.249,133,610,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.fill,115.115,56,56,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.click,90.904,31,57,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.snapshot,648.420,18,461,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.batch,1838.261,133,610,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.fill,203.936,56,56,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.click,163.304,31,57,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.snapshot,921.656,18,461,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.batch,914.516,133,610,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.fill,105.251,56,56,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.click,105.509,31,57,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.snapshot,679.719,18,461,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.batch,795.705,133,610,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.fill,100.493,56,56,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.click,94.186,31,57,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.snapshot,653.524,18,461,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.batch,804.935,133,610,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.fill,106.244,56,56,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.click,109.664,31,57,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.snapshot,663.295,18,461,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.batch,849.003,133,610,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.fill,97.857,56,56,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.click,95.893,31,57,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.snapshot,620.881,18,461,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.batch,876.588,133,610,false -cdb-aligned-websocket-before-run2,websocket,ref,browser.click:blocked,2001.939,31,172,true -cdb-aligned-websocket-before-run2,websocket,ref,browser.click:blocked,2000.123,31,172,true -cdb-aligned-websocket-before-run2,websocket,ref,browser.click:blocked,2000.814,31,172,true -cdb-aligned-websocket-before-run2,websocket,ref,browser.click:blocked,2002.306,31,172,true -cdb-aligned-websocket-before-run2,websocket,ref,browser.click:blocked,2001.079,31,172,true -cdb-aligned-websocket-before-run2,websocket,ref,browser.click:blocked,1999.650,31,172,true -cdb-aligned-websocket-before-run2,websocket,ref,browser.click:blocked,2002.160,31,172,true -cdb-aligned-websocket-before-run2,websocket,ref,browser.click:blocked,2002.765,31,172,true -cdb-aligned-websocket-before-run2,websocket,ref,browser.click:blocked,2002.350,31,172,true -cdb-aligned-websocket-before-run2,websocket,ref,browser.click:blocked,2001.590,31,172,true -cdb-aligned-websocket-before-run2,websocket,ref,browser.click:blocked,2002.408,31,172,true -cdb-aligned-websocket-before-run2,websocket,ref,browser.click:blocked,2001.390,31,172,true -cdb-aligned-websocket-before-run2,websocket,ref,browser.click:blocked,2001.619,31,172,true -cdb-aligned-websocket-before-run2,websocket,ref,browser.click:blocked,2006.222,31,172,true -cdb-aligned-websocket-before-run2,websocket,ref,browser.click:blocked,2000.315,31,172,true -cdb-aligned-websocket-before-run2,websocket,ref,browser.click:blocked,2001.311,31,172,true -cdb-aligned-websocket-before-run2,websocket,ref,browser.click:blocked,2000.537,31,172,true -cdb-aligned-websocket-before-run2,websocket,ref,browser.click:blocked,2003.014,31,172,true -cdb-aligned-websocket-before-run2,websocket,ref,browser.click:blocked,2002.922,31,172,true -cdb-aligned-websocket-before-run2,websocket,ref,browser.click:blocked,2002.310,31,172,true -cdb-aligned-websocket-before-run2,websocket,ref,browser.click:blocked,2002.079,31,172,true -cdb-aligned-websocket-before-run2,websocket,ref,browser.click:blocked,2002.015,31,172,true -cdb-aligned-websocket-before-run2,websocket,ref,browser.click:blocked,2000.808,31,172,true -cdb-aligned-websocket-before-run2,websocket,ref,browser.click:blocked,2003.238,31,172,true -cdb-aligned-websocket-before-run2,websocket,ref,browser.click:blocked,2001.955,31,172,true -cdb-aligned-websocket-before-run2,websocket,ref,browser.click:blocked,2001.658,31,172,true -cdb-aligned-websocket-before-run2,websocket,ref,browser.click:blocked,2000.696,31,172,true -cdb-aligned-websocket-before-run2,websocket,ref,browser.click:blocked,2001.753,31,172,true -cdb-aligned-websocket-before-run2,websocket,ref,browser.click:blocked,2002.687,31,172,true -cdb-aligned-websocket-before-run2,websocket,ref,browser.click:blocked,2003.001,31,172,true -cdb-aligned-websocket-before-run2,websocket,locator,browser.fill,644.083,118,56,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.click,286.249,97,57,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.snapshot,700.771,18,461,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.batch,988.665,261,610,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.fill,208.793,118,56,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.click,227.215,97,57,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.snapshot,607.240,18,461,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.batch,1275.969,261,610,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.fill,215.072,118,56,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.click,188.187,97,57,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.snapshot,854.498,18,461,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.batch,908.889,261,610,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.fill,203.706,118,56,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.click,188.000,97,57,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.snapshot,748.006,18,461,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.batch,1025.232,261,610,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.fill,205.680,118,56,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.click,198.003,97,57,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.snapshot,646.509,18,461,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.batch,1078.249,261,610,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.fill,212.226,118,56,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.click,179.524,97,57,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.snapshot,592.072,18,461,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.batch,860.039,261,610,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.fill,195.687,118,56,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.click,174.439,97,57,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.snapshot,608.519,18,461,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.batch,860.894,261,610,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.fill,189.663,118,56,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.click,175.308,97,57,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.snapshot,584.770,18,461,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.batch,863.129,261,610,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.fill,195.050,118,56,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.click,181.961,97,57,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.snapshot,576.311,18,461,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.batch,919.936,261,610,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.fill,203.526,118,56,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.click,187.130,97,57,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.snapshot,597.039,18,461,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.batch,858.232,261,610,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.fill,190.644,118,56,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.click,173.140,97,57,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.snapshot,620.674,18,461,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.batch,863.546,261,610,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.fill,190.270,118,56,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.click,177.798,97,57,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.snapshot,597.355,18,461,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.batch,858.176,261,610,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.fill,194.374,118,56,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.click,188.062,97,57,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.snapshot,634.014,18,461,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.batch,891.770,261,610,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.fill,203.661,118,56,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.click,181.900,97,57,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.snapshot,605.101,18,461,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.batch,893.355,261,610,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.fill,197.527,118,56,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.click,182.822,97,57,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.snapshot,612.556,18,461,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.batch,862.920,261,610,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.fill,195.462,118,56,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.click,177.294,97,57,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.snapshot,581.507,18,461,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.batch,910.839,261,610,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.fill,202.803,118,56,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.click,176.560,97,57,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.snapshot,573.069,18,461,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.batch,877.780,261,610,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.fill,192.100,118,56,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.click,179.809,97,57,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.snapshot,585.387,18,461,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.batch,883.730,261,610,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.fill,192.374,118,56,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.click,176.698,97,57,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.snapshot,587.039,18,461,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.batch,891.264,261,610,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.fill,194.365,118,56,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.click,194.201,97,57,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.snapshot,608.139,18,461,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.batch,854.655,261,610,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.fill,193.740,118,56,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.click,176.155,97,57,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.snapshot,634.267,18,461,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.batch,895.048,261,610,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.fill,201.982,118,56,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.click,185.460,97,57,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.snapshot,643.023,18,461,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.batch,900.368,261,610,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.fill,198.589,118,56,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.click,229.749,97,57,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.snapshot,901.770,18,461,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.batch,1254.013,261,610,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.fill,247.359,118,56,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.click,270.576,97,57,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.snapshot,1483.972,18,461,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.batch,1716.991,261,610,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.fill,266.970,118,56,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.click,287.017,97,57,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.snapshot,809.155,18,461,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.batch,1085.140,261,610,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.fill,315.629,118,56,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.click,245.230,97,57,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.snapshot,680.485,18,461,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.batch,946.977,261,610,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.fill,339.351,118,56,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.click,186.384,97,57,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.snapshot,608.960,18,461,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.batch,961.514,261,610,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.fill,202.998,118,56,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.click,176.249,97,57,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.snapshot,682.514,18,461,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.batch,926.268,261,610,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.fill,205.884,118,56,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.click,181.272,97,57,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.snapshot,581.463,18,461,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.batch,852.156,261,610,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.fill,194.727,118,56,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.click,193.143,97,57,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.snapshot,599.226,18,461,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.batch,870.157,261,610,false -cdb-aligned-websocket-before-run2,websocket,locator,browser.click:blocked,2000.409,97,172,true -cdb-aligned-websocket-before-run2,websocket,locator,browser.click:blocked,2002.386,97,172,true -cdb-aligned-websocket-before-run2,websocket,locator,browser.click:blocked,2003.197,97,172,true -cdb-aligned-websocket-before-run2,websocket,locator,browser.click:blocked,2001.514,97,172,true -cdb-aligned-websocket-before-run2,websocket,locator,browser.click:blocked,2000.527,97,172,true -cdb-aligned-websocket-before-run2,websocket,locator,browser.click:blocked,2001.392,97,172,true -cdb-aligned-websocket-before-run2,websocket,locator,browser.click:blocked,2001.921,97,172,true -cdb-aligned-websocket-before-run2,websocket,locator,browser.click:blocked,2000.817,97,172,true -cdb-aligned-websocket-before-run2,websocket,locator,browser.click:blocked,2000.693,97,172,true -cdb-aligned-websocket-before-run2,websocket,locator,browser.click:blocked,2001.429,97,172,true -cdb-aligned-websocket-before-run2,websocket,locator,browser.click:blocked,2001.018,97,172,true -cdb-aligned-websocket-before-run2,websocket,locator,browser.click:blocked,2002.225,97,172,true -cdb-aligned-websocket-before-run2,websocket,locator,browser.click:blocked,2000.536,97,172,true -cdb-aligned-websocket-before-run2,websocket,locator,browser.click:blocked,2001.945,97,172,true -cdb-aligned-websocket-before-run2,websocket,locator,browser.click:blocked,1999.834,97,172,true -cdb-aligned-websocket-before-run2,websocket,locator,browser.click:blocked,2001.935,97,172,true -cdb-aligned-websocket-before-run2,websocket,locator,browser.click:blocked,2000.458,97,172,true -cdb-aligned-websocket-before-run2,websocket,locator,browser.click:blocked,2003.039,97,172,true -cdb-aligned-websocket-before-run2,websocket,locator,browser.click:blocked,2025.623,97,172,true -cdb-aligned-websocket-before-run2,websocket,locator,browser.click:blocked,2005.278,97,172,true -cdb-aligned-websocket-before-run2,websocket,locator,browser.click:blocked,2000.817,97,172,true -cdb-aligned-websocket-before-run2,websocket,locator,browser.click:blocked,2001.227,97,172,true -cdb-aligned-websocket-before-run2,websocket,locator,browser.click:blocked,2002.003,97,172,true -cdb-aligned-websocket-before-run2,websocket,locator,browser.click:blocked,2000.952,97,172,true -cdb-aligned-websocket-before-run2,websocket,locator,browser.click:blocked,2001.079,97,172,true -cdb-aligned-websocket-before-run2,websocket,locator,browser.click:blocked,2000.728,97,172,true -cdb-aligned-websocket-before-run2,websocket,locator,browser.click:blocked,2001.198,97,172,true -cdb-aligned-websocket-before-run2,websocket,locator,browser.click:blocked,2002.719,97,172,true -cdb-aligned-websocket-before-run2,websocket,locator,browser.click:blocked,2000.377,97,172,true -cdb-aligned-websocket-before-run2,websocket,locator,browser.click:blocked,2001.766,97,172,true -cdb-aligned-devframe-before,devframe,ref,browser.fill,104.066,55,56,false -cdb-aligned-devframe-before,devframe,ref,browser.click,93.529,30,57,false -cdb-aligned-devframe-before,devframe,ref,browser.snapshot,730.327,18,456,false -cdb-aligned-devframe-before,devframe,ref,browser.batch,753.342,131,605,false -cdb-aligned-devframe-before,devframe,ref,browser.fill,100.512,55,56,false -cdb-aligned-devframe-before,devframe,ref,browser.click,84.664,30,57,false -cdb-aligned-devframe-before,devframe,ref,browser.snapshot,575.800,18,456,false -cdb-aligned-devframe-before,devframe,ref,browser.batch,711.061,131,605,false -cdb-aligned-devframe-before,devframe,ref,browser.fill,94.192,55,56,false -cdb-aligned-devframe-before,devframe,ref,browser.click,88.922,30,57,false -cdb-aligned-devframe-before,devframe,ref,browser.snapshot,614.592,18,456,false -cdb-aligned-devframe-before,devframe,ref,browser.batch,837.354,131,605,false -cdb-aligned-devframe-before,devframe,ref,browser.fill,100.221,55,56,false -cdb-aligned-devframe-before,devframe,ref,browser.click,115.483,30,57,false -cdb-aligned-devframe-before,devframe,ref,browser.snapshot,714.280,18,456,false -cdb-aligned-devframe-before,devframe,ref,browser.batch,751.249,131,605,false -cdb-aligned-devframe-before,devframe,ref,browser.fill,99.544,55,56,false -cdb-aligned-devframe-before,devframe,ref,browser.click,87.867,30,57,false -cdb-aligned-devframe-before,devframe,ref,browser.snapshot,596.735,18,457,false -cdb-aligned-devframe-before,devframe,ref,browser.batch,741.387,132,610,false -cdb-aligned-devframe-before,devframe,ref,browser.fill,94.342,56,56,false -cdb-aligned-devframe-before,devframe,ref,browser.click,89.745,31,57,false -cdb-aligned-devframe-before,devframe,ref,browser.snapshot,660.726,18,461,false -cdb-aligned-devframe-before,devframe,ref,browser.batch,812.642,133,610,false -cdb-aligned-devframe-before,devframe,ref,browser.fill,104.339,56,56,false -cdb-aligned-devframe-before,devframe,ref,browser.click,89.699,31,57,false -cdb-aligned-devframe-before,devframe,ref,browser.snapshot,622.521,18,461,false -cdb-aligned-devframe-before,devframe,ref,browser.batch,906.388,133,610,false -cdb-aligned-devframe-before,devframe,ref,browser.fill,96.110,56,56,false -cdb-aligned-devframe-before,devframe,ref,browser.click,83.051,31,57,false -cdb-aligned-devframe-before,devframe,ref,browser.snapshot,579.372,18,461,false -cdb-aligned-devframe-before,devframe,ref,browser.batch,749.656,133,610,false -cdb-aligned-devframe-before,devframe,ref,browser.fill,88.501,56,56,false -cdb-aligned-devframe-before,devframe,ref,browser.click,82.776,31,57,false -cdb-aligned-devframe-before,devframe,ref,browser.snapshot,590.464,18,461,false -cdb-aligned-devframe-before,devframe,ref,browser.batch,769.605,133,610,false -cdb-aligned-devframe-before,devframe,ref,browser.fill,95.667,56,56,false -cdb-aligned-devframe-before,devframe,ref,browser.click,83.181,31,57,false -cdb-aligned-devframe-before,devframe,ref,browser.snapshot,651.451,18,461,false -cdb-aligned-devframe-before,devframe,ref,browser.batch,810.476,133,610,false -cdb-aligned-devframe-before,devframe,ref,browser.fill,105.964,56,56,false -cdb-aligned-devframe-before,devframe,ref,browser.click,112.907,31,57,false -cdb-aligned-devframe-before,devframe,ref,browser.snapshot,651.291,18,461,false -cdb-aligned-devframe-before,devframe,ref,browser.batch,930.393,133,610,false -cdb-aligned-devframe-before,devframe,ref,browser.fill,108.655,56,56,false -cdb-aligned-devframe-before,devframe,ref,browser.click,111.568,31,57,false -cdb-aligned-devframe-before,devframe,ref,browser.snapshot,1012.675,18,461,false -cdb-aligned-devframe-before,devframe,ref,browser.batch,791.056,133,610,false -cdb-aligned-devframe-before,devframe,ref,browser.fill,130.655,56,56,false -cdb-aligned-devframe-before,devframe,ref,browser.click,90.408,31,57,false -cdb-aligned-devframe-before,devframe,ref,browser.snapshot,615.943,18,461,false -cdb-aligned-devframe-before,devframe,ref,browser.batch,786.404,133,610,false -cdb-aligned-devframe-before,devframe,ref,browser.fill,98.144,56,56,false -cdb-aligned-devframe-before,devframe,ref,browser.click,83.996,31,57,false -cdb-aligned-devframe-before,devframe,ref,browser.snapshot,613.391,18,461,false -cdb-aligned-devframe-before,devframe,ref,browser.batch,761.260,133,610,false -cdb-aligned-devframe-before,devframe,ref,browser.fill,96.506,56,56,false -cdb-aligned-devframe-before,devframe,ref,browser.click,87.619,31,57,false -cdb-aligned-devframe-before,devframe,ref,browser.snapshot,634.891,18,461,false -cdb-aligned-devframe-before,devframe,ref,browser.batch,801.653,133,610,false -cdb-aligned-devframe-before,devframe,ref,browser.fill,91.649,56,56,false -cdb-aligned-devframe-before,devframe,ref,browser.click,87.699,31,57,false -cdb-aligned-devframe-before,devframe,ref,browser.snapshot,594.566,18,461,false -cdb-aligned-devframe-before,devframe,ref,browser.batch,734.554,133,610,false -cdb-aligned-devframe-before,devframe,ref,browser.fill,89.940,56,56,false -cdb-aligned-devframe-before,devframe,ref,browser.click,106.134,31,57,false -cdb-aligned-devframe-before,devframe,ref,browser.snapshot,608.549,18,461,false -cdb-aligned-devframe-before,devframe,ref,browser.batch,756.881,133,610,false -cdb-aligned-devframe-before,devframe,ref,browser.fill,93.339,56,56,false -cdb-aligned-devframe-before,devframe,ref,browser.click,85.273,31,57,false -cdb-aligned-devframe-before,devframe,ref,browser.snapshot,600.843,18,461,false -cdb-aligned-devframe-before,devframe,ref,browser.batch,795.403,133,610,false -cdb-aligned-devframe-before,devframe,ref,browser.fill,94.342,56,56,false -cdb-aligned-devframe-before,devframe,ref,browser.click,92.417,31,57,false -cdb-aligned-devframe-before,devframe,ref,browser.snapshot,619.168,18,461,false -cdb-aligned-devframe-before,devframe,ref,browser.batch,772.628,133,610,false -cdb-aligned-devframe-before,devframe,ref,browser.fill,96.869,56,56,false -cdb-aligned-devframe-before,devframe,ref,browser.click,83.843,31,57,false -cdb-aligned-devframe-before,devframe,ref,browser.snapshot,613.331,18,461,false -cdb-aligned-devframe-before,devframe,ref,browser.batch,746.664,133,610,false -cdb-aligned-devframe-before,devframe,ref,browser.fill,88.428,56,56,false -cdb-aligned-devframe-before,devframe,ref,browser.click,82.767,31,57,false -cdb-aligned-devframe-before,devframe,ref,browser.snapshot,573.358,18,461,false -cdb-aligned-devframe-before,devframe,ref,browser.batch,726.509,133,610,false -cdb-aligned-devframe-before,devframe,ref,browser.fill,93.237,56,56,false -cdb-aligned-devframe-before,devframe,ref,browser.click,88.080,31,57,false -cdb-aligned-devframe-before,devframe,ref,browser.snapshot,565.703,18,461,false -cdb-aligned-devframe-before,devframe,ref,browser.batch,745.814,133,610,false -cdb-aligned-devframe-before,devframe,ref,browser.fill,93.573,56,56,false -cdb-aligned-devframe-before,devframe,ref,browser.click,89.406,31,57,false -cdb-aligned-devframe-before,devframe,ref,browser.snapshot,572.814,18,461,false -cdb-aligned-devframe-before,devframe,ref,browser.batch,720.655,133,610,false -cdb-aligned-devframe-before,devframe,ref,browser.fill,98.156,56,56,false -cdb-aligned-devframe-before,devframe,ref,browser.click,82.908,31,57,false -cdb-aligned-devframe-before,devframe,ref,browser.snapshot,589.519,18,461,false -cdb-aligned-devframe-before,devframe,ref,browser.batch,715.673,133,610,false -cdb-aligned-devframe-before,devframe,ref,browser.fill,89.456,56,56,false -cdb-aligned-devframe-before,devframe,ref,browser.click,82.304,31,57,false -cdb-aligned-devframe-before,devframe,ref,browser.snapshot,567.874,18,461,false -cdb-aligned-devframe-before,devframe,ref,browser.batch,732.491,133,610,false -cdb-aligned-devframe-before,devframe,ref,browser.fill,89.008,56,56,false -cdb-aligned-devframe-before,devframe,ref,browser.click,84.414,31,57,false -cdb-aligned-devframe-before,devframe,ref,browser.snapshot,580.061,18,461,false -cdb-aligned-devframe-before,devframe,ref,browser.batch,726.630,133,610,false -cdb-aligned-devframe-before,devframe,ref,browser.fill,89.356,56,56,false -cdb-aligned-devframe-before,devframe,ref,browser.click,86.130,31,57,false -cdb-aligned-devframe-before,devframe,ref,browser.snapshot,565.131,18,461,false -cdb-aligned-devframe-before,devframe,ref,browser.batch,746.996,133,610,false -cdb-aligned-devframe-before,devframe,ref,browser.fill,93.723,56,56,false -cdb-aligned-devframe-before,devframe,ref,browser.click,88.652,31,57,false -cdb-aligned-devframe-before,devframe,ref,browser.snapshot,594.171,18,461,false -cdb-aligned-devframe-before,devframe,ref,browser.batch,740.821,133,610,false -cdb-aligned-devframe-before,devframe,ref,browser.fill,94.555,56,56,false -cdb-aligned-devframe-before,devframe,ref,browser.click,84.390,31,57,false -cdb-aligned-devframe-before,devframe,ref,browser.snapshot,578.605,18,461,false -cdb-aligned-devframe-before,devframe,ref,browser.batch,726.696,133,610,false -cdb-aligned-devframe-before,devframe,ref,browser.fill,88.792,56,56,false -cdb-aligned-devframe-before,devframe,ref,browser.click,82.149,31,57,false -cdb-aligned-devframe-before,devframe,ref,browser.snapshot,581.087,18,461,false -cdb-aligned-devframe-before,devframe,ref,browser.batch,717.663,133,610,false -cdb-aligned-devframe-before,devframe,ref,browser.click:blocked,2005.463,31,197,true -cdb-aligned-devframe-before,devframe,ref,browser.click:blocked,2004.453,31,197,true -cdb-aligned-devframe-before,devframe,ref,browser.click:blocked,2003.356,31,197,true -cdb-aligned-devframe-before,devframe,ref,browser.click:blocked,2002.313,31,197,true -cdb-aligned-devframe-before,devframe,ref,browser.click:blocked,2005.888,31,197,true -cdb-aligned-devframe-before,devframe,ref,browser.click:blocked,2003.654,31,197,true -cdb-aligned-devframe-before,devframe,ref,browser.click:blocked,2008.438,31,197,true -cdb-aligned-devframe-before,devframe,ref,browser.click:blocked,2004.979,31,197,true -cdb-aligned-devframe-before,devframe,ref,browser.click:blocked,2004.797,31,197,true -cdb-aligned-devframe-before,devframe,ref,browser.click:blocked,2010.147,31,197,true -cdb-aligned-devframe-before,devframe,ref,browser.click:blocked,2004.570,31,197,true -cdb-aligned-devframe-before,devframe,ref,browser.click:blocked,2003.098,31,197,true -cdb-aligned-devframe-before,devframe,ref,browser.click:blocked,2006.892,31,197,true -cdb-aligned-devframe-before,devframe,ref,browser.click:blocked,2003.687,31,197,true -cdb-aligned-devframe-before,devframe,ref,browser.click:blocked,2004.773,31,197,true -cdb-aligned-devframe-before,devframe,ref,browser.click:blocked,2001.920,31,197,true -cdb-aligned-devframe-before,devframe,ref,browser.click:blocked,2005.558,31,197,true -cdb-aligned-devframe-before,devframe,ref,browser.click:blocked,2005.765,31,197,true -cdb-aligned-devframe-before,devframe,ref,browser.click:blocked,2001.628,31,197,true -cdb-aligned-devframe-before,devframe,ref,browser.click:blocked,2005.079,31,197,true -cdb-aligned-devframe-before,devframe,ref,browser.click:blocked,2009.399,31,197,true -cdb-aligned-devframe-before,devframe,ref,browser.click:blocked,2005.163,31,197,true -cdb-aligned-devframe-before,devframe,ref,browser.click:blocked,2002.479,31,197,true -cdb-aligned-devframe-before,devframe,ref,browser.click:blocked,2006.422,31,197,true -cdb-aligned-devframe-before,devframe,ref,browser.click:blocked,2002.785,31,197,true -cdb-aligned-devframe-before,devframe,ref,browser.click:blocked,2004.916,31,197,true -cdb-aligned-devframe-before,devframe,ref,browser.click:blocked,2004.614,31,197,true -cdb-aligned-devframe-before,devframe,ref,browser.click:blocked,2004.111,31,197,true -cdb-aligned-devframe-before,devframe,ref,browser.click:blocked,2001.204,31,197,true -cdb-aligned-devframe-before,devframe,ref,browser.click:blocked,2001.942,31,197,true -cdb-aligned-devframe-before,devframe,locator,browser.fill,307.098,118,56,false -cdb-aligned-devframe-before,devframe,locator,browser.click,446.050,97,57,false -cdb-aligned-devframe-before,devframe,locator,browser.snapshot,668.532,18,461,false -cdb-aligned-devframe-before,devframe,locator,browser.batch,1542.640,261,610,false -cdb-aligned-devframe-before,devframe,locator,browser.fill,294.148,118,56,false -cdb-aligned-devframe-before,devframe,locator,browser.click,265.277,97,57,false -cdb-aligned-devframe-before,devframe,locator,browser.snapshot,704.455,18,461,false -cdb-aligned-devframe-before,devframe,locator,browser.batch,1127.458,261,610,false -cdb-aligned-devframe-before,devframe,locator,browser.fill,274.874,118,56,false -cdb-aligned-devframe-before,devframe,locator,browser.click,333.387,97,57,false -cdb-aligned-devframe-before,devframe,locator,browser.snapshot,679.520,18,461,false -cdb-aligned-devframe-before,devframe,locator,browser.batch,1858.327,261,610,false -cdb-aligned-devframe-before,devframe,locator,browser.fill,322.065,118,56,false -cdb-aligned-devframe-before,devframe,locator,browser.click,348.484,97,57,false -cdb-aligned-devframe-before,devframe,locator,browser.snapshot,767.463,18,461,false -cdb-aligned-devframe-before,devframe,locator,browser.batch,1224.699,261,610,false -cdb-aligned-devframe-before,devframe,locator,browser.fill,345.412,118,56,false -cdb-aligned-devframe-before,devframe,locator,browser.click,456.265,97,57,false -cdb-aligned-devframe-before,devframe,locator,browser.snapshot,859.477,18,461,false -cdb-aligned-devframe-before,devframe,locator,browser.batch,1052.461,261,610,false -cdb-aligned-devframe-before,devframe,locator,browser.fill,324.846,118,56,false -cdb-aligned-devframe-before,devframe,locator,browser.click,339.914,97,57,false -cdb-aligned-devframe-before,devframe,locator,browser.snapshot,603.646,18,461,false -cdb-aligned-devframe-before,devframe,locator,browser.batch,1119.891,261,610,false -cdb-aligned-devframe-before,devframe,locator,browser.fill,364.026,118,56,false -cdb-aligned-devframe-before,devframe,locator,browser.click,291.372,97,57,false -cdb-aligned-devframe-before,devframe,locator,browser.snapshot,695.998,18,461,false -cdb-aligned-devframe-before,devframe,locator,browser.batch,1121.545,261,610,false -cdb-aligned-devframe-before,devframe,locator,browser.fill,344.368,118,56,false -cdb-aligned-devframe-before,devframe,locator,browser.click,275.336,97,57,false -cdb-aligned-devframe-before,devframe,locator,browser.snapshot,590.111,18,461,false -cdb-aligned-devframe-before,devframe,locator,browser.batch,1085.547,261,610,false -cdb-aligned-devframe-before,devframe,locator,browser.fill,331.611,118,56,false -cdb-aligned-devframe-before,devframe,locator,browser.click,349.221,97,57,false -cdb-aligned-devframe-before,devframe,locator,browser.snapshot,567.709,18,461,false -cdb-aligned-devframe-before,devframe,locator,browser.batch,1261.126,261,610,false -cdb-aligned-devframe-before,devframe,locator,browser.fill,437.917,118,56,false -cdb-aligned-devframe-before,devframe,locator,browser.click,441.670,97,57,false -cdb-aligned-devframe-before,devframe,locator,browser.snapshot,614.084,18,461,false -cdb-aligned-devframe-before,devframe,locator,browser.batch,1278.103,261,610,false -cdb-aligned-devframe-before,devframe,locator,browser.fill,357.383,118,56,false -cdb-aligned-devframe-before,devframe,locator,browser.click,370.424,97,57,false -cdb-aligned-devframe-before,devframe,locator,browser.snapshot,571.920,18,461,false -cdb-aligned-devframe-before,devframe,locator,browser.batch,1229.091,261,610,false -cdb-aligned-devframe-before,devframe,locator,browser.fill,323.656,118,56,false -cdb-aligned-devframe-before,devframe,locator,browser.click,355.424,97,57,false -cdb-aligned-devframe-before,devframe,locator,browser.snapshot,611.128,18,461,false -cdb-aligned-devframe-before,devframe,locator,browser.batch,1291.342,261,610,false -cdb-aligned-devframe-before,devframe,locator,browser.fill,406.162,118,56,false -cdb-aligned-devframe-before,devframe,locator,browser.click,344.034,97,57,false -cdb-aligned-devframe-before,devframe,locator,browser.snapshot,581.763,18,461,false -cdb-aligned-devframe-before,devframe,locator,browser.batch,1293.550,261,610,false -cdb-aligned-devframe-before,devframe,locator,browser.fill,424.984,118,56,false -cdb-aligned-devframe-before,devframe,locator,browser.click,456.078,97,57,false -cdb-aligned-devframe-before,devframe,locator,browser.snapshot,623.690,18,461,false -cdb-aligned-devframe-before,devframe,locator,browser.batch,1126.250,261,610,false -cdb-aligned-devframe-before,devframe,locator,browser.fill,415.438,118,56,false -cdb-aligned-devframe-before,devframe,locator,browser.click,376.528,97,57,false -cdb-aligned-devframe-before,devframe,locator,browser.snapshot,632.152,18,461,false -cdb-aligned-devframe-before,devframe,locator,browser.batch,1265.944,261,610,false -cdb-aligned-devframe-before,devframe,locator,browser.fill,433.839,118,56,false -cdb-aligned-devframe-before,devframe,locator,browser.click,374.878,97,57,false -cdb-aligned-devframe-before,devframe,locator,browser.snapshot,633.119,18,461,false -cdb-aligned-devframe-before,devframe,locator,browser.batch,1149.099,261,610,false -cdb-aligned-devframe-before,devframe,locator,browser.fill,326.585,118,56,false -cdb-aligned-devframe-before,devframe,locator,browser.click,407.169,97,57,false -cdb-aligned-devframe-before,devframe,locator,browser.snapshot,646.519,18,461,false -cdb-aligned-devframe-before,devframe,locator,browser.batch,1285.977,261,610,false -cdb-aligned-devframe-before,devframe,locator,browser.fill,429.891,118,56,false -cdb-aligned-devframe-before,devframe,locator,browser.click,438.233,97,57,false -cdb-aligned-devframe-before,devframe,locator,browser.snapshot,631.215,18,461,false -cdb-aligned-devframe-before,devframe,locator,browser.batch,1215.314,261,610,false -cdb-aligned-devframe-before,devframe,locator,browser.fill,420.812,118,56,false -cdb-aligned-devframe-before,devframe,locator,browser.click,432.174,97,57,false -cdb-aligned-devframe-before,devframe,locator,browser.snapshot,664.959,18,461,false -cdb-aligned-devframe-before,devframe,locator,browser.batch,1257.799,261,610,false -cdb-aligned-devframe-before,devframe,locator,browser.fill,326.526,118,56,false -cdb-aligned-devframe-before,devframe,locator,browser.click,432.816,97,57,false -cdb-aligned-devframe-before,devframe,locator,browser.snapshot,624.788,18,461,false -cdb-aligned-devframe-before,devframe,locator,browser.batch,1251.837,261,610,false -cdb-aligned-devframe-before,devframe,locator,browser.fill,371.122,118,56,false -cdb-aligned-devframe-before,devframe,locator,browser.click,336.903,97,57,false -cdb-aligned-devframe-before,devframe,locator,browser.snapshot,592.021,18,461,false -cdb-aligned-devframe-before,devframe,locator,browser.batch,1330.396,261,610,false -cdb-aligned-devframe-before,devframe,locator,browser.fill,332.089,118,56,false -cdb-aligned-devframe-before,devframe,locator,browser.click,273.460,97,57,false -cdb-aligned-devframe-before,devframe,locator,browser.snapshot,663.480,18,461,false -cdb-aligned-devframe-before,devframe,locator,browser.batch,1251.497,261,610,false -cdb-aligned-devframe-before,devframe,locator,browser.fill,322.947,118,56,false -cdb-aligned-devframe-before,devframe,locator,browser.click,340.396,97,57,false -cdb-aligned-devframe-before,devframe,locator,browser.snapshot,625.137,18,461,false -cdb-aligned-devframe-before,devframe,locator,browser.batch,1168.285,261,610,false -cdb-aligned-devframe-before,devframe,locator,browser.fill,537.405,118,56,false -cdb-aligned-devframe-before,devframe,locator,browser.click,290.459,97,57,false -cdb-aligned-devframe-before,devframe,locator,browser.snapshot,643.662,18,461,false -cdb-aligned-devframe-before,devframe,locator,browser.batch,1122.630,261,610,false -cdb-aligned-devframe-before,devframe,locator,browser.fill,331.875,118,56,false -cdb-aligned-devframe-before,devframe,locator,browser.click,297.636,97,57,false -cdb-aligned-devframe-before,devframe,locator,browser.snapshot,601.933,18,461,false -cdb-aligned-devframe-before,devframe,locator,browser.batch,1289.874,261,610,false -cdb-aligned-devframe-before,devframe,locator,browser.fill,450.520,118,56,false -cdb-aligned-devframe-before,devframe,locator,browser.click,296.028,97,57,false -cdb-aligned-devframe-before,devframe,locator,browser.snapshot,729.033,18,461,false -cdb-aligned-devframe-before,devframe,locator,browser.batch,1079.317,261,610,false -cdb-aligned-devframe-before,devframe,locator,browser.fill,349.810,118,56,false -cdb-aligned-devframe-before,devframe,locator,browser.click,424.241,97,57,false -cdb-aligned-devframe-before,devframe,locator,browser.snapshot,672.811,18,461,false -cdb-aligned-devframe-before,devframe,locator,browser.batch,1380.985,261,610,false -cdb-aligned-devframe-before,devframe,locator,browser.fill,298.762,118,56,false -cdb-aligned-devframe-before,devframe,locator,browser.click,381.518,97,57,false -cdb-aligned-devframe-before,devframe,locator,browser.snapshot,673.477,18,461,false -cdb-aligned-devframe-before,devframe,locator,browser.batch,1276.475,261,610,false -cdb-aligned-devframe-before,devframe,locator,browser.fill,349.092,118,56,false -cdb-aligned-devframe-before,devframe,locator,browser.click,315.707,97,57,false -cdb-aligned-devframe-before,devframe,locator,browser.snapshot,656.487,18,461,false -cdb-aligned-devframe-before,devframe,locator,browser.batch,1136.783,261,610,false -cdb-aligned-devframe-before,devframe,locator,browser.fill,295.324,118,56,false -cdb-aligned-devframe-before,devframe,locator,browser.click,285.077,97,57,false -cdb-aligned-devframe-before,devframe,locator,browser.snapshot,633.594,18,461,false -cdb-aligned-devframe-before,devframe,locator,browser.batch,1292.400,261,610,false -cdb-aligned-devframe-before,devframe,locator,browser.click:blocked,2026.332,97,197,true -cdb-aligned-devframe-before,devframe,locator,browser.click:blocked,2078.657,97,197,true -cdb-aligned-devframe-before,devframe,locator,browser.click:blocked,2166.388,97,197,true -cdb-aligned-devframe-before,devframe,locator,browser.click:blocked,2001.493,97,197,true -cdb-aligned-devframe-before,devframe,locator,browser.click:blocked,2060.925,97,197,true -cdb-aligned-devframe-before,devframe,locator,browser.click:blocked,2051.839,97,197,true -cdb-aligned-devframe-before,devframe,locator,browser.click:blocked,2001.442,97,197,true -cdb-aligned-devframe-before,devframe,locator,browser.click:blocked,2000.921,97,197,true -cdb-aligned-devframe-before,devframe,locator,browser.click:blocked,2000.384,97,197,true -cdb-aligned-devframe-before,devframe,locator,browser.click:blocked,2001.923,97,197,true -cdb-aligned-devframe-before,devframe,locator,browser.click:blocked,2003.992,97,197,true -cdb-aligned-devframe-before,devframe,locator,browser.click:blocked,2002.736,97,197,true -cdb-aligned-devframe-before,devframe,locator,browser.click:blocked,2000.461,97,197,true -cdb-aligned-devframe-before,devframe,locator,browser.click:blocked,2003.326,97,197,true -cdb-aligned-devframe-before,devframe,locator,browser.click:blocked,2003.266,97,197,true -cdb-aligned-devframe-before,devframe,locator,browser.click:blocked,2000.196,97,197,true -cdb-aligned-devframe-before,devframe,locator,browser.click:blocked,2004.421,97,197,true -cdb-aligned-devframe-before,devframe,locator,browser.click:blocked,2002.615,97,197,true -cdb-aligned-devframe-before,devframe,locator,browser.click:blocked,2032.387,97,197,true -cdb-aligned-devframe-before,devframe,locator,browser.click:blocked,2013.513,97,197,true -cdb-aligned-devframe-before,devframe,locator,browser.click:blocked,2005.347,97,197,true -cdb-aligned-devframe-before,devframe,locator,browser.click:blocked,2047.933,97,197,true -cdb-aligned-devframe-before,devframe,locator,browser.click:blocked,2137.193,97,197,true -cdb-aligned-devframe-before,devframe,locator,browser.click:blocked,2003.621,97,197,true -cdb-aligned-devframe-before,devframe,locator,browser.click:blocked,2046.911,97,197,true -cdb-aligned-devframe-before,devframe,locator,browser.click:blocked,2002.456,97,197,true -cdb-aligned-devframe-before,devframe,locator,browser.click:blocked,2003.009,97,197,true -cdb-aligned-devframe-before,devframe,locator,browser.click:blocked,2000.290,97,197,true -cdb-aligned-devframe-before,devframe,locator,browser.click:blocked,2001.732,97,197,true -cdb-aligned-devframe-before,devframe,locator,browser.click:blocked,2004.124,97,197,true -cdb-aligned-websocket-final,websocket,ref,browser.fill,125.710,55,56,false -cdb-aligned-websocket-final,websocket,ref,browser.click,102.587,30,57,false -cdb-aligned-websocket-final,websocket,ref,browser.snapshot,643.090,18,456,false -cdb-aligned-websocket-final,websocket,ref,browser.batch,761.473,131,605,false -cdb-aligned-websocket-final,websocket,ref,browser.fill,101.112,55,56,false -cdb-aligned-websocket-final,websocket,ref,browser.click,96.903,30,57,false -cdb-aligned-websocket-final,websocket,ref,browser.snapshot,636.080,18,456,false -cdb-aligned-websocket-final,websocket,ref,browser.batch,934.368,131,605,false -cdb-aligned-websocket-final,websocket,ref,browser.fill,97.252,55,56,false -cdb-aligned-websocket-final,websocket,ref,browser.click,99.629,30,57,false -cdb-aligned-websocket-final,websocket,ref,browser.snapshot,636.894,18,456,false -cdb-aligned-websocket-final,websocket,ref,browser.batch,782.730,131,605,false -cdb-aligned-websocket-final,websocket,ref,browser.fill,96.522,55,56,false -cdb-aligned-websocket-final,websocket,ref,browser.click,98.351,30,57,false -cdb-aligned-websocket-final,websocket,ref,browser.snapshot,657.567,18,456,false -cdb-aligned-websocket-final,websocket,ref,browser.batch,894.459,131,605,false -cdb-aligned-websocket-final,websocket,ref,browser.fill,103.947,55,56,false -cdb-aligned-websocket-final,websocket,ref,browser.click,121.643,30,57,false -cdb-aligned-websocket-final,websocket,ref,browser.snapshot,636.255,18,457,false -cdb-aligned-websocket-final,websocket,ref,browser.batch,908.680,132,610,false -cdb-aligned-websocket-final,websocket,ref,browser.fill,104.533,56,56,false -cdb-aligned-websocket-final,websocket,ref,browser.click,93.928,31,57,false -cdb-aligned-websocket-final,websocket,ref,browser.snapshot,648.901,18,461,false -cdb-aligned-websocket-final,websocket,ref,browser.batch,789.784,133,610,false -cdb-aligned-websocket-final,websocket,ref,browser.fill,107.163,56,56,false -cdb-aligned-websocket-final,websocket,ref,browser.click,134.566,31,57,false -cdb-aligned-websocket-final,websocket,ref,browser.snapshot,652.533,18,461,false -cdb-aligned-websocket-final,websocket,ref,browser.batch,894.372,133,610,false -cdb-aligned-websocket-final,websocket,ref,browser.fill,110.137,56,56,false -cdb-aligned-websocket-final,websocket,ref,browser.click,93.737,31,57,false -cdb-aligned-websocket-final,websocket,ref,browser.snapshot,610.916,18,461,false -cdb-aligned-websocket-final,websocket,ref,browser.batch,787.507,133,610,false -cdb-aligned-websocket-final,websocket,ref,browser.fill,118.146,56,56,false -cdb-aligned-websocket-final,websocket,ref,browser.click,140.368,31,57,false -cdb-aligned-websocket-final,websocket,ref,browser.snapshot,649.870,18,461,false -cdb-aligned-websocket-final,websocket,ref,browser.batch,851.749,133,610,false -cdb-aligned-websocket-final,websocket,ref,browser.fill,107.676,56,56,false -cdb-aligned-websocket-final,websocket,ref,browser.click,91.018,31,57,false -cdb-aligned-websocket-final,websocket,ref,browser.snapshot,641.999,18,461,false -cdb-aligned-websocket-final,websocket,ref,browser.batch,783.138,133,610,false -cdb-aligned-websocket-final,websocket,ref,browser.fill,98.303,56,56,false -cdb-aligned-websocket-final,websocket,ref,browser.click,90.478,31,57,false -cdb-aligned-websocket-final,websocket,ref,browser.snapshot,605.817,18,461,false -cdb-aligned-websocket-final,websocket,ref,browser.batch,790.715,133,610,false -cdb-aligned-websocket-final,websocket,ref,browser.fill,99.515,56,56,false -cdb-aligned-websocket-final,websocket,ref,browser.click,93.240,31,57,false -cdb-aligned-websocket-final,websocket,ref,browser.snapshot,596.222,18,461,false -cdb-aligned-websocket-final,websocket,ref,browser.batch,795.898,133,610,false -cdb-aligned-websocket-final,websocket,ref,browser.fill,99.308,56,56,false -cdb-aligned-websocket-final,websocket,ref,browser.click,93.232,31,57,false -cdb-aligned-websocket-final,websocket,ref,browser.snapshot,650.366,18,461,false -cdb-aligned-websocket-final,websocket,ref,browser.batch,821.946,133,610,false -cdb-aligned-websocket-final,websocket,ref,browser.fill,101.138,56,56,false -cdb-aligned-websocket-final,websocket,ref,browser.click,91.005,31,57,false -cdb-aligned-websocket-final,websocket,ref,browser.snapshot,665.184,18,461,false -cdb-aligned-websocket-final,websocket,ref,browser.batch,752.382,133,610,false -cdb-aligned-websocket-final,websocket,ref,browser.fill,100.397,56,56,false -cdb-aligned-websocket-final,websocket,ref,browser.click,92.085,31,57,false -cdb-aligned-websocket-final,websocket,ref,browser.snapshot,589.777,18,461,false -cdb-aligned-websocket-final,websocket,ref,browser.batch,815.886,133,610,false -cdb-aligned-websocket-final,websocket,ref,browser.fill,116.241,56,56,false -cdb-aligned-websocket-final,websocket,ref,browser.click,91.539,31,57,false -cdb-aligned-websocket-final,websocket,ref,browser.snapshot,614.700,18,461,false -cdb-aligned-websocket-final,websocket,ref,browser.batch,847.624,133,610,false -cdb-aligned-websocket-final,websocket,ref,browser.fill,97.635,56,56,false -cdb-aligned-websocket-final,websocket,ref,browser.click,134.918,31,57,false -cdb-aligned-websocket-final,websocket,ref,browser.snapshot,691.625,18,461,false -cdb-aligned-websocket-final,websocket,ref,browser.batch,899.642,133,610,false -cdb-aligned-websocket-final,websocket,ref,browser.fill,108.843,56,56,false -cdb-aligned-websocket-final,websocket,ref,browser.click,132.810,31,57,false -cdb-aligned-websocket-final,websocket,ref,browser.snapshot,653.788,18,461,false -cdb-aligned-websocket-final,websocket,ref,browser.batch,863.976,133,610,false -cdb-aligned-websocket-final,websocket,ref,browser.fill,108.961,56,56,false -cdb-aligned-websocket-final,websocket,ref,browser.click,95.425,31,57,false -cdb-aligned-websocket-final,websocket,ref,browser.snapshot,711.863,18,461,false -cdb-aligned-websocket-final,websocket,ref,browser.batch,950.744,133,610,false -cdb-aligned-websocket-final,websocket,ref,browser.fill,97.588,56,56,false -cdb-aligned-websocket-final,websocket,ref,browser.click,93.612,31,57,false -cdb-aligned-websocket-final,websocket,ref,browser.snapshot,625.285,18,461,false -cdb-aligned-websocket-final,websocket,ref,browser.batch,770.658,133,610,false -cdb-aligned-websocket-final,websocket,ref,browser.fill,96.505,56,56,false -cdb-aligned-websocket-final,websocket,ref,browser.click,136.740,31,57,false -cdb-aligned-websocket-final,websocket,ref,browser.snapshot,650.293,18,461,false -cdb-aligned-websocket-final,websocket,ref,browser.batch,781.756,133,610,false -cdb-aligned-websocket-final,websocket,ref,browser.fill,132.347,56,56,false -cdb-aligned-websocket-final,websocket,ref,browser.click,93.288,31,57,false -cdb-aligned-websocket-final,websocket,ref,browser.snapshot,634.770,18,461,false -cdb-aligned-websocket-final,websocket,ref,browser.batch,931.025,133,610,false -cdb-aligned-websocket-final,websocket,ref,browser.fill,104.678,56,56,false -cdb-aligned-websocket-final,websocket,ref,browser.click,90.536,31,57,false -cdb-aligned-websocket-final,websocket,ref,browser.snapshot,618.921,18,461,false -cdb-aligned-websocket-final,websocket,ref,browser.batch,796.601,133,610,false -cdb-aligned-websocket-final,websocket,ref,browser.fill,99.963,56,56,false -cdb-aligned-websocket-final,websocket,ref,browser.click,89.815,31,57,false -cdb-aligned-websocket-final,websocket,ref,browser.snapshot,620.308,18,461,false -cdb-aligned-websocket-final,websocket,ref,browser.batch,791.375,133,610,false -cdb-aligned-websocket-final,websocket,ref,browser.fill,112.617,56,56,false -cdb-aligned-websocket-final,websocket,ref,browser.click,93.466,31,57,false -cdb-aligned-websocket-final,websocket,ref,browser.snapshot,658.628,18,461,false -cdb-aligned-websocket-final,websocket,ref,browser.batch,1116.960,133,610,false -cdb-aligned-websocket-final,websocket,ref,browser.fill,103.187,56,56,false -cdb-aligned-websocket-final,websocket,ref,browser.click,96.616,31,57,false -cdb-aligned-websocket-final,websocket,ref,browser.snapshot,648.176,18,461,false -cdb-aligned-websocket-final,websocket,ref,browser.batch,822.425,133,610,false -cdb-aligned-websocket-final,websocket,ref,browser.fill,99.033,56,56,false -cdb-aligned-websocket-final,websocket,ref,browser.click,89.423,31,57,false -cdb-aligned-websocket-final,websocket,ref,browser.snapshot,619.810,18,461,false -cdb-aligned-websocket-final,websocket,ref,browser.batch,804.675,133,610,false -cdb-aligned-websocket-final,websocket,ref,browser.fill,113.941,56,56,false -cdb-aligned-websocket-final,websocket,ref,browser.click,97.044,31,57,false -cdb-aligned-websocket-final,websocket,ref,browser.snapshot,679.276,18,461,false -cdb-aligned-websocket-final,websocket,ref,browser.batch,874.531,133,610,false -cdb-aligned-websocket-final,websocket,ref,browser.fill,150.116,56,56,false -cdb-aligned-websocket-final,websocket,ref,browser.click,96.505,31,57,false -cdb-aligned-websocket-final,websocket,ref,browser.snapshot,651.503,18,461,false -cdb-aligned-websocket-final,websocket,ref,browser.batch,812.727,133,610,false -cdb-aligned-websocket-final,websocket,ref,browser.fill,101.557,56,56,false -cdb-aligned-websocket-final,websocket,ref,browser.click,94.317,31,57,false -cdb-aligned-websocket-final,websocket,ref,browser.snapshot,623.638,18,461,false -cdb-aligned-websocket-final,websocket,ref,browser.batch,813.837,133,610,false -cdb-aligned-websocket-final,websocket,ref,browser.click:blocked,2000.811,31,172,true -cdb-aligned-websocket-final,websocket,ref,browser.click:blocked,2000.571,31,172,true -cdb-aligned-websocket-final,websocket,ref,browser.click:blocked,2001.031,31,172,true -cdb-aligned-websocket-final,websocket,ref,browser.click:blocked,2001.712,31,172,true -cdb-aligned-websocket-final,websocket,ref,browser.click:blocked,2001.002,31,172,true -cdb-aligned-websocket-final,websocket,ref,browser.click:blocked,2001.847,31,172,true -cdb-aligned-websocket-final,websocket,ref,browser.click:blocked,2001.669,31,172,true -cdb-aligned-websocket-final,websocket,ref,browser.click:blocked,2001.996,31,172,true -cdb-aligned-websocket-final,websocket,ref,browser.click:blocked,2001.776,31,172,true -cdb-aligned-websocket-final,websocket,ref,browser.click:blocked,2002.933,31,172,true -cdb-aligned-websocket-final,websocket,ref,browser.click:blocked,2002.023,31,172,true -cdb-aligned-websocket-final,websocket,ref,browser.click:blocked,2001.039,31,172,true -cdb-aligned-websocket-final,websocket,ref,browser.click:blocked,2000.864,31,172,true -cdb-aligned-websocket-final,websocket,ref,browser.click:blocked,2001.650,31,172,true -cdb-aligned-websocket-final,websocket,ref,browser.click:blocked,2000.767,31,172,true -cdb-aligned-websocket-final,websocket,ref,browser.click:blocked,2002.014,31,172,true -cdb-aligned-websocket-final,websocket,ref,browser.click:blocked,2001.880,31,172,true -cdb-aligned-websocket-final,websocket,ref,browser.click:blocked,2000.563,31,172,true -cdb-aligned-websocket-final,websocket,ref,browser.click:blocked,2000.807,31,172,true -cdb-aligned-websocket-final,websocket,ref,browser.click:blocked,2000.914,31,172,true -cdb-aligned-websocket-final,websocket,ref,browser.click:blocked,2001.849,31,172,true -cdb-aligned-websocket-final,websocket,ref,browser.click:blocked,2001.820,31,172,true -cdb-aligned-websocket-final,websocket,ref,browser.click:blocked,2001.684,31,172,true -cdb-aligned-websocket-final,websocket,ref,browser.click:blocked,2002.417,31,172,true -cdb-aligned-websocket-final,websocket,ref,browser.click:blocked,2001.985,31,172,true -cdb-aligned-websocket-final,websocket,ref,browser.click:blocked,2001.420,31,172,true -cdb-aligned-websocket-final,websocket,ref,browser.click:blocked,2002.013,31,172,true -cdb-aligned-websocket-final,websocket,ref,browser.click:blocked,2001.867,31,172,true -cdb-aligned-websocket-final,websocket,ref,browser.click:blocked,2001.530,31,172,true -cdb-aligned-websocket-final,websocket,ref,browser.click:blocked,2001.457,31,172,true -cdb-aligned-websocket-final,websocket,locator,browser.fill,256.794,118,56,false -cdb-aligned-websocket-final,websocket,locator,browser.click,602.280,97,57,false -cdb-aligned-websocket-final,websocket,locator,browser.snapshot,717.292,18,461,false -cdb-aligned-websocket-final,websocket,locator,browser.batch,1074.523,261,610,false -cdb-aligned-websocket-final,websocket,locator,browser.fill,203.307,118,56,false -cdb-aligned-websocket-final,websocket,locator,browser.click,201.543,97,57,false -cdb-aligned-websocket-final,websocket,locator,browser.snapshot,634.541,18,461,false -cdb-aligned-websocket-final,websocket,locator,browser.batch,893.878,261,610,false -cdb-aligned-websocket-final,websocket,locator,browser.fill,200.952,118,56,false -cdb-aligned-websocket-final,websocket,locator,browser.click,182.223,97,57,false -cdb-aligned-websocket-final,websocket,locator,browser.snapshot,575.174,18,461,false -cdb-aligned-websocket-final,websocket,locator,browser.batch,847.651,261,610,false -cdb-aligned-websocket-final,websocket,locator,browser.fill,188.445,118,56,false -cdb-aligned-websocket-final,websocket,locator,browser.click,170.307,97,57,false -cdb-aligned-websocket-final,websocket,locator,browser.snapshot,591.031,18,461,false -cdb-aligned-websocket-final,websocket,locator,browser.batch,966.351,261,610,false -cdb-aligned-websocket-final,websocket,locator,browser.fill,199.649,118,56,false -cdb-aligned-websocket-final,websocket,locator,browser.click,185.688,97,57,false -cdb-aligned-websocket-final,websocket,locator,browser.snapshot,620.812,18,461,false -cdb-aligned-websocket-final,websocket,locator,browser.batch,921.538,261,610,false -cdb-aligned-websocket-final,websocket,locator,browser.fill,187.456,118,56,false -cdb-aligned-websocket-final,websocket,locator,browser.click,172.897,97,57,false -cdb-aligned-websocket-final,websocket,locator,browser.snapshot,781.027,18,461,false -cdb-aligned-websocket-final,websocket,locator,browser.batch,1270.952,261,610,false -cdb-aligned-websocket-final,websocket,locator,browser.fill,210.310,118,56,false -cdb-aligned-websocket-final,websocket,locator,browser.click,211.076,97,57,false -cdb-aligned-websocket-final,websocket,locator,browser.snapshot,618.221,18,461,false -cdb-aligned-websocket-final,websocket,locator,browser.batch,899.880,261,610,false -cdb-aligned-websocket-final,websocket,locator,browser.fill,192.650,118,56,false -cdb-aligned-websocket-final,websocket,locator,browser.click,175.391,97,57,false -cdb-aligned-websocket-final,websocket,locator,browser.snapshot,595.797,18,461,false -cdb-aligned-websocket-final,websocket,locator,browser.batch,950.270,261,610,false -cdb-aligned-websocket-final,websocket,locator,browser.fill,284.535,118,56,false -cdb-aligned-websocket-final,websocket,locator,browser.click,222.219,97,57,false -cdb-aligned-websocket-final,websocket,locator,browser.snapshot,615.605,18,461,false -cdb-aligned-websocket-final,websocket,locator,browser.batch,934.534,261,610,false -cdb-aligned-websocket-final,websocket,locator,browser.fill,211.998,118,56,false -cdb-aligned-websocket-final,websocket,locator,browser.click,214.718,97,57,false -cdb-aligned-websocket-final,websocket,locator,browser.snapshot,666.219,18,461,false -cdb-aligned-websocket-final,websocket,locator,browser.batch,979.126,261,610,false -cdb-aligned-websocket-final,websocket,locator,browser.fill,184.274,118,56,false -cdb-aligned-websocket-final,websocket,locator,browser.click,189.235,97,57,false -cdb-aligned-websocket-final,websocket,locator,browser.snapshot,709.514,18,461,false -cdb-aligned-websocket-final,websocket,locator,browser.batch,895.532,261,610,false -cdb-aligned-websocket-final,websocket,locator,browser.fill,209.854,118,56,false -cdb-aligned-websocket-final,websocket,locator,browser.click,190.554,97,57,false -cdb-aligned-websocket-final,websocket,locator,browser.snapshot,612.038,18,461,false -cdb-aligned-websocket-final,websocket,locator,browser.batch,885.612,261,610,false -cdb-aligned-websocket-final,websocket,locator,browser.fill,193.010,118,56,false -cdb-aligned-websocket-final,websocket,locator,browser.click,179.371,97,57,false -cdb-aligned-websocket-final,websocket,locator,browser.snapshot,614.251,18,461,false -cdb-aligned-websocket-final,websocket,locator,browser.batch,885.574,261,610,false -cdb-aligned-websocket-final,websocket,locator,browser.fill,207.411,118,56,false -cdb-aligned-websocket-final,websocket,locator,browser.click,240.675,97,57,false -cdb-aligned-websocket-final,websocket,locator,browser.snapshot,649.766,18,461,false -cdb-aligned-websocket-final,websocket,locator,browser.batch,908.012,261,610,false -cdb-aligned-websocket-final,websocket,locator,browser.fill,195.954,118,56,false -cdb-aligned-websocket-final,websocket,locator,browser.click,204.538,97,57,false -cdb-aligned-websocket-final,websocket,locator,browser.snapshot,613.003,18,461,false -cdb-aligned-websocket-final,websocket,locator,browser.batch,878.021,261,610,false -cdb-aligned-websocket-final,websocket,locator,browser.fill,227.191,118,56,false -cdb-aligned-websocket-final,websocket,locator,browser.click,185.537,97,57,false -cdb-aligned-websocket-final,websocket,locator,browser.snapshot,633.970,18,461,false -cdb-aligned-websocket-final,websocket,locator,browser.batch,907.635,261,610,false -cdb-aligned-websocket-final,websocket,locator,browser.fill,205.674,118,56,false -cdb-aligned-websocket-final,websocket,locator,browser.click,194.097,97,57,false -cdb-aligned-websocket-final,websocket,locator,browser.snapshot,592.348,18,461,false -cdb-aligned-websocket-final,websocket,locator,browser.batch,867.610,261,610,false -cdb-aligned-websocket-final,websocket,locator,browser.fill,218.754,118,56,false -cdb-aligned-websocket-final,websocket,locator,browser.click,178.115,97,57,false -cdb-aligned-websocket-final,websocket,locator,browser.snapshot,626.267,18,461,false -cdb-aligned-websocket-final,websocket,locator,browser.batch,868.579,261,610,false -cdb-aligned-websocket-final,websocket,locator,browser.fill,189.357,118,56,false -cdb-aligned-websocket-final,websocket,locator,browser.click,182.839,97,57,false -cdb-aligned-websocket-final,websocket,locator,browser.snapshot,638.431,18,461,false -cdb-aligned-websocket-final,websocket,locator,browser.batch,923.786,261,610,false -cdb-aligned-websocket-final,websocket,locator,browser.fill,199.916,118,56,false -cdb-aligned-websocket-final,websocket,locator,browser.click,181.638,97,57,false -cdb-aligned-websocket-final,websocket,locator,browser.snapshot,614.112,18,461,false -cdb-aligned-websocket-final,websocket,locator,browser.batch,1025.136,261,610,false -cdb-aligned-websocket-final,websocket,locator,browser.fill,197.851,118,56,false -cdb-aligned-websocket-final,websocket,locator,browser.click,180.848,97,57,false -cdb-aligned-websocket-final,websocket,locator,browser.snapshot,623.590,18,461,false -cdb-aligned-websocket-final,websocket,locator,browser.batch,898.218,261,610,false -cdb-aligned-websocket-final,websocket,locator,browser.fill,189.515,118,56,false -cdb-aligned-websocket-final,websocket,locator,browser.click,193.180,97,57,false -cdb-aligned-websocket-final,websocket,locator,browser.snapshot,670.752,18,461,false -cdb-aligned-websocket-final,websocket,locator,browser.batch,1087.348,261,610,false -cdb-aligned-websocket-final,websocket,locator,browser.fill,192.539,118,56,false -cdb-aligned-websocket-final,websocket,locator,browser.click,179.636,97,57,false -cdb-aligned-websocket-final,websocket,locator,browser.snapshot,583.847,18,461,false -cdb-aligned-websocket-final,websocket,locator,browser.batch,893.445,261,610,false -cdb-aligned-websocket-final,websocket,locator,browser.fill,197.718,118,56,false -cdb-aligned-websocket-final,websocket,locator,browser.click,178.522,97,57,false -cdb-aligned-websocket-final,websocket,locator,browser.snapshot,602.867,18,461,false -cdb-aligned-websocket-final,websocket,locator,browser.batch,990.211,261,610,false -cdb-aligned-websocket-final,websocket,locator,browser.fill,201.075,118,56,false -cdb-aligned-websocket-final,websocket,locator,browser.click,178.606,97,57,false -cdb-aligned-websocket-final,websocket,locator,browser.snapshot,612.621,18,461,false -cdb-aligned-websocket-final,websocket,locator,browser.batch,919.138,261,610,false -cdb-aligned-websocket-final,websocket,locator,browser.fill,199.865,118,56,false -cdb-aligned-websocket-final,websocket,locator,browser.click,182.938,97,57,false -cdb-aligned-websocket-final,websocket,locator,browser.snapshot,632.192,18,461,false -cdb-aligned-websocket-final,websocket,locator,browser.batch,922.215,261,610,false -cdb-aligned-websocket-final,websocket,locator,browser.fill,196.468,118,56,false -cdb-aligned-websocket-final,websocket,locator,browser.click,186.570,97,57,false -cdb-aligned-websocket-final,websocket,locator,browser.snapshot,577.686,18,461,false -cdb-aligned-websocket-final,websocket,locator,browser.batch,873.323,261,610,false -cdb-aligned-websocket-final,websocket,locator,browser.fill,190.303,118,56,false -cdb-aligned-websocket-final,websocket,locator,browser.click,176.376,97,57,false -cdb-aligned-websocket-final,websocket,locator,browser.snapshot,594.695,18,461,false -cdb-aligned-websocket-final,websocket,locator,browser.batch,917.169,261,610,false -cdb-aligned-websocket-final,websocket,locator,browser.fill,195.796,118,56,false -cdb-aligned-websocket-final,websocket,locator,browser.click,171.049,97,57,false -cdb-aligned-websocket-final,websocket,locator,browser.snapshot,624.176,18,461,false -cdb-aligned-websocket-final,websocket,locator,browser.batch,928.746,261,610,false -cdb-aligned-websocket-final,websocket,locator,browser.fill,186.579,118,56,false -cdb-aligned-websocket-final,websocket,locator,browser.click,179.168,97,57,false -cdb-aligned-websocket-final,websocket,locator,browser.snapshot,594.746,18,461,false -cdb-aligned-websocket-final,websocket,locator,browser.batch,920.616,261,610,false -cdb-aligned-websocket-final,websocket,locator,browser.click:blocked,2001.022,97,172,true -cdb-aligned-websocket-final,websocket,locator,browser.click:blocked,2010.702,97,172,true -cdb-aligned-websocket-final,websocket,locator,browser.click:blocked,2015.405,97,172,true -cdb-aligned-websocket-final,websocket,locator,browser.click:blocked,2034.893,97,172,true -cdb-aligned-websocket-final,websocket,locator,browser.click:blocked,2002.248,97,172,true -cdb-aligned-websocket-final,websocket,locator,browser.click:blocked,2008.607,97,172,true -cdb-aligned-websocket-final,websocket,locator,browser.click:blocked,2001.469,97,172,true -cdb-aligned-websocket-final,websocket,locator,browser.click:blocked,2000.179,97,172,true -cdb-aligned-websocket-final,websocket,locator,browser.click:blocked,2001.383,97,172,true -cdb-aligned-websocket-final,websocket,locator,browser.click:blocked,2001.007,97,172,true -cdb-aligned-websocket-final,websocket,locator,browser.click:blocked,2000.695,97,172,true -cdb-aligned-websocket-final,websocket,locator,browser.click:blocked,2001.795,97,172,true -cdb-aligned-websocket-final,websocket,locator,browser.click:blocked,2000.216,97,172,true -cdb-aligned-websocket-final,websocket,locator,browser.click:blocked,2001.291,97,172,true -cdb-aligned-websocket-final,websocket,locator,browser.click:blocked,2000.902,97,172,true -cdb-aligned-websocket-final,websocket,locator,browser.click:blocked,2001.941,97,172,true -cdb-aligned-websocket-final,websocket,locator,browser.click:blocked,2001.171,97,172,true -cdb-aligned-websocket-final,websocket,locator,browser.click:blocked,2000.859,97,172,true -cdb-aligned-websocket-final,websocket,locator,browser.click:blocked,2000.161,97,172,true -cdb-aligned-websocket-final,websocket,locator,browser.click:blocked,2002.201,97,172,true -cdb-aligned-websocket-final,websocket,locator,browser.click:blocked,2001.783,97,172,true -cdb-aligned-websocket-final,websocket,locator,browser.click:blocked,2019.602,97,172,true -cdb-aligned-websocket-final,websocket,locator,browser.click:blocked,2007.508,97,172,true -cdb-aligned-websocket-final,websocket,locator,browser.click:blocked,2000.628,97,172,true -cdb-aligned-websocket-final,websocket,locator,browser.click:blocked,2001.322,97,172,true -cdb-aligned-websocket-final,websocket,locator,browser.click:blocked,2002.131,97,172,true -cdb-aligned-websocket-final,websocket,locator,browser.click:blocked,2001.335,97,172,true -cdb-aligned-websocket-final,websocket,locator,browser.click:blocked,2000.018,97,172,true -cdb-aligned-websocket-final,websocket,locator,browser.click:blocked,2000.776,97,172,true -cdb-aligned-websocket-final,websocket,locator,browser.click:blocked,2001.525,97,172,true -cdb-aligned-devframe-final,devframe,ref,browser.fill,88.018,55,56,false -cdb-aligned-devframe-final,devframe,ref,browser.click,82.298,30,57,false -cdb-aligned-devframe-final,devframe,ref,browser.snapshot,603.242,18,456,false -cdb-aligned-devframe-final,devframe,ref,browser.batch,731.865,131,605,false -cdb-aligned-devframe-final,devframe,ref,browser.fill,96.004,55,56,false -cdb-aligned-devframe-final,devframe,ref,browser.click,83.228,30,57,false -cdb-aligned-devframe-final,devframe,ref,browser.snapshot,572.510,18,456,false -cdb-aligned-devframe-final,devframe,ref,browser.batch,745.158,131,605,false -cdb-aligned-devframe-final,devframe,ref,browser.fill,96.372,55,56,false -cdb-aligned-devframe-final,devframe,ref,browser.click,85.345,30,57,false -cdb-aligned-devframe-final,devframe,ref,browser.snapshot,601.268,18,456,false -cdb-aligned-devframe-final,devframe,ref,browser.batch,718.126,131,605,false -cdb-aligned-devframe-final,devframe,ref,browser.fill,86.163,55,56,false -cdb-aligned-devframe-final,devframe,ref,browser.click,84.152,30,57,false -cdb-aligned-devframe-final,devframe,ref,browser.snapshot,590.813,18,456,false -cdb-aligned-devframe-final,devframe,ref,browser.batch,717.286,131,605,false -cdb-aligned-devframe-final,devframe,ref,browser.fill,89.482,55,56,false -cdb-aligned-devframe-final,devframe,ref,browser.click,84.672,30,57,false -cdb-aligned-devframe-final,devframe,ref,browser.snapshot,605.309,18,457,false -cdb-aligned-devframe-final,devframe,ref,browser.batch,758.002,132,610,false -cdb-aligned-devframe-final,devframe,ref,browser.fill,94.520,56,56,false -cdb-aligned-devframe-final,devframe,ref,browser.click,89.802,31,57,false -cdb-aligned-devframe-final,devframe,ref,browser.snapshot,653.743,18,461,false -cdb-aligned-devframe-final,devframe,ref,browser.batch,712.995,133,610,false -cdb-aligned-devframe-final,devframe,ref,browser.fill,94.071,56,56,false -cdb-aligned-devframe-final,devframe,ref,browser.click,87.562,31,57,false -cdb-aligned-devframe-final,devframe,ref,browser.snapshot,722.050,18,461,false -cdb-aligned-devframe-final,devframe,ref,browser.batch,744.758,133,610,false -cdb-aligned-devframe-final,devframe,ref,browser.fill,94.699,56,56,false -cdb-aligned-devframe-final,devframe,ref,browser.click,82.504,31,57,false -cdb-aligned-devframe-final,devframe,ref,browser.snapshot,634.316,18,461,false -cdb-aligned-devframe-final,devframe,ref,browser.batch,724.557,133,610,false -cdb-aligned-devframe-final,devframe,ref,browser.fill,135.901,56,56,false -cdb-aligned-devframe-final,devframe,ref,browser.click,104.579,31,57,false -cdb-aligned-devframe-final,devframe,ref,browser.snapshot,648.629,18,461,false -cdb-aligned-devframe-final,devframe,ref,browser.batch,842.125,133,610,false -cdb-aligned-devframe-final,devframe,ref,browser.fill,94.424,56,56,false -cdb-aligned-devframe-final,devframe,ref,browser.click,86.297,31,57,false -cdb-aligned-devframe-final,devframe,ref,browser.snapshot,696.299,18,461,false -cdb-aligned-devframe-final,devframe,ref,browser.batch,742.042,133,610,false -cdb-aligned-devframe-final,devframe,ref,browser.fill,95.025,56,56,false -cdb-aligned-devframe-final,devframe,ref,browser.click,84.823,31,57,false -cdb-aligned-devframe-final,devframe,ref,browser.snapshot,605.350,18,461,false -cdb-aligned-devframe-final,devframe,ref,browser.batch,726.580,133,610,false -cdb-aligned-devframe-final,devframe,ref,browser.fill,86.780,56,56,false -cdb-aligned-devframe-final,devframe,ref,browser.click,83.910,31,57,false -cdb-aligned-devframe-final,devframe,ref,browser.snapshot,609.939,18,461,false -cdb-aligned-devframe-final,devframe,ref,browser.batch,840.831,133,610,false -cdb-aligned-devframe-final,devframe,ref,browser.fill,90.771,56,56,false -cdb-aligned-devframe-final,devframe,ref,browser.click,100.282,31,57,false -cdb-aligned-devframe-final,devframe,ref,browser.snapshot,614.312,18,461,false -cdb-aligned-devframe-final,devframe,ref,browser.batch,727.900,133,610,false -cdb-aligned-devframe-final,devframe,ref,browser.fill,96.800,56,56,false -cdb-aligned-devframe-final,devframe,ref,browser.click,83.378,31,57,false -cdb-aligned-devframe-final,devframe,ref,browser.snapshot,614.054,18,461,false -cdb-aligned-devframe-final,devframe,ref,browser.batch,750.327,133,610,false -cdb-aligned-devframe-final,devframe,ref,browser.fill,95.509,56,56,false -cdb-aligned-devframe-final,devframe,ref,browser.click,83.715,31,57,false -cdb-aligned-devframe-final,devframe,ref,browser.snapshot,645.982,18,461,false -cdb-aligned-devframe-final,devframe,ref,browser.batch,805.162,133,610,false -cdb-aligned-devframe-final,devframe,ref,browser.fill,100.283,56,56,false -cdb-aligned-devframe-final,devframe,ref,browser.click,93.447,31,57,false -cdb-aligned-devframe-final,devframe,ref,browser.snapshot,637.871,18,461,false -cdb-aligned-devframe-final,devframe,ref,browser.batch,766.388,133,610,false -cdb-aligned-devframe-final,devframe,ref,browser.fill,94.958,56,56,false -cdb-aligned-devframe-final,devframe,ref,browser.click,99.695,31,57,false -cdb-aligned-devframe-final,devframe,ref,browser.snapshot,669.262,18,461,false -cdb-aligned-devframe-final,devframe,ref,browser.batch,761.252,133,610,false -cdb-aligned-devframe-final,devframe,ref,browser.fill,94.869,56,56,false -cdb-aligned-devframe-final,devframe,ref,browser.click,83.617,31,57,false -cdb-aligned-devframe-final,devframe,ref,browser.snapshot,583.434,18,461,false -cdb-aligned-devframe-final,devframe,ref,browser.batch,792.972,133,610,false -cdb-aligned-devframe-final,devframe,ref,browser.fill,92.929,56,56,false -cdb-aligned-devframe-final,devframe,ref,browser.click,82.606,31,57,false -cdb-aligned-devframe-final,devframe,ref,browser.snapshot,595.436,18,461,false -cdb-aligned-devframe-final,devframe,ref,browser.batch,716.975,133,610,false -cdb-aligned-devframe-final,devframe,ref,browser.fill,87.617,56,56,false -cdb-aligned-devframe-final,devframe,ref,browser.click,79.033,31,57,false -cdb-aligned-devframe-final,devframe,ref,browser.snapshot,599.608,18,461,false -cdb-aligned-devframe-final,devframe,ref,browser.batch,719.685,133,610,false -cdb-aligned-devframe-final,devframe,ref,browser.fill,94.224,56,56,false -cdb-aligned-devframe-final,devframe,ref,browser.click,87.863,31,57,false -cdb-aligned-devframe-final,devframe,ref,browser.snapshot,585.709,18,461,false -cdb-aligned-devframe-final,devframe,ref,browser.batch,778.706,133,610,false -cdb-aligned-devframe-final,devframe,ref,browser.fill,100.268,56,56,false -cdb-aligned-devframe-final,devframe,ref,browser.click,99.297,31,57,false -cdb-aligned-devframe-final,devframe,ref,browser.snapshot,584.344,18,461,false -cdb-aligned-devframe-final,devframe,ref,browser.batch,786.607,133,610,false -cdb-aligned-devframe-final,devframe,ref,browser.fill,91.816,56,56,false -cdb-aligned-devframe-final,devframe,ref,browser.click,123.683,31,57,false -cdb-aligned-devframe-final,devframe,ref,browser.snapshot,721.600,18,461,false -cdb-aligned-devframe-final,devframe,ref,browser.batch,753.618,133,610,false -cdb-aligned-devframe-final,devframe,ref,browser.fill,95.610,56,56,false -cdb-aligned-devframe-final,devframe,ref,browser.click,86.497,31,57,false -cdb-aligned-devframe-final,devframe,ref,browser.snapshot,600.570,18,461,false -cdb-aligned-devframe-final,devframe,ref,browser.batch,729.854,133,610,false -cdb-aligned-devframe-final,devframe,ref,browser.fill,91.808,56,56,false -cdb-aligned-devframe-final,devframe,ref,browser.click,84.225,31,57,false -cdb-aligned-devframe-final,devframe,ref,browser.snapshot,637.709,18,461,false -cdb-aligned-devframe-final,devframe,ref,browser.batch,809.601,133,610,false -cdb-aligned-devframe-final,devframe,ref,browser.fill,99.468,56,56,false -cdb-aligned-devframe-final,devframe,ref,browser.click,86.270,31,57,false -cdb-aligned-devframe-final,devframe,ref,browser.snapshot,574.601,18,461,false -cdb-aligned-devframe-final,devframe,ref,browser.batch,788.271,133,610,false -cdb-aligned-devframe-final,devframe,ref,browser.fill,94.330,56,56,false -cdb-aligned-devframe-final,devframe,ref,browser.click,87.336,31,57,false -cdb-aligned-devframe-final,devframe,ref,browser.snapshot,569.574,18,461,false -cdb-aligned-devframe-final,devframe,ref,browser.batch,728.545,133,610,false -cdb-aligned-devframe-final,devframe,ref,browser.fill,91.929,56,56,false -cdb-aligned-devframe-final,devframe,ref,browser.click,83.765,31,57,false -cdb-aligned-devframe-final,devframe,ref,browser.snapshot,580.447,18,461,false -cdb-aligned-devframe-final,devframe,ref,browser.batch,708.669,133,610,false -cdb-aligned-devframe-final,devframe,ref,browser.fill,89.274,56,56,false -cdb-aligned-devframe-final,devframe,ref,browser.click,83.036,31,57,false -cdb-aligned-devframe-final,devframe,ref,browser.snapshot,602.801,18,461,false -cdb-aligned-devframe-final,devframe,ref,browser.batch,738.504,133,610,false -cdb-aligned-devframe-final,devframe,ref,browser.fill,91.906,56,56,false -cdb-aligned-devframe-final,devframe,ref,browser.click,91.025,31,57,false -cdb-aligned-devframe-final,devframe,ref,browser.snapshot,607.479,18,461,false -cdb-aligned-devframe-final,devframe,ref,browser.batch,809.914,133,610,false -cdb-aligned-devframe-final,devframe,ref,browser.click:blocked,2004.977,31,197,true -cdb-aligned-devframe-final,devframe,ref,browser.click:blocked,2003.364,31,197,true -cdb-aligned-devframe-final,devframe,ref,browser.click:blocked,2002.574,31,197,true -cdb-aligned-devframe-final,devframe,ref,browser.click:blocked,2001.980,31,197,true -cdb-aligned-devframe-final,devframe,ref,browser.click:blocked,2001.241,31,197,true -cdb-aligned-devframe-final,devframe,ref,browser.click:blocked,2004.907,31,197,true -cdb-aligned-devframe-final,devframe,ref,browser.click:blocked,2035.897,31,197,true -cdb-aligned-devframe-final,devframe,ref,browser.click:blocked,2003.762,31,197,true -cdb-aligned-devframe-final,devframe,ref,browser.click:blocked,2003.521,31,197,true -cdb-aligned-devframe-final,devframe,ref,browser.click:blocked,2002.160,31,197,true -cdb-aligned-devframe-final,devframe,ref,browser.click:blocked,2004.448,31,197,true -cdb-aligned-devframe-final,devframe,ref,browser.click:blocked,2001.832,31,197,true -cdb-aligned-devframe-final,devframe,ref,browser.click:blocked,2006.704,31,197,true -cdb-aligned-devframe-final,devframe,ref,browser.click:blocked,2005.048,31,197,true -cdb-aligned-devframe-final,devframe,ref,browser.click:blocked,2004.385,31,197,true -cdb-aligned-devframe-final,devframe,ref,browser.click:blocked,2005.645,31,197,true -cdb-aligned-devframe-final,devframe,ref,browser.click:blocked,2003.371,31,197,true -cdb-aligned-devframe-final,devframe,ref,browser.click:blocked,2002.687,31,197,true -cdb-aligned-devframe-final,devframe,ref,browser.click:blocked,2002.376,31,197,true -cdb-aligned-devframe-final,devframe,ref,browser.click:blocked,2002.315,31,197,true -cdb-aligned-devframe-final,devframe,ref,browser.click:blocked,2004.631,31,197,true -cdb-aligned-devframe-final,devframe,ref,browser.click:blocked,2002.205,31,197,true -cdb-aligned-devframe-final,devframe,ref,browser.click:blocked,2007.229,31,197,true -cdb-aligned-devframe-final,devframe,ref,browser.click:blocked,2001.779,31,197,true -cdb-aligned-devframe-final,devframe,ref,browser.click:blocked,2005.731,31,197,true -cdb-aligned-devframe-final,devframe,ref,browser.click:blocked,2001.764,31,197,true -cdb-aligned-devframe-final,devframe,ref,browser.click:blocked,2002.831,31,197,true -cdb-aligned-devframe-final,devframe,ref,browser.click:blocked,2003.137,31,197,true -cdb-aligned-devframe-final,devframe,ref,browser.click:blocked,2006.857,31,197,true -cdb-aligned-devframe-final,devframe,ref,browser.click:blocked,2004.026,31,197,true -cdb-aligned-devframe-final,devframe,locator,browser.fill,326.028,118,56,false -cdb-aligned-devframe-final,devframe,locator,browser.click,269.845,97,57,false -cdb-aligned-devframe-final,devframe,locator,browser.snapshot,737.313,18,461,false -cdb-aligned-devframe-final,devframe,locator,browser.batch,1134.772,261,610,false -cdb-aligned-devframe-final,devframe,locator,browser.fill,459.162,118,56,false -cdb-aligned-devframe-final,devframe,locator,browser.click,522.173,97,57,false -cdb-aligned-devframe-final,devframe,locator,browser.snapshot,704.864,18,461,false -cdb-aligned-devframe-final,devframe,locator,browser.batch,1270.937,261,610,false -cdb-aligned-devframe-final,devframe,locator,browser.fill,304.845,118,56,false -cdb-aligned-devframe-final,devframe,locator,browser.click,839.432,97,57,false -cdb-aligned-devframe-final,devframe,locator,browser.snapshot,663.875,18,461,false -cdb-aligned-devframe-final,devframe,locator,browser.batch,1075.940,261,610,false -cdb-aligned-devframe-final,devframe,locator,browser.fill,453.844,118,56,false -cdb-aligned-devframe-final,devframe,locator,browser.click,357.202,97,57,false -cdb-aligned-devframe-final,devframe,locator,browser.snapshot,596.851,18,461,false -cdb-aligned-devframe-final,devframe,locator,browser.batch,1362.910,261,610,false -cdb-aligned-devframe-final,devframe,locator,browser.fill,321.378,118,56,false -cdb-aligned-devframe-final,devframe,locator,browser.click,315.966,97,57,false -cdb-aligned-devframe-final,devframe,locator,browser.snapshot,598.542,18,461,false -cdb-aligned-devframe-final,devframe,locator,browser.batch,1246.113,261,610,false -cdb-aligned-devframe-final,devframe,locator,browser.fill,286.183,118,56,false -cdb-aligned-devframe-final,devframe,locator,browser.click,318.662,97,57,false -cdb-aligned-devframe-final,devframe,locator,browser.snapshot,647.900,18,461,false -cdb-aligned-devframe-final,devframe,locator,browser.batch,1334.540,261,610,false -cdb-aligned-devframe-final,devframe,locator,browser.fill,420.563,118,56,false -cdb-aligned-devframe-final,devframe,locator,browser.click,367.449,97,57,false -cdb-aligned-devframe-final,devframe,locator,browser.snapshot,848.921,18,461,false -cdb-aligned-devframe-final,devframe,locator,browser.batch,1292.561,261,610,false -cdb-aligned-devframe-final,devframe,locator,browser.fill,338.127,118,56,false -cdb-aligned-devframe-final,devframe,locator,browser.click,320.017,97,57,false -cdb-aligned-devframe-final,devframe,locator,browser.snapshot,621.152,18,461,false -cdb-aligned-devframe-final,devframe,locator,browser.batch,1187.023,261,610,false -cdb-aligned-devframe-final,devframe,locator,browser.fill,310.342,118,56,false -cdb-aligned-devframe-final,devframe,locator,browser.click,248.405,97,57,false -cdb-aligned-devframe-final,devframe,locator,browser.snapshot,777.826,18,461,false -cdb-aligned-devframe-final,devframe,locator,browser.batch,1113.329,261,610,false -cdb-aligned-devframe-final,devframe,locator,browser.fill,286.460,118,56,false -cdb-aligned-devframe-final,devframe,locator,browser.click,391.620,97,57,false -cdb-aligned-devframe-final,devframe,locator,browser.snapshot,916.579,18,461,false -cdb-aligned-devframe-final,devframe,locator,browser.batch,1033.854,261,610,false -cdb-aligned-devframe-final,devframe,locator,browser.fill,325.078,118,56,false -cdb-aligned-devframe-final,devframe,locator,browser.click,325.236,97,57,false -cdb-aligned-devframe-final,devframe,locator,browser.snapshot,746.520,18,461,false -cdb-aligned-devframe-final,devframe,locator,browser.batch,1509.607,261,610,false -cdb-aligned-devframe-final,devframe,locator,browser.fill,320.006,118,56,false -cdb-aligned-devframe-final,devframe,locator,browser.click,266.932,97,57,false -cdb-aligned-devframe-final,devframe,locator,browser.snapshot,633.822,18,461,false -cdb-aligned-devframe-final,devframe,locator,browser.batch,1121.278,261,610,false -cdb-aligned-devframe-final,devframe,locator,browser.fill,301.773,118,56,false -cdb-aligned-devframe-final,devframe,locator,browser.click,287.714,97,57,false -cdb-aligned-devframe-final,devframe,locator,browser.snapshot,663.195,18,461,false -cdb-aligned-devframe-final,devframe,locator,browser.batch,1008.390,261,610,false -cdb-aligned-devframe-final,devframe,locator,browser.fill,266.984,118,56,false -cdb-aligned-devframe-final,devframe,locator,browser.click,260.587,97,57,false -cdb-aligned-devframe-final,devframe,locator,browser.snapshot,717.765,18,461,false -cdb-aligned-devframe-final,devframe,locator,browser.batch,983.019,261,610,false -cdb-aligned-devframe-final,devframe,locator,browser.fill,247.999,118,56,false -cdb-aligned-devframe-final,devframe,locator,browser.click,310.843,97,57,false -cdb-aligned-devframe-final,devframe,locator,browser.snapshot,603.090,18,461,false -cdb-aligned-devframe-final,devframe,locator,browser.batch,1096.830,261,610,false -cdb-aligned-devframe-final,devframe,locator,browser.fill,255.051,118,56,false -cdb-aligned-devframe-final,devframe,locator,browser.click,274.290,97,57,false -cdb-aligned-devframe-final,devframe,locator,browser.snapshot,628.900,18,461,false -cdb-aligned-devframe-final,devframe,locator,browser.batch,1045.957,261,610,false -cdb-aligned-devframe-final,devframe,locator,browser.fill,258.932,118,56,false -cdb-aligned-devframe-final,devframe,locator,browser.click,247.100,97,57,false -cdb-aligned-devframe-final,devframe,locator,browser.snapshot,593.495,18,461,false -cdb-aligned-devframe-final,devframe,locator,browser.batch,1195.198,261,610,false -cdb-aligned-devframe-final,devframe,locator,browser.fill,363.917,118,56,false -cdb-aligned-devframe-final,devframe,locator,browser.click,364.284,97,57,false -cdb-aligned-devframe-final,devframe,locator,browser.snapshot,667.268,18,461,false -cdb-aligned-devframe-final,devframe,locator,browser.batch,1173.217,261,610,false -cdb-aligned-devframe-final,devframe,locator,browser.fill,390.237,118,56,false -cdb-aligned-devframe-final,devframe,locator,browser.click,317.914,97,57,false -cdb-aligned-devframe-final,devframe,locator,browser.snapshot,598.087,18,461,false -cdb-aligned-devframe-final,devframe,locator,browser.batch,1246.020,261,610,false -cdb-aligned-devframe-final,devframe,locator,browser.fill,359.646,118,56,false -cdb-aligned-devframe-final,devframe,locator,browser.click,341.275,97,57,false -cdb-aligned-devframe-final,devframe,locator,browser.snapshot,630.758,18,461,false -cdb-aligned-devframe-final,devframe,locator,browser.batch,1116.495,261,610,false -cdb-aligned-devframe-final,devframe,locator,browser.fill,319.452,118,56,false -cdb-aligned-devframe-final,devframe,locator,browser.click,388.269,97,57,false -cdb-aligned-devframe-final,devframe,locator,browser.snapshot,614.624,18,461,false -cdb-aligned-devframe-final,devframe,locator,browser.batch,1153.822,261,610,false -cdb-aligned-devframe-final,devframe,locator,browser.fill,275.739,118,56,false -cdb-aligned-devframe-final,devframe,locator,browser.click,284.748,97,57,false -cdb-aligned-devframe-final,devframe,locator,browser.snapshot,601.509,18,461,false -cdb-aligned-devframe-final,devframe,locator,browser.batch,1105.548,261,610,false -cdb-aligned-devframe-final,devframe,locator,browser.fill,339.995,118,56,false -cdb-aligned-devframe-final,devframe,locator,browser.click,293.390,97,57,false -cdb-aligned-devframe-final,devframe,locator,browser.snapshot,583.573,18,461,false -cdb-aligned-devframe-final,devframe,locator,browser.batch,1407.198,261,610,false -cdb-aligned-devframe-final,devframe,locator,browser.fill,324.645,118,56,false -cdb-aligned-devframe-final,devframe,locator,browser.click,332.169,97,57,false -cdb-aligned-devframe-final,devframe,locator,browser.snapshot,601.874,18,461,false -cdb-aligned-devframe-final,devframe,locator,browser.batch,1076.067,261,610,false -cdb-aligned-devframe-final,devframe,locator,browser.fill,298.937,118,56,false -cdb-aligned-devframe-final,devframe,locator,browser.click,279.638,97,57,false -cdb-aligned-devframe-final,devframe,locator,browser.snapshot,634.963,18,461,false -cdb-aligned-devframe-final,devframe,locator,browser.batch,1123.801,261,610,false -cdb-aligned-devframe-final,devframe,locator,browser.fill,335.841,118,56,false -cdb-aligned-devframe-final,devframe,locator,browser.click,288.333,97,57,false -cdb-aligned-devframe-final,devframe,locator,browser.snapshot,716.417,18,461,false -cdb-aligned-devframe-final,devframe,locator,browser.batch,1298.758,261,610,false -cdb-aligned-devframe-final,devframe,locator,browser.fill,340.047,118,56,false -cdb-aligned-devframe-final,devframe,locator,browser.click,284.323,97,57,false -cdb-aligned-devframe-final,devframe,locator,browser.snapshot,685.227,18,461,false -cdb-aligned-devframe-final,devframe,locator,browser.batch,1174.204,261,610,false -cdb-aligned-devframe-final,devframe,locator,browser.fill,313.723,118,56,false -cdb-aligned-devframe-final,devframe,locator,browser.click,413.386,97,57,false -cdb-aligned-devframe-final,devframe,locator,browser.snapshot,621.537,18,461,false -cdb-aligned-devframe-final,devframe,locator,browser.batch,1193.260,261,610,false -cdb-aligned-devframe-final,devframe,locator,browser.fill,341.308,118,56,false -cdb-aligned-devframe-final,devframe,locator,browser.click,341.048,97,57,false -cdb-aligned-devframe-final,devframe,locator,browser.snapshot,646.385,18,461,false -cdb-aligned-devframe-final,devframe,locator,browser.batch,1200.274,261,610,false -cdb-aligned-devframe-final,devframe,locator,browser.fill,435.978,118,56,false -cdb-aligned-devframe-final,devframe,locator,browser.click,305.365,97,57,false -cdb-aligned-devframe-final,devframe,locator,browser.snapshot,675.460,18,461,false -cdb-aligned-devframe-final,devframe,locator,browser.batch,1146.640,261,610,false -cdb-aligned-devframe-final,devframe,locator,browser.click:blocked,2001.521,97,197,true -cdb-aligned-devframe-final,devframe,locator,browser.click:blocked,2000.465,97,197,true -cdb-aligned-devframe-final,devframe,locator,browser.click:blocked,2003.136,97,197,true -cdb-aligned-devframe-final,devframe,locator,browser.click:blocked,2001.352,97,197,true -cdb-aligned-devframe-final,devframe,locator,browser.click:blocked,2013.479,97,197,true -cdb-aligned-devframe-final,devframe,locator,browser.click:blocked,2002.140,97,197,true -cdb-aligned-devframe-final,devframe,locator,browser.click:blocked,2001.411,97,197,true -cdb-aligned-devframe-final,devframe,locator,browser.click:blocked,2001.020,97,197,true -cdb-aligned-devframe-final,devframe,locator,browser.click:blocked,2001.642,97,197,true -cdb-aligned-devframe-final,devframe,locator,browser.click:blocked,2000.741,97,197,true -cdb-aligned-devframe-final,devframe,locator,browser.click:blocked,2002.094,97,197,true -cdb-aligned-devframe-final,devframe,locator,browser.click:blocked,2002.356,97,197,true -cdb-aligned-devframe-final,devframe,locator,browser.click:blocked,2007.245,97,197,true -cdb-aligned-devframe-final,devframe,locator,browser.click:blocked,2001.516,97,197,true -cdb-aligned-devframe-final,devframe,locator,browser.click:blocked,2001.842,97,197,true -cdb-aligned-devframe-final,devframe,locator,browser.click:blocked,2001.987,97,197,true -cdb-aligned-devframe-final,devframe,locator,browser.click:blocked,2001.793,97,197,true -cdb-aligned-devframe-final,devframe,locator,browser.click:blocked,2002.590,97,197,true -cdb-aligned-devframe-final,devframe,locator,browser.click:blocked,2160.022,97,197,true -cdb-aligned-devframe-final,devframe,locator,browser.click:blocked,2002.763,97,197,true -cdb-aligned-devframe-final,devframe,locator,browser.click:blocked,2001.481,97,197,true -cdb-aligned-devframe-final,devframe,locator,browser.click:blocked,2038.299,97,197,true -cdb-aligned-devframe-final,devframe,locator,browser.click:blocked,2000.835,97,197,true -cdb-aligned-devframe-final,devframe,locator,browser.click:blocked,2001.782,97,197,true -cdb-aligned-devframe-final,devframe,locator,browser.click:blocked,2001.691,97,197,true -cdb-aligned-devframe-final,devframe,locator,browser.click:blocked,2063.094,97,197,true -cdb-aligned-devframe-final,devframe,locator,browser.click:blocked,2001.165,97,197,true -cdb-aligned-devframe-final,devframe,locator,browser.click:blocked,2002.930,97,197,true -cdb-aligned-devframe-final,devframe,locator,browser.click:blocked,2001.441,97,197,true -cdb-aligned-devframe-final,devframe,locator,browser.click:blocked,2001.192,97,197,true -cdb-aligned-websocket-parallel,websocket,ref,browser.fill,110.349,55,56,false -cdb-aligned-websocket-parallel,websocket,ref,browser.click,164.319,30,57,false -cdb-aligned-websocket-parallel,websocket,ref,browser.snapshot,693.576,18,456,false -cdb-aligned-websocket-parallel,websocket,ref,browser.batch,1049.033,131,605,false -cdb-aligned-websocket-parallel,websocket,ref,browser.fill,126.469,55,56,false -cdb-aligned-websocket-parallel,websocket,ref,browser.click,122.989,30,57,false -cdb-aligned-websocket-parallel,websocket,ref,browser.snapshot,712.034,18,456,false -cdb-aligned-websocket-parallel,websocket,ref,browser.batch,857.539,131,605,false -cdb-aligned-websocket-parallel,websocket,ref,browser.fill,108.178,55,56,false -cdb-aligned-websocket-parallel,websocket,ref,browser.click,123.634,30,57,false -cdb-aligned-websocket-parallel,websocket,ref,browser.snapshot,738.403,18,456,false -cdb-aligned-websocket-parallel,websocket,ref,browser.batch,881.006,131,605,false -cdb-aligned-websocket-parallel,websocket,ref,browser.fill,131.101,55,56,false -cdb-aligned-websocket-parallel,websocket,ref,browser.click,116.807,30,57,false -cdb-aligned-websocket-parallel,websocket,ref,browser.snapshot,689.484,18,456,false -cdb-aligned-websocket-parallel,websocket,ref,browser.batch,939.188,131,605,false -cdb-aligned-websocket-parallel,websocket,ref,browser.fill,207.175,55,56,false -cdb-aligned-websocket-parallel,websocket,ref,browser.click,181.065,30,57,false -cdb-aligned-websocket-parallel,websocket,ref,browser.snapshot,787.925,18,457,false -cdb-aligned-websocket-parallel,websocket,ref,browser.batch,1008.126,132,610,false -cdb-aligned-websocket-parallel,websocket,ref,browser.fill,119.565,56,56,false -cdb-aligned-websocket-parallel,websocket,ref,browser.click,99.139,31,57,false -cdb-aligned-websocket-parallel,websocket,ref,browser.snapshot,607.530,18,461,false -cdb-aligned-websocket-parallel,websocket,ref,browser.batch,973.006,133,610,false -cdb-aligned-websocket-parallel,websocket,ref,browser.fill,137.401,56,56,false -cdb-aligned-websocket-parallel,websocket,ref,browser.click,99.396,31,57,false -cdb-aligned-websocket-parallel,websocket,ref,browser.snapshot,637.585,18,461,false -cdb-aligned-websocket-parallel,websocket,ref,browser.batch,1044.072,133,610,false -cdb-aligned-websocket-parallel,websocket,ref,browser.fill,110.177,56,56,false -cdb-aligned-websocket-parallel,websocket,ref,browser.click,133.755,31,57,false -cdb-aligned-websocket-parallel,websocket,ref,browser.snapshot,786.440,18,461,false -cdb-aligned-websocket-parallel,websocket,ref,browser.batch,1079.344,133,610,false -cdb-aligned-websocket-parallel,websocket,ref,browser.fill,111.786,56,56,false -cdb-aligned-websocket-parallel,websocket,ref,browser.click,93.484,31,57,false -cdb-aligned-websocket-parallel,websocket,ref,browser.snapshot,766.016,18,461,false -cdb-aligned-websocket-parallel,websocket,ref,browser.batch,1177.653,133,610,false -cdb-aligned-websocket-parallel,websocket,ref,browser.fill,135.028,56,56,false -cdb-aligned-websocket-parallel,websocket,ref,browser.click,92.730,31,57,false -cdb-aligned-websocket-parallel,websocket,ref,browser.snapshot,951.966,18,461,false -cdb-aligned-websocket-parallel,websocket,ref,browser.batch,990.045,133,610,false -cdb-aligned-websocket-parallel,websocket,ref,browser.fill,103.306,56,56,false -cdb-aligned-websocket-parallel,websocket,ref,browser.click,99.970,31,57,false -cdb-aligned-websocket-parallel,websocket,ref,browser.snapshot,829.333,18,461,false -cdb-aligned-websocket-parallel,websocket,ref,browser.batch,1041.501,133,610,false -cdb-aligned-websocket-parallel,websocket,ref,browser.fill,113.457,56,56,false -cdb-aligned-websocket-parallel,websocket,ref,browser.click,102.575,31,57,false -cdb-aligned-websocket-parallel,websocket,ref,browser.snapshot,972.310,18,461,false -cdb-aligned-websocket-parallel,websocket,ref,browser.batch,1142.404,133,610,false -cdb-aligned-websocket-parallel,websocket,ref,browser.fill,110.661,56,56,false -cdb-aligned-websocket-parallel,websocket,ref,browser.click,100.616,31,57,false -cdb-aligned-websocket-parallel,websocket,ref,browser.snapshot,1019.410,18,461,false -cdb-aligned-websocket-parallel,websocket,ref,browser.batch,1057.046,133,610,false -cdb-aligned-websocket-parallel,websocket,ref,browser.fill,121.422,56,56,false -cdb-aligned-websocket-parallel,websocket,ref,browser.click,111.216,31,57,false -cdb-aligned-websocket-parallel,websocket,ref,browser.snapshot,1035.068,18,461,false -cdb-aligned-websocket-parallel,websocket,ref,browser.batch,1820.802,133,610,false -cdb-aligned-websocket-parallel,websocket,ref,browser.fill,116.403,56,56,false -cdb-aligned-websocket-parallel,websocket,ref,browser.click,118.669,31,57,false -cdb-aligned-websocket-parallel,websocket,ref,browser.snapshot,946.644,18,461,false -cdb-aligned-websocket-parallel,websocket,ref,browser.batch,950.472,133,610,false -cdb-aligned-websocket-parallel,websocket,ref,browser.fill,116.066,56,56,false -cdb-aligned-websocket-parallel,websocket,ref,browser.click,93.150,31,57,false -cdb-aligned-websocket-parallel,websocket,ref,browser.snapshot,1057.186,18,461,false -cdb-aligned-websocket-parallel,websocket,ref,browser.batch,957.435,133,610,false -cdb-aligned-websocket-parallel,websocket,ref,browser.fill,117.712,56,56,false -cdb-aligned-websocket-parallel,websocket,ref,browser.click,153.857,31,57,false -cdb-aligned-websocket-parallel,websocket,ref,browser.snapshot,850.697,18,461,false -cdb-aligned-websocket-parallel,websocket,ref,browser.batch,979.736,133,610,false -cdb-aligned-websocket-parallel,websocket,ref,browser.fill,138.779,56,56,false -cdb-aligned-websocket-parallel,websocket,ref,browser.click,91.395,31,57,false -cdb-aligned-websocket-parallel,websocket,ref,browser.snapshot,774.771,18,461,false -cdb-aligned-websocket-parallel,websocket,ref,browser.batch,1030.622,133,610,false -cdb-aligned-websocket-parallel,websocket,ref,browser.fill,101.021,56,56,false -cdb-aligned-websocket-parallel,websocket,ref,browser.click,91.131,31,57,false -cdb-aligned-websocket-parallel,websocket,ref,browser.snapshot,720.719,18,461,false -cdb-aligned-websocket-parallel,websocket,ref,browser.batch,1364.584,133,610,false -cdb-aligned-websocket-parallel,websocket,ref,browser.fill,106.906,56,56,false -cdb-aligned-websocket-parallel,websocket,ref,browser.click,105.362,31,57,false -cdb-aligned-websocket-parallel,websocket,ref,browser.snapshot,828.797,18,461,false -cdb-aligned-websocket-parallel,websocket,ref,browser.batch,908.623,133,610,false -cdb-aligned-websocket-parallel,websocket,ref,browser.fill,129.382,56,56,false -cdb-aligned-websocket-parallel,websocket,ref,browser.click,106.926,31,57,false -cdb-aligned-websocket-parallel,websocket,ref,browser.snapshot,768.416,18,461,false -cdb-aligned-websocket-parallel,websocket,ref,browser.batch,1048.596,133,610,false -cdb-aligned-websocket-parallel,websocket,ref,browser.fill,104.211,56,56,false -cdb-aligned-websocket-parallel,websocket,ref,browser.click,94.817,31,57,false -cdb-aligned-websocket-parallel,websocket,ref,browser.snapshot,633.342,18,461,false -cdb-aligned-websocket-parallel,websocket,ref,browser.batch,2214.394,133,610,false -cdb-aligned-websocket-parallel,websocket,ref,browser.fill,143.629,56,56,false -cdb-aligned-websocket-parallel,websocket,ref,browser.click,152.732,31,57,false -cdb-aligned-websocket-parallel,websocket,ref,browser.snapshot,954.444,18,461,false -cdb-aligned-websocket-parallel,websocket,ref,browser.batch,1186.991,133,610,false -cdb-aligned-websocket-parallel,websocket,ref,browser.fill,115.693,56,56,false -cdb-aligned-websocket-parallel,websocket,ref,browser.click,148.189,31,57,false -cdb-aligned-websocket-parallel,websocket,ref,browser.snapshot,777.599,18,461,false -cdb-aligned-websocket-parallel,websocket,ref,browser.batch,1054.380,133,610,false -cdb-aligned-websocket-parallel,websocket,ref,browser.fill,130.046,56,56,false -cdb-aligned-websocket-parallel,websocket,ref,browser.click,156.248,31,57,false -cdb-aligned-websocket-parallel,websocket,ref,browser.snapshot,731.703,18,461,false -cdb-aligned-websocket-parallel,websocket,ref,browser.batch,1171.236,133,610,false -cdb-aligned-websocket-parallel,websocket,ref,browser.fill,136.566,56,56,false -cdb-aligned-websocket-parallel,websocket,ref,browser.click,115.851,31,57,false -cdb-aligned-websocket-parallel,websocket,ref,browser.snapshot,700.691,18,461,false -cdb-aligned-websocket-parallel,websocket,ref,browser.batch,823.961,133,610,false -cdb-aligned-websocket-parallel,websocket,ref,browser.fill,134.948,56,56,false -cdb-aligned-websocket-parallel,websocket,ref,browser.click,189.183,31,57,false -cdb-aligned-websocket-parallel,websocket,ref,browser.snapshot,1415.783,18,461,false -cdb-aligned-websocket-parallel,websocket,ref,browser.batch,1145.585,133,610,false -cdb-aligned-websocket-parallel,websocket,ref,browser.fill,162.917,56,56,false -cdb-aligned-websocket-parallel,websocket,ref,browser.click,129.704,31,57,false -cdb-aligned-websocket-parallel,websocket,ref,browser.snapshot,920.290,18,461,false -cdb-aligned-websocket-parallel,websocket,ref,browser.batch,1005.237,133,610,false -cdb-aligned-websocket-parallel,websocket,ref,browser.fill,107.833,56,56,false -cdb-aligned-websocket-parallel,websocket,ref,browser.click,106.504,31,57,false -cdb-aligned-websocket-parallel,websocket,ref,browser.snapshot,678.908,18,461,false -cdb-aligned-websocket-parallel,websocket,ref,browser.batch,1014.632,133,610,false -cdb-aligned-websocket-parallel,websocket,ref,browser.fill,154.685,56,56,false -cdb-aligned-websocket-parallel,websocket,ref,browser.click,113.525,31,57,false -cdb-aligned-websocket-parallel,websocket,ref,browser.snapshot,784.899,18,461,false -cdb-aligned-websocket-parallel,websocket,ref,browser.batch,1175.897,133,610,false -cdb-aligned-websocket-parallel,websocket,ref,browser.click:blocked,2002.806,31,172,true -cdb-aligned-websocket-parallel,websocket,ref,browser.click:blocked,2001.768,31,172,true -cdb-aligned-websocket-parallel,websocket,ref,browser.click:blocked,2001.700,31,172,true -cdb-aligned-websocket-parallel,websocket,ref,browser.click:blocked,2002.180,31,172,true -cdb-aligned-websocket-parallel,websocket,ref,browser.click:blocked,2000.952,31,172,true -cdb-aligned-websocket-parallel,websocket,ref,browser.click:blocked,2001.833,31,172,true -cdb-aligned-websocket-parallel,websocket,ref,browser.click:blocked,2002.386,31,172,true -cdb-aligned-websocket-parallel,websocket,ref,browser.click:blocked,2001.544,31,172,true -cdb-aligned-websocket-parallel,websocket,ref,browser.click:blocked,2000.259,31,172,true -cdb-aligned-websocket-parallel,websocket,ref,browser.click:blocked,2002.446,31,172,true -cdb-aligned-websocket-parallel,websocket,ref,browser.click:blocked,2002.160,31,172,true -cdb-aligned-websocket-parallel,websocket,ref,browser.click:blocked,2002.779,31,172,true -cdb-aligned-websocket-parallel,websocket,ref,browser.click:blocked,2001.890,31,172,true -cdb-aligned-websocket-parallel,websocket,ref,browser.click:blocked,2006.350,31,172,true -cdb-aligned-websocket-parallel,websocket,ref,browser.click:blocked,2001.961,31,172,true -cdb-aligned-websocket-parallel,websocket,ref,browser.click:blocked,2001.752,31,172,true -cdb-aligned-websocket-parallel,websocket,ref,browser.click:blocked,2004.805,31,172,true -cdb-aligned-websocket-parallel,websocket,ref,browser.click:blocked,2002.063,31,172,true -cdb-aligned-websocket-parallel,websocket,ref,browser.click:blocked,2002.033,31,172,true -cdb-aligned-websocket-parallel,websocket,ref,browser.click:blocked,2000.879,31,172,true -cdb-aligned-websocket-parallel,websocket,ref,browser.click:blocked,2001.715,31,172,true -cdb-aligned-websocket-parallel,websocket,ref,browser.click:blocked,2003.600,31,172,true -cdb-aligned-websocket-parallel,websocket,ref,browser.click:blocked,2003.396,31,172,true -cdb-aligned-websocket-parallel,websocket,ref,browser.click:blocked,2000.732,31,172,true -cdb-aligned-websocket-parallel,websocket,ref,browser.click:blocked,2001.205,31,172,true -cdb-aligned-websocket-parallel,websocket,ref,browser.click:blocked,2001.392,31,172,true -cdb-aligned-websocket-parallel,websocket,ref,browser.click:blocked,2002.487,31,172,true -cdb-aligned-websocket-parallel,websocket,ref,browser.click:blocked,2002.153,31,172,true -cdb-aligned-websocket-parallel,websocket,ref,browser.click:blocked,2001.326,31,172,true -cdb-aligned-websocket-parallel,websocket,ref,browser.click:blocked,2002.353,31,172,true -cdb-aligned-websocket-parallel,websocket,locator,browser.fill,186.919,118,56,false -cdb-aligned-websocket-parallel,websocket,locator,browser.click,157.957,97,57,false -cdb-aligned-websocket-parallel,websocket,locator,browser.snapshot,624.641,18,461,false -cdb-aligned-websocket-parallel,websocket,locator,browser.batch,878.213,261,610,false -cdb-aligned-websocket-parallel,websocket,locator,browser.fill,170.292,118,56,false -cdb-aligned-websocket-parallel,websocket,locator,browser.click,171.458,97,57,false -cdb-aligned-websocket-parallel,websocket,locator,browser.snapshot,631.029,18,461,false -cdb-aligned-websocket-parallel,websocket,locator,browser.batch,918.305,261,610,false -cdb-aligned-websocket-parallel,websocket,locator,browser.fill,792.121,118,56,false -cdb-aligned-websocket-parallel,websocket,locator,browser.click,492.599,97,57,false -cdb-aligned-websocket-parallel,websocket,locator,browser.snapshot,1074.950,18,461,false -cdb-aligned-websocket-parallel,websocket,locator,browser.batch,894.465,261,610,false -cdb-aligned-websocket-parallel,websocket,locator,browser.fill,207.911,118,56,false -cdb-aligned-websocket-parallel,websocket,locator,browser.click,177.870,97,57,false -cdb-aligned-websocket-parallel,websocket,locator,browser.snapshot,646.864,18,461,false -cdb-aligned-websocket-parallel,websocket,locator,browser.batch,899.422,261,610,false -cdb-aligned-websocket-parallel,websocket,locator,browser.fill,167.055,118,56,false -cdb-aligned-websocket-parallel,websocket,locator,browser.click,175.318,97,57,false -cdb-aligned-websocket-parallel,websocket,locator,browser.snapshot,688.071,18,461,false -cdb-aligned-websocket-parallel,websocket,locator,browser.batch,944.081,261,610,false -cdb-aligned-websocket-parallel,websocket,locator,browser.fill,203.437,118,56,false -cdb-aligned-websocket-parallel,websocket,locator,browser.click,187.904,97,57,false -cdb-aligned-websocket-parallel,websocket,locator,browser.snapshot,781.198,18,461,false -cdb-aligned-websocket-parallel,websocket,locator,browser.batch,885.090,261,610,false -cdb-aligned-websocket-parallel,websocket,locator,browser.fill,181.080,118,56,false -cdb-aligned-websocket-parallel,websocket,locator,browser.click,174.575,97,57,false -cdb-aligned-websocket-parallel,websocket,locator,browser.snapshot,624.320,18,461,false -cdb-aligned-websocket-parallel,websocket,locator,browser.batch,875.409,261,610,false -cdb-aligned-websocket-parallel,websocket,locator,browser.fill,169.806,118,56,false -cdb-aligned-websocket-parallel,websocket,locator,browser.click,156.707,97,57,false -cdb-aligned-websocket-parallel,websocket,locator,browser.snapshot,846.582,18,461,false -cdb-aligned-websocket-parallel,websocket,locator,browser.batch,985.931,261,610,false -cdb-aligned-websocket-parallel,websocket,locator,browser.fill,244.171,118,56,false -cdb-aligned-websocket-parallel,websocket,locator,browser.click,370.548,97,57,false -cdb-aligned-websocket-parallel,websocket,locator,browser.snapshot,881.152,18,461,false -cdb-aligned-websocket-parallel,websocket,locator,browser.batch,1122.031,261,610,false -cdb-aligned-websocket-parallel,websocket,locator,browser.fill,305.344,118,56,false -cdb-aligned-websocket-parallel,websocket,locator,browser.click,268.498,97,57,false -cdb-aligned-websocket-parallel,websocket,locator,browser.snapshot,2400.201,18,461,false -cdb-aligned-websocket-parallel,websocket,locator,browser.batch,1168.382,261,610,false -cdb-aligned-websocket-parallel,websocket,locator,browser.fill,310.887,118,56,false -cdb-aligned-websocket-parallel,websocket,locator,browser.click,291.318,97,57,false -cdb-aligned-websocket-parallel,websocket,locator,browser.snapshot,992.076,18,461,false -cdb-aligned-websocket-parallel,websocket,locator,browser.batch,1083.283,261,610,false -cdb-aligned-websocket-parallel,websocket,locator,browser.fill,276.059,118,56,false -cdb-aligned-websocket-parallel,websocket,locator,browser.click,168.151,97,57,false -cdb-aligned-websocket-parallel,websocket,locator,browser.snapshot,738.523,18,461,false -cdb-aligned-websocket-parallel,websocket,locator,browser.batch,1038.702,261,610,false -cdb-aligned-websocket-parallel,websocket,locator,browser.fill,201.661,118,56,false -cdb-aligned-websocket-parallel,websocket,locator,browser.click,167.604,97,57,false -cdb-aligned-websocket-parallel,websocket,locator,browser.snapshot,1021.818,18,461,false -cdb-aligned-websocket-parallel,websocket,locator,browser.batch,1031.407,261,610,false -cdb-aligned-websocket-parallel,websocket,locator,browser.fill,186.432,118,56,false -cdb-aligned-websocket-parallel,websocket,locator,browser.click,184.637,97,57,false -cdb-aligned-websocket-parallel,websocket,locator,browser.snapshot,932.359,18,461,false -cdb-aligned-websocket-parallel,websocket,locator,browser.batch,1123.913,261,610,false -cdb-aligned-websocket-parallel,websocket,locator,browser.fill,200.625,118,56,false -cdb-aligned-websocket-parallel,websocket,locator,browser.click,186.374,97,57,false -cdb-aligned-websocket-parallel,websocket,locator,browser.snapshot,949.285,18,461,false -cdb-aligned-websocket-parallel,websocket,locator,browser.batch,1057.290,261,610,false -cdb-aligned-websocket-parallel,websocket,locator,browser.fill,176.076,118,56,false -cdb-aligned-websocket-parallel,websocket,locator,browser.click,159.764,97,57,false -cdb-aligned-websocket-parallel,websocket,locator,browser.snapshot,805.208,18,461,false -cdb-aligned-websocket-parallel,websocket,locator,browser.batch,1158.724,261,610,false -cdb-aligned-websocket-parallel,websocket,locator,browser.fill,184.754,118,56,false -cdb-aligned-websocket-parallel,websocket,locator,browser.click,242.464,97,57,false -cdb-aligned-websocket-parallel,websocket,locator,browser.snapshot,880.564,18,461,false -cdb-aligned-websocket-parallel,websocket,locator,browser.batch,1085.170,261,610,false -cdb-aligned-websocket-parallel,websocket,locator,browser.fill,175.074,118,56,false -cdb-aligned-websocket-parallel,websocket,locator,browser.click,149.245,97,57,false -cdb-aligned-websocket-parallel,websocket,locator,browser.snapshot,597.488,18,461,false -cdb-aligned-websocket-parallel,websocket,locator,browser.batch,898.255,261,610,false -cdb-aligned-websocket-parallel,websocket,locator,browser.fill,183.960,118,56,false -cdb-aligned-websocket-parallel,websocket,locator,browser.click,159.084,97,57,false -cdb-aligned-websocket-parallel,websocket,locator,browser.snapshot,658.645,18,461,false -cdb-aligned-websocket-parallel,websocket,locator,browser.batch,915.920,261,610,false -cdb-aligned-websocket-parallel,websocket,locator,browser.fill,172.895,118,56,false -cdb-aligned-websocket-parallel,websocket,locator,browser.click,170.232,97,57,false -cdb-aligned-websocket-parallel,websocket,locator,browser.snapshot,676.669,18,461,false -cdb-aligned-websocket-parallel,websocket,locator,browser.batch,912.837,261,610,false -cdb-aligned-websocket-parallel,websocket,locator,browser.fill,166.385,118,56,false -cdb-aligned-websocket-parallel,websocket,locator,browser.click,157.058,97,57,false -cdb-aligned-websocket-parallel,websocket,locator,browser.snapshot,623.304,18,461,false -cdb-aligned-websocket-parallel,websocket,locator,browser.batch,3187.630,261,610,false -cdb-aligned-websocket-parallel,websocket,locator,browser.fill,190.422,118,56,false -cdb-aligned-websocket-parallel,websocket,locator,browser.click,157.176,97,57,false -cdb-aligned-websocket-parallel,websocket,locator,browser.snapshot,704.043,18,461,false -cdb-aligned-websocket-parallel,websocket,locator,browser.batch,1003.968,261,610,false -cdb-aligned-websocket-parallel,websocket,locator,browser.fill,181.559,118,56,false -cdb-aligned-websocket-parallel,websocket,locator,browser.click,161.496,97,57,false -cdb-aligned-websocket-parallel,websocket,locator,browser.snapshot,655.429,18,461,false -cdb-aligned-websocket-parallel,websocket,locator,browser.batch,921.803,261,610,false -cdb-aligned-websocket-parallel,websocket,locator,browser.fill,209.655,118,56,false -cdb-aligned-websocket-parallel,websocket,locator,browser.click,172.741,97,57,false -cdb-aligned-websocket-parallel,websocket,locator,browser.snapshot,659.700,18,461,false -cdb-aligned-websocket-parallel,websocket,locator,browser.batch,928.558,261,610,false -cdb-aligned-websocket-parallel,websocket,locator,browser.fill,171.313,118,56,false -cdb-aligned-websocket-parallel,websocket,locator,browser.click,190.029,97,57,false -cdb-aligned-websocket-parallel,websocket,locator,browser.snapshot,587.254,18,461,false -cdb-aligned-websocket-parallel,websocket,locator,browser.batch,843.839,261,610,false -cdb-aligned-websocket-parallel,websocket,locator,browser.fill,166.008,118,56,false -cdb-aligned-websocket-parallel,websocket,locator,browser.click,333.554,97,57,false -cdb-aligned-websocket-parallel,websocket,locator,browser.snapshot,2649.651,18,461,false -cdb-aligned-websocket-parallel,websocket,locator,browser.batch,1096.055,261,610,false -cdb-aligned-websocket-parallel,websocket,locator,browser.fill,162.828,118,56,false -cdb-aligned-websocket-parallel,websocket,locator,browser.click,152.435,97,57,false -cdb-aligned-websocket-parallel,websocket,locator,browser.snapshot,604.902,18,461,false -cdb-aligned-websocket-parallel,websocket,locator,browser.batch,904.953,261,610,false -cdb-aligned-websocket-parallel,websocket,locator,browser.fill,176.077,118,56,false -cdb-aligned-websocket-parallel,websocket,locator,browser.click,153.202,97,57,false -cdb-aligned-websocket-parallel,websocket,locator,browser.snapshot,588.750,18,461,false -cdb-aligned-websocket-parallel,websocket,locator,browser.batch,848.441,261,610,false -cdb-aligned-websocket-parallel,websocket,locator,browser.fill,173.684,118,56,false -cdb-aligned-websocket-parallel,websocket,locator,browser.click,183.442,97,57,false -cdb-aligned-websocket-parallel,websocket,locator,browser.snapshot,634.614,18,461,false -cdb-aligned-websocket-parallel,websocket,locator,browser.batch,952.296,261,610,false -cdb-aligned-websocket-parallel,websocket,locator,browser.fill,172.646,118,56,false -cdb-aligned-websocket-parallel,websocket,locator,browser.click,154.571,97,57,false -cdb-aligned-websocket-parallel,websocket,locator,browser.snapshot,710.832,18,461,false -cdb-aligned-websocket-parallel,websocket,locator,browser.batch,891.158,261,610,false -cdb-aligned-websocket-parallel,websocket,locator,browser.click:blocked,2000.715,97,172,true -cdb-aligned-websocket-parallel,websocket,locator,browser.click:blocked,2001.658,97,172,true -cdb-aligned-websocket-parallel,websocket,locator,browser.click:blocked,2001.335,97,172,true -cdb-aligned-websocket-parallel,websocket,locator,browser.click:blocked,2001.192,97,172,true -cdb-aligned-websocket-parallel,websocket,locator,browser.click:blocked,1999.984,97,172,true -cdb-aligned-websocket-parallel,websocket,locator,browser.click:blocked,2000.683,97,172,true -cdb-aligned-websocket-parallel,websocket,locator,browser.click:blocked,2001.946,97,172,true -cdb-aligned-websocket-parallel,websocket,locator,browser.click:blocked,2000.643,97,172,true -cdb-aligned-websocket-parallel,websocket,locator,browser.click:blocked,2001.977,97,172,true -cdb-aligned-websocket-parallel,websocket,locator,browser.click:blocked,2002.659,97,172,true -cdb-aligned-websocket-parallel,websocket,locator,browser.click:blocked,2001.788,97,172,true -cdb-aligned-websocket-parallel,websocket,locator,browser.click:blocked,2001.821,97,172,true -cdb-aligned-websocket-parallel,websocket,locator,browser.click:blocked,2001.168,97,172,true -cdb-aligned-websocket-parallel,websocket,locator,browser.click:blocked,2001.263,97,172,true -cdb-aligned-websocket-parallel,websocket,locator,browser.click:blocked,1999.656,97,172,true -cdb-aligned-websocket-parallel,websocket,locator,browser.click:blocked,2002.116,97,172,true -cdb-aligned-websocket-parallel,websocket,locator,browser.click:blocked,2001.195,97,172,true -cdb-aligned-websocket-parallel,websocket,locator,browser.click:blocked,2002.438,97,172,true -cdb-aligned-websocket-parallel,websocket,locator,browser.click:blocked,2000.673,97,172,true -cdb-aligned-websocket-parallel,websocket,locator,browser.click:blocked,2002.514,97,172,true -cdb-aligned-websocket-parallel,websocket,locator,browser.click:blocked,2001.843,97,172,true -cdb-aligned-websocket-parallel,websocket,locator,browser.click:blocked,2001.219,97,172,true -cdb-aligned-websocket-parallel,websocket,locator,browser.click:blocked,2111.669,97,172,true -cdb-aligned-websocket-parallel,websocket,locator,browser.click:blocked,2001.559,97,172,true -cdb-aligned-websocket-parallel,websocket,locator,browser.click:blocked,2001.060,97,172,true -cdb-aligned-websocket-parallel,websocket,locator,browser.click:blocked,2002.314,97,172,true -cdb-aligned-websocket-parallel,websocket,locator,browser.click:blocked,2001.773,97,172,true -cdb-aligned-websocket-parallel,websocket,locator,browser.click:blocked,2001.696,97,172,true -cdb-aligned-websocket-parallel,websocket,locator,browser.click:blocked,2000.856,97,172,true -cdb-aligned-websocket-parallel,websocket,locator,browser.click:blocked,2001.205,97,172,true -cdb-aligned-devframe-parallel,devframe,ref,browser.fill,91.667,55,56,false -cdb-aligned-devframe-parallel,devframe,ref,browser.click,89.024,30,57,false -cdb-aligned-devframe-parallel,devframe,ref,browser.snapshot,624.362,18,456,false -cdb-aligned-devframe-parallel,devframe,ref,browser.batch,761.904,131,605,false -cdb-aligned-devframe-parallel,devframe,ref,browser.fill,95.069,55,56,false -cdb-aligned-devframe-parallel,devframe,ref,browser.click,90.354,30,57,false -cdb-aligned-devframe-parallel,devframe,ref,browser.snapshot,658.250,18,456,false -cdb-aligned-devframe-parallel,devframe,ref,browser.batch,790.348,131,605,false -cdb-aligned-devframe-parallel,devframe,ref,browser.fill,96.795,55,56,false -cdb-aligned-devframe-parallel,devframe,ref,browser.click,87.989,30,57,false -cdb-aligned-devframe-parallel,devframe,ref,browser.snapshot,614.183,18,456,false -cdb-aligned-devframe-parallel,devframe,ref,browser.batch,831.230,131,605,false -cdb-aligned-devframe-parallel,devframe,ref,browser.fill,96.125,55,56,false -cdb-aligned-devframe-parallel,devframe,ref,browser.click,86.960,30,57,false -cdb-aligned-devframe-parallel,devframe,ref,browser.snapshot,579.877,18,456,false -cdb-aligned-devframe-parallel,devframe,ref,browser.batch,728.778,131,605,false -cdb-aligned-devframe-parallel,devframe,ref,browser.fill,92.671,55,56,false -cdb-aligned-devframe-parallel,devframe,ref,browser.click,85.669,30,57,false -cdb-aligned-devframe-parallel,devframe,ref,browser.snapshot,596.535,18,457,false -cdb-aligned-devframe-parallel,devframe,ref,browser.batch,749.437,132,610,false -cdb-aligned-devframe-parallel,devframe,ref,browser.fill,88.100,56,56,false -cdb-aligned-devframe-parallel,devframe,ref,browser.click,85.844,31,57,false -cdb-aligned-devframe-parallel,devframe,ref,browser.snapshot,615.740,18,461,false -cdb-aligned-devframe-parallel,devframe,ref,browser.batch,811.460,133,610,false -cdb-aligned-devframe-parallel,devframe,ref,browser.fill,114.554,56,56,false -cdb-aligned-devframe-parallel,devframe,ref,browser.click,96.538,31,57,false -cdb-aligned-devframe-parallel,devframe,ref,browser.snapshot,869.858,18,461,false -cdb-aligned-devframe-parallel,devframe,ref,browser.batch,926.199,133,610,false -cdb-aligned-devframe-parallel,devframe,ref,browser.fill,121.933,56,56,false -cdb-aligned-devframe-parallel,devframe,ref,browser.click,89.147,31,57,false -cdb-aligned-devframe-parallel,devframe,ref,browser.snapshot,675.819,18,461,false -cdb-aligned-devframe-parallel,devframe,ref,browser.batch,826.541,133,610,false -cdb-aligned-devframe-parallel,devframe,ref,browser.fill,112.547,56,56,false -cdb-aligned-devframe-parallel,devframe,ref,browser.click,91.110,31,57,false -cdb-aligned-devframe-parallel,devframe,ref,browser.snapshot,646.759,18,461,false -cdb-aligned-devframe-parallel,devframe,ref,browser.batch,813.701,133,610,false -cdb-aligned-devframe-parallel,devframe,ref,browser.fill,97.617,56,56,false -cdb-aligned-devframe-parallel,devframe,ref,browser.click,90.717,31,57,false -cdb-aligned-devframe-parallel,devframe,ref,browser.snapshot,597.095,18,461,false -cdb-aligned-devframe-parallel,devframe,ref,browser.batch,806.210,133,610,false -cdb-aligned-devframe-parallel,devframe,ref,browser.fill,110.219,56,56,false -cdb-aligned-devframe-parallel,devframe,ref,browser.click,119.468,31,57,false -cdb-aligned-devframe-parallel,devframe,ref,browser.snapshot,629.906,18,461,false -cdb-aligned-devframe-parallel,devframe,ref,browser.batch,817.025,133,610,false -cdb-aligned-devframe-parallel,devframe,ref,browser.fill,94.006,56,56,false -cdb-aligned-devframe-parallel,devframe,ref,browser.click,94.755,31,57,false -cdb-aligned-devframe-parallel,devframe,ref,browser.snapshot,589.674,18,461,false -cdb-aligned-devframe-parallel,devframe,ref,browser.batch,739.625,133,610,false -cdb-aligned-devframe-parallel,devframe,ref,browser.fill,96.109,56,56,false -cdb-aligned-devframe-parallel,devframe,ref,browser.click,92.243,31,57,false -cdb-aligned-devframe-parallel,devframe,ref,browser.snapshot,580.142,18,461,false -cdb-aligned-devframe-parallel,devframe,ref,browser.batch,747.817,133,610,false -cdb-aligned-devframe-parallel,devframe,ref,browser.fill,92.797,56,56,false -cdb-aligned-devframe-parallel,devframe,ref,browser.click,90.340,31,57,false -cdb-aligned-devframe-parallel,devframe,ref,browser.snapshot,1434.461,18,461,false -cdb-aligned-devframe-parallel,devframe,ref,browser.batch,1369.162,133,610,false -cdb-aligned-devframe-parallel,devframe,ref,browser.fill,113.176,56,56,false -cdb-aligned-devframe-parallel,devframe,ref,browser.click,106.822,31,57,false -cdb-aligned-devframe-parallel,devframe,ref,browser.snapshot,624.884,18,461,false -cdb-aligned-devframe-parallel,devframe,ref,browser.batch,823.458,133,610,false -cdb-aligned-devframe-parallel,devframe,ref,browser.fill,90.845,56,56,false -cdb-aligned-devframe-parallel,devframe,ref,browser.click,89.941,31,57,false -cdb-aligned-devframe-parallel,devframe,ref,browser.snapshot,593.698,18,461,false -cdb-aligned-devframe-parallel,devframe,ref,browser.batch,735.836,133,610,false -cdb-aligned-devframe-parallel,devframe,ref,browser.fill,99.790,56,56,false -cdb-aligned-devframe-parallel,devframe,ref,browser.click,127.825,31,57,false -cdb-aligned-devframe-parallel,devframe,ref,browser.snapshot,608.446,18,461,false -cdb-aligned-devframe-parallel,devframe,ref,browser.batch,761.048,133,610,false -cdb-aligned-devframe-parallel,devframe,ref,browser.fill,95.144,56,56,false -cdb-aligned-devframe-parallel,devframe,ref,browser.click,87.736,31,57,false -cdb-aligned-devframe-parallel,devframe,ref,browser.snapshot,639.191,18,461,false -cdb-aligned-devframe-parallel,devframe,ref,browser.batch,741.015,133,610,false -cdb-aligned-devframe-parallel,devframe,ref,browser.fill,118.654,56,56,false -cdb-aligned-devframe-parallel,devframe,ref,browser.click,91.948,31,57,false -cdb-aligned-devframe-parallel,devframe,ref,browser.snapshot,585.938,18,461,false -cdb-aligned-devframe-parallel,devframe,ref,browser.batch,758.183,133,610,false -cdb-aligned-devframe-parallel,devframe,ref,browser.fill,104.214,56,56,false -cdb-aligned-devframe-parallel,devframe,ref,browser.click,84.800,31,57,false -cdb-aligned-devframe-parallel,devframe,ref,browser.snapshot,1931.217,18,461,false -cdb-aligned-devframe-parallel,devframe,ref,browser.batch,1278.607,133,610,false -cdb-aligned-devframe-parallel,devframe,ref,browser.fill,632.577,56,56,false -cdb-aligned-devframe-parallel,devframe,ref,browser.click,105.073,31,57,false -cdb-aligned-devframe-parallel,devframe,ref,browser.snapshot,660.242,18,461,false -cdb-aligned-devframe-parallel,devframe,ref,browser.batch,826.971,133,610,false -cdb-aligned-devframe-parallel,devframe,ref,browser.fill,92.506,56,56,false -cdb-aligned-devframe-parallel,devframe,ref,browser.click,89.643,31,57,false -cdb-aligned-devframe-parallel,devframe,ref,browser.snapshot,668.048,18,461,false -cdb-aligned-devframe-parallel,devframe,ref,browser.batch,796.927,133,610,false -cdb-aligned-devframe-parallel,devframe,ref,browser.fill,103.884,56,56,false -cdb-aligned-devframe-parallel,devframe,ref,browser.click,90.197,31,57,false -cdb-aligned-devframe-parallel,devframe,ref,browser.snapshot,671.967,18,461,false -cdb-aligned-devframe-parallel,devframe,ref,browser.batch,864.170,133,610,false -cdb-aligned-devframe-parallel,devframe,ref,browser.fill,97.514,56,56,false -cdb-aligned-devframe-parallel,devframe,ref,browser.click,106.835,31,57,false -cdb-aligned-devframe-parallel,devframe,ref,browser.snapshot,743.842,18,461,false -cdb-aligned-devframe-parallel,devframe,ref,browser.batch,978.128,133,610,false -cdb-aligned-devframe-parallel,devframe,ref,browser.fill,131.384,56,56,false -cdb-aligned-devframe-parallel,devframe,ref,browser.click,116.636,31,57,false -cdb-aligned-devframe-parallel,devframe,ref,browser.snapshot,711.136,18,461,false -cdb-aligned-devframe-parallel,devframe,ref,browser.batch,814.178,133,610,false -cdb-aligned-devframe-parallel,devframe,ref,browser.fill,96.480,56,56,false -cdb-aligned-devframe-parallel,devframe,ref,browser.click,88.202,31,57,false -cdb-aligned-devframe-parallel,devframe,ref,browser.snapshot,609.013,18,461,false -cdb-aligned-devframe-parallel,devframe,ref,browser.batch,745.690,133,610,false -cdb-aligned-devframe-parallel,devframe,ref,browser.fill,102.351,56,56,false -cdb-aligned-devframe-parallel,devframe,ref,browser.click,88.616,31,57,false -cdb-aligned-devframe-parallel,devframe,ref,browser.snapshot,633.731,18,461,false -cdb-aligned-devframe-parallel,devframe,ref,browser.batch,797.594,133,610,false -cdb-aligned-devframe-parallel,devframe,ref,browser.fill,90.237,56,56,false -cdb-aligned-devframe-parallel,devframe,ref,browser.click,89.141,31,57,false -cdb-aligned-devframe-parallel,devframe,ref,browser.snapshot,619.259,18,461,false -cdb-aligned-devframe-parallel,devframe,ref,browser.batch,779.927,133,610,false -cdb-aligned-devframe-parallel,devframe,ref,browser.fill,98.087,56,56,false -cdb-aligned-devframe-parallel,devframe,ref,browser.click,87.773,31,57,false -cdb-aligned-devframe-parallel,devframe,ref,browser.snapshot,605.417,18,461,false -cdb-aligned-devframe-parallel,devframe,ref,browser.batch,800.567,133,610,false -cdb-aligned-devframe-parallel,devframe,ref,browser.fill,94.995,56,56,false -cdb-aligned-devframe-parallel,devframe,ref,browser.click,88.496,31,57,false -cdb-aligned-devframe-parallel,devframe,ref,browser.snapshot,624.103,18,461,false -cdb-aligned-devframe-parallel,devframe,ref,browser.batch,763.434,133,610,false -cdb-aligned-devframe-parallel,devframe,ref,browser.click:blocked,2002.001,31,197,true -cdb-aligned-devframe-parallel,devframe,ref,browser.click:blocked,2002.302,31,197,true -cdb-aligned-devframe-parallel,devframe,ref,browser.click:blocked,2004.067,31,197,true -cdb-aligned-devframe-parallel,devframe,ref,browser.click:blocked,2003.002,31,197,true -cdb-aligned-devframe-parallel,devframe,ref,browser.click:blocked,2002.472,31,197,true -cdb-aligned-devframe-parallel,devframe,ref,browser.click:blocked,2003.026,31,197,true -cdb-aligned-devframe-parallel,devframe,ref,browser.click:blocked,2003.095,31,197,true -cdb-aligned-devframe-parallel,devframe,ref,browser.click:blocked,2001.926,31,197,true -cdb-aligned-devframe-parallel,devframe,ref,browser.click:blocked,2002.958,31,197,true -cdb-aligned-devframe-parallel,devframe,ref,browser.click:blocked,2002.513,31,197,true -cdb-aligned-devframe-parallel,devframe,ref,browser.click:blocked,2003.166,31,197,true -cdb-aligned-devframe-parallel,devframe,ref,browser.click:blocked,2006.542,31,197,true -cdb-aligned-devframe-parallel,devframe,ref,browser.click:blocked,2002.286,31,197,true -cdb-aligned-devframe-parallel,devframe,ref,browser.click:blocked,2001.347,31,197,true -cdb-aligned-devframe-parallel,devframe,ref,browser.click:blocked,2005.214,31,197,true -cdb-aligned-devframe-parallel,devframe,ref,browser.click:blocked,2007.669,31,197,true -cdb-aligned-devframe-parallel,devframe,ref,browser.click:blocked,2003.104,31,197,true -cdb-aligned-devframe-parallel,devframe,ref,browser.click:blocked,2004.251,31,197,true -cdb-aligned-devframe-parallel,devframe,ref,browser.click:blocked,2001.415,31,197,true -cdb-aligned-devframe-parallel,devframe,ref,browser.click:blocked,2003.275,31,197,true -cdb-aligned-devframe-parallel,devframe,ref,browser.click:blocked,2003.024,31,197,true -cdb-aligned-devframe-parallel,devframe,ref,browser.click:blocked,2002.451,31,197,true -cdb-aligned-devframe-parallel,devframe,ref,browser.click:blocked,2003.767,31,197,true -cdb-aligned-devframe-parallel,devframe,ref,browser.click:blocked,2001.129,31,197,true -cdb-aligned-devframe-parallel,devframe,ref,browser.click:blocked,2004.473,31,197,true -cdb-aligned-devframe-parallel,devframe,ref,browser.click:blocked,2002.208,31,197,true -cdb-aligned-devframe-parallel,devframe,ref,browser.click:blocked,2003.079,31,197,true -cdb-aligned-devframe-parallel,devframe,ref,browser.click:blocked,2005.112,31,197,true -cdb-aligned-devframe-parallel,devframe,ref,browser.click:blocked,2003.347,31,197,true -cdb-aligned-devframe-parallel,devframe,ref,browser.click:blocked,2017.630,31,197,true -cdb-aligned-devframe-parallel,devframe,locator,browser.fill,343.878,118,56,false -cdb-aligned-devframe-parallel,devframe,locator,browser.click,362.447,97,57,false -cdb-aligned-devframe-parallel,devframe,locator,browser.snapshot,649.495,18,461,false -cdb-aligned-devframe-parallel,devframe,locator,browser.batch,1200.727,261,610,false -cdb-aligned-devframe-parallel,devframe,locator,browser.fill,301.832,118,56,false -cdb-aligned-devframe-parallel,devframe,locator,browser.click,306.418,97,57,false -cdb-aligned-devframe-parallel,devframe,locator,browser.snapshot,612.640,18,461,false -cdb-aligned-devframe-parallel,devframe,locator,browser.batch,1185.483,261,610,false -cdb-aligned-devframe-parallel,devframe,locator,browser.fill,415.333,118,56,false -cdb-aligned-devframe-parallel,devframe,locator,browser.click,384.484,97,57,false -cdb-aligned-devframe-parallel,devframe,locator,browser.snapshot,605.616,18,461,false -cdb-aligned-devframe-parallel,devframe,locator,browser.batch,1110.596,261,610,false -cdb-aligned-devframe-parallel,devframe,locator,browser.fill,254.772,118,56,false -cdb-aligned-devframe-parallel,devframe,locator,browser.click,321.904,97,57,false -cdb-aligned-devframe-parallel,devframe,locator,browser.snapshot,607.333,18,461,false -cdb-aligned-devframe-parallel,devframe,locator,browser.batch,1110.273,261,610,false -cdb-aligned-devframe-parallel,devframe,locator,browser.fill,355.504,118,56,false -cdb-aligned-devframe-parallel,devframe,locator,browser.click,345.351,97,57,false -cdb-aligned-devframe-parallel,devframe,locator,browser.snapshot,650.820,18,461,false -cdb-aligned-devframe-parallel,devframe,locator,browser.batch,1250.533,261,610,false -cdb-aligned-devframe-parallel,devframe,locator,browser.fill,349.912,118,56,false -cdb-aligned-devframe-parallel,devframe,locator,browser.click,332.918,97,57,false -cdb-aligned-devframe-parallel,devframe,locator,browser.snapshot,615.227,18,461,false -cdb-aligned-devframe-parallel,devframe,locator,browser.batch,1147.195,261,610,false -cdb-aligned-devframe-parallel,devframe,locator,browser.fill,294.970,118,56,false -cdb-aligned-devframe-parallel,devframe,locator,browser.click,328.172,97,57,false -cdb-aligned-devframe-parallel,devframe,locator,browser.snapshot,590.474,18,461,false -cdb-aligned-devframe-parallel,devframe,locator,browser.batch,1163.189,261,610,false -cdb-aligned-devframe-parallel,devframe,locator,browser.fill,298.989,118,56,false -cdb-aligned-devframe-parallel,devframe,locator,browser.click,303.981,97,57,false -cdb-aligned-devframe-parallel,devframe,locator,browser.snapshot,609.896,18,461,false -cdb-aligned-devframe-parallel,devframe,locator,browser.batch,1139.502,261,610,false -cdb-aligned-devframe-parallel,devframe,locator,browser.fill,262.027,118,56,false -cdb-aligned-devframe-parallel,devframe,locator,browser.click,273.683,97,57,false -cdb-aligned-devframe-parallel,devframe,locator,browser.snapshot,738.023,18,461,false -cdb-aligned-devframe-parallel,devframe,locator,browser.batch,1194.548,261,610,false -cdb-aligned-devframe-parallel,devframe,locator,browser.fill,282.162,118,56,false -cdb-aligned-devframe-parallel,devframe,locator,browser.click,372.218,97,57,false -cdb-aligned-devframe-parallel,devframe,locator,browser.snapshot,743.125,18,461,false -cdb-aligned-devframe-parallel,devframe,locator,browser.batch,1151.916,261,610,false -cdb-aligned-devframe-parallel,devframe,locator,browser.fill,342.405,118,56,false -cdb-aligned-devframe-parallel,devframe,locator,browser.click,264.983,97,57,false -cdb-aligned-devframe-parallel,devframe,locator,browser.snapshot,654.609,18,461,false -cdb-aligned-devframe-parallel,devframe,locator,browser.batch,1128.951,261,610,false -cdb-aligned-devframe-parallel,devframe,locator,browser.fill,342.101,118,56,false -cdb-aligned-devframe-parallel,devframe,locator,browser.click,313.383,97,57,false -cdb-aligned-devframe-parallel,devframe,locator,browser.snapshot,634.689,18,461,false -cdb-aligned-devframe-parallel,devframe,locator,browser.batch,1043.351,261,610,false -cdb-aligned-devframe-parallel,devframe,locator,browser.fill,336.614,118,56,false -cdb-aligned-devframe-parallel,devframe,locator,browser.click,337.122,97,57,false -cdb-aligned-devframe-parallel,devframe,locator,browser.snapshot,611.129,18,461,false -cdb-aligned-devframe-parallel,devframe,locator,browser.batch,1087.987,261,610,false -cdb-aligned-devframe-parallel,devframe,locator,browser.fill,359.423,118,56,false -cdb-aligned-devframe-parallel,devframe,locator,browser.click,360.064,97,57,false -cdb-aligned-devframe-parallel,devframe,locator,browser.snapshot,657.165,18,461,false -cdb-aligned-devframe-parallel,devframe,locator,browser.batch,1268.609,261,610,false -cdb-aligned-devframe-parallel,devframe,locator,browser.fill,313.408,118,56,false -cdb-aligned-devframe-parallel,devframe,locator,browser.click,290.847,97,57,false -cdb-aligned-devframe-parallel,devframe,locator,browser.snapshot,593.930,18,461,false -cdb-aligned-devframe-parallel,devframe,locator,browser.batch,1147.832,261,610,false -cdb-aligned-devframe-parallel,devframe,locator,browser.fill,307.000,118,56,false -cdb-aligned-devframe-parallel,devframe,locator,browser.click,327.856,97,57,false -cdb-aligned-devframe-parallel,devframe,locator,browser.snapshot,650.090,18,461,false -cdb-aligned-devframe-parallel,devframe,locator,browser.batch,1123.516,261,610,false -cdb-aligned-devframe-parallel,devframe,locator,browser.fill,368.030,118,56,false -cdb-aligned-devframe-parallel,devframe,locator,browser.click,293.225,97,57,false -cdb-aligned-devframe-parallel,devframe,locator,browser.snapshot,589.074,18,461,false -cdb-aligned-devframe-parallel,devframe,locator,browser.batch,1170.615,261,610,false -cdb-aligned-devframe-parallel,devframe,locator,browser.fill,281.768,118,56,false -cdb-aligned-devframe-parallel,devframe,locator,browser.click,338.813,97,57,false -cdb-aligned-devframe-parallel,devframe,locator,browser.snapshot,604.913,18,461,false -cdb-aligned-devframe-parallel,devframe,locator,browser.batch,1129.521,261,610,false -cdb-aligned-devframe-parallel,devframe,locator,browser.fill,341.171,118,56,false -cdb-aligned-devframe-parallel,devframe,locator,browser.click,344.287,97,57,false -cdb-aligned-devframe-parallel,devframe,locator,browser.snapshot,614.663,18,461,false -cdb-aligned-devframe-parallel,devframe,locator,browser.batch,1111.778,261,610,false -cdb-aligned-devframe-parallel,devframe,locator,browser.fill,344.815,118,56,false -cdb-aligned-devframe-parallel,devframe,locator,browser.click,313.572,97,57,false -cdb-aligned-devframe-parallel,devframe,locator,browser.snapshot,597.834,18,461,false -cdb-aligned-devframe-parallel,devframe,locator,browser.batch,1171.382,261,610,false -cdb-aligned-devframe-parallel,devframe,locator,browser.fill,302.477,118,56,false -cdb-aligned-devframe-parallel,devframe,locator,browser.click,349.445,97,57,false -cdb-aligned-devframe-parallel,devframe,locator,browser.snapshot,929.356,18,461,false -cdb-aligned-devframe-parallel,devframe,locator,browser.batch,1134.051,261,610,false -cdb-aligned-devframe-parallel,devframe,locator,browser.fill,275.040,118,56,false -cdb-aligned-devframe-parallel,devframe,locator,browser.click,253.006,97,57,false -cdb-aligned-devframe-parallel,devframe,locator,browser.snapshot,636.430,18,461,false -cdb-aligned-devframe-parallel,devframe,locator,browser.batch,1253.700,261,610,false -cdb-aligned-devframe-parallel,devframe,locator,browser.fill,324.376,118,56,false -cdb-aligned-devframe-parallel,devframe,locator,browser.click,404.584,97,57,false -cdb-aligned-devframe-parallel,devframe,locator,browser.snapshot,624.552,18,461,false -cdb-aligned-devframe-parallel,devframe,locator,browser.batch,1120.869,261,610,false -cdb-aligned-devframe-parallel,devframe,locator,browser.fill,322.219,118,56,false -cdb-aligned-devframe-parallel,devframe,locator,browser.click,325.481,97,57,false -cdb-aligned-devframe-parallel,devframe,locator,browser.snapshot,651.269,18,461,false -cdb-aligned-devframe-parallel,devframe,locator,browser.batch,1169.016,261,610,false -cdb-aligned-devframe-parallel,devframe,locator,browser.fill,304.001,118,56,false -cdb-aligned-devframe-parallel,devframe,locator,browser.click,311.858,97,57,false -cdb-aligned-devframe-parallel,devframe,locator,browser.snapshot,628.319,18,461,false -cdb-aligned-devframe-parallel,devframe,locator,browser.batch,1229.689,261,610,false -cdb-aligned-devframe-parallel,devframe,locator,browser.fill,359.483,118,56,false -cdb-aligned-devframe-parallel,devframe,locator,browser.click,323.923,97,57,false -cdb-aligned-devframe-parallel,devframe,locator,browser.snapshot,634.239,18,461,false -cdb-aligned-devframe-parallel,devframe,locator,browser.batch,1200.666,261,610,false -cdb-aligned-devframe-parallel,devframe,locator,browser.fill,313.036,118,56,false -cdb-aligned-devframe-parallel,devframe,locator,browser.click,342.929,97,57,false -cdb-aligned-devframe-parallel,devframe,locator,browser.snapshot,610.055,18,461,false -cdb-aligned-devframe-parallel,devframe,locator,browser.batch,1185.509,261,610,false -cdb-aligned-devframe-parallel,devframe,locator,browser.fill,318.649,118,56,false -cdb-aligned-devframe-parallel,devframe,locator,browser.click,297.043,97,57,false -cdb-aligned-devframe-parallel,devframe,locator,browser.snapshot,601.170,18,461,false -cdb-aligned-devframe-parallel,devframe,locator,browser.batch,1081.488,261,610,false -cdb-aligned-devframe-parallel,devframe,locator,browser.fill,266.779,118,56,false -cdb-aligned-devframe-parallel,devframe,locator,browser.click,294.073,97,57,false -cdb-aligned-devframe-parallel,devframe,locator,browser.snapshot,609.492,18,461,false -cdb-aligned-devframe-parallel,devframe,locator,browser.batch,1175.130,261,610,false -cdb-aligned-devframe-parallel,devframe,locator,browser.fill,342.626,118,56,false -cdb-aligned-devframe-parallel,devframe,locator,browser.click,343.571,97,57,false -cdb-aligned-devframe-parallel,devframe,locator,browser.snapshot,613.587,18,461,false -cdb-aligned-devframe-parallel,devframe,locator,browser.batch,1098.826,261,610,false -cdb-aligned-devframe-parallel,devframe,locator,browser.click:blocked,2182.087,97,197,true -cdb-aligned-devframe-parallel,devframe,locator,browser.click:blocked,2001.650,97,197,true -cdb-aligned-devframe-parallel,devframe,locator,browser.click:blocked,2037.990,97,197,true -cdb-aligned-devframe-parallel,devframe,locator,browser.click:blocked,2045.566,97,197,true -cdb-aligned-devframe-parallel,devframe,locator,browser.click:blocked,2007.097,97,197,true -cdb-aligned-devframe-parallel,devframe,locator,browser.click:blocked,2130.200,97,197,true -cdb-aligned-devframe-parallel,devframe,locator,browser.click:blocked,2001.966,97,197,true -cdb-aligned-devframe-parallel,devframe,locator,browser.click:blocked,2002.780,97,197,true -cdb-aligned-devframe-parallel,devframe,locator,browser.click:blocked,2002.336,97,197,true -cdb-aligned-devframe-parallel,devframe,locator,browser.click:blocked,2002.173,97,197,true -cdb-aligned-devframe-parallel,devframe,locator,browser.click:blocked,2019.244,97,197,true -cdb-aligned-devframe-parallel,devframe,locator,browser.click:blocked,2002.413,97,197,true -cdb-aligned-devframe-parallel,devframe,locator,browser.click:blocked,2002.317,97,197,true -cdb-aligned-devframe-parallel,devframe,locator,browser.click:blocked,2037.278,97,197,true -cdb-aligned-devframe-parallel,devframe,locator,browser.click:blocked,2000.562,97,197,true -cdb-aligned-devframe-parallel,devframe,locator,browser.click:blocked,2002.319,97,197,true -cdb-aligned-devframe-parallel,devframe,locator,browser.click:blocked,2003.948,97,197,true -cdb-aligned-devframe-parallel,devframe,locator,browser.click:blocked,2003.444,97,197,true -cdb-aligned-devframe-parallel,devframe,locator,browser.click:blocked,2002.363,97,197,true -cdb-aligned-devframe-parallel,devframe,locator,browser.click:blocked,2000.596,97,197,true -cdb-aligned-devframe-parallel,devframe,locator,browser.click:blocked,2001.707,97,197,true -cdb-aligned-devframe-parallel,devframe,locator,browser.click:blocked,2004.356,97,197,true -cdb-aligned-devframe-parallel,devframe,locator,browser.click:blocked,2002.351,97,197,true -cdb-aligned-devframe-parallel,devframe,locator,browser.click:blocked,2000.900,97,197,true -cdb-aligned-devframe-parallel,devframe,locator,browser.click:blocked,2000.664,97,197,true -cdb-aligned-devframe-parallel,devframe,locator,browser.click:blocked,2001.348,97,197,true -cdb-aligned-devframe-parallel,devframe,locator,browser.click:blocked,2006.980,97,197,true -cdb-aligned-devframe-parallel,devframe,locator,browser.click:blocked,2002.055,97,197,true -cdb-aligned-devframe-parallel,devframe,locator,browser.click:blocked,2001.101,97,197,true -cdb-aligned-devframe-parallel,devframe,locator,browser.click:blocked,2000.630,97,197,true -cdb-aligned-websocket-attributed,websocket,ref,browser.fill,103.258,55,56,false -cdb-aligned-websocket-attributed,websocket,ref,browser.click,94.940,30,57,false -cdb-aligned-websocket-attributed,websocket,ref,browser.snapshot,636.357,18,456,false -cdb-aligned-websocket-attributed,websocket,ref,browser.batch,865.627,131,605,false -cdb-aligned-websocket-attributed,websocket,ref,browser.fill,102.306,55,56,false -cdb-aligned-websocket-attributed,websocket,ref,browser.click,94.840,30,57,false -cdb-aligned-websocket-attributed,websocket,ref,browser.snapshot,718.189,18,456,false -cdb-aligned-websocket-attributed,websocket,ref,browser.batch,953.304,131,605,false -cdb-aligned-websocket-attributed,websocket,ref,browser.fill,112.192,55,56,false -cdb-aligned-websocket-attributed,websocket,ref,browser.click,114.723,30,57,false -cdb-aligned-websocket-attributed,websocket,ref,browser.snapshot,1028.246,18,456,false -cdb-aligned-websocket-attributed,websocket,ref,browser.batch,1202.970,131,605,false -cdb-aligned-websocket-attributed,websocket,ref,browser.fill,98.377,55,56,false -cdb-aligned-websocket-attributed,websocket,ref,browser.click,92.004,30,57,false -cdb-aligned-websocket-attributed,websocket,ref,browser.snapshot,708.976,18,456,false -cdb-aligned-websocket-attributed,websocket,ref,browser.batch,856.520,131,605,false -cdb-aligned-websocket-attributed,websocket,ref,browser.fill,138.074,55,56,false -cdb-aligned-websocket-attributed,websocket,ref,browser.click,99.120,30,57,false -cdb-aligned-websocket-attributed,websocket,ref,browser.snapshot,634.194,18,457,false -cdb-aligned-websocket-attributed,websocket,ref,browser.batch,919.388,132,610,false -cdb-aligned-websocket-attributed,websocket,ref,browser.fill,115.756,56,56,false -cdb-aligned-websocket-attributed,websocket,ref,browser.click,91.099,31,57,false -cdb-aligned-websocket-attributed,websocket,ref,browser.snapshot,662.508,18,461,false -cdb-aligned-websocket-attributed,websocket,ref,browser.batch,826.678,133,610,false -cdb-aligned-websocket-attributed,websocket,ref,browser.fill,114.837,56,56,false -cdb-aligned-websocket-attributed,websocket,ref,browser.click,99.950,31,57,false -cdb-aligned-websocket-attributed,websocket,ref,browser.snapshot,627.123,18,461,false -cdb-aligned-websocket-attributed,websocket,ref,browser.batch,801.874,133,610,false -cdb-aligned-websocket-attributed,websocket,ref,browser.fill,103.737,56,56,false -cdb-aligned-websocket-attributed,websocket,ref,browser.click,96.900,31,57,false -cdb-aligned-websocket-attributed,websocket,ref,browser.snapshot,2427.297,18,461,false -cdb-aligned-websocket-attributed,websocket,ref,browser.batch,1237.249,133,610,false -cdb-aligned-websocket-attributed,websocket,ref,browser.fill,122.049,56,56,false -cdb-aligned-websocket-attributed,websocket,ref,browser.click,101.877,31,57,false -cdb-aligned-websocket-attributed,websocket,ref,browser.snapshot,823.780,18,461,false -cdb-aligned-websocket-attributed,websocket,ref,browser.batch,877.168,133,610,false -cdb-aligned-websocket-attributed,websocket,ref,browser.fill,112.004,56,56,false -cdb-aligned-websocket-attributed,websocket,ref,browser.click,93.773,31,57,false -cdb-aligned-websocket-attributed,websocket,ref,browser.snapshot,624.199,18,461,false -cdb-aligned-websocket-attributed,websocket,ref,browser.batch,811.891,133,610,false -cdb-aligned-websocket-attributed,websocket,ref,browser.fill,111.934,56,56,false -cdb-aligned-websocket-attributed,websocket,ref,browser.click,101.350,31,57,false -cdb-aligned-websocket-attributed,websocket,ref,browser.snapshot,654.585,18,461,false -cdb-aligned-websocket-attributed,websocket,ref,browser.batch,804.201,133,610,false -cdb-aligned-websocket-attributed,websocket,ref,browser.fill,100.958,56,56,false -cdb-aligned-websocket-attributed,websocket,ref,browser.click,94.201,31,57,false -cdb-aligned-websocket-attributed,websocket,ref,browser.snapshot,879.862,18,461,false -cdb-aligned-websocket-attributed,websocket,ref,browser.batch,827.983,133,610,false -cdb-aligned-websocket-attributed,websocket,ref,browser.fill,99.623,56,56,false -cdb-aligned-websocket-attributed,websocket,ref,browser.click,93.493,31,57,false -cdb-aligned-websocket-attributed,websocket,ref,browser.snapshot,673.934,18,461,false -cdb-aligned-websocket-attributed,websocket,ref,browser.batch,838.780,133,610,false -cdb-aligned-websocket-attributed,websocket,ref,browser.fill,105.428,56,56,false -cdb-aligned-websocket-attributed,websocket,ref,browser.click,98.773,31,57,false -cdb-aligned-websocket-attributed,websocket,ref,browser.snapshot,710.341,18,461,false -cdb-aligned-websocket-attributed,websocket,ref,browser.batch,835.751,133,610,false -cdb-aligned-websocket-attributed,websocket,ref,browser.fill,156.116,56,56,false -cdb-aligned-websocket-attributed,websocket,ref,browser.click,107.675,31,57,false -cdb-aligned-websocket-attributed,websocket,ref,browser.snapshot,650.444,18,461,false -cdb-aligned-websocket-attributed,websocket,ref,browser.batch,832.707,133,610,false -cdb-aligned-websocket-attributed,websocket,ref,browser.fill,105.839,56,56,false -cdb-aligned-websocket-attributed,websocket,ref,browser.click,95.926,31,57,false -cdb-aligned-websocket-attributed,websocket,ref,browser.snapshot,741.816,18,461,false -cdb-aligned-websocket-attributed,websocket,ref,browser.batch,1077.123,133,610,false -cdb-aligned-websocket-attributed,websocket,ref,browser.fill,151.081,56,56,false -cdb-aligned-websocket-attributed,websocket,ref,browser.click,266.554,31,57,false -cdb-aligned-websocket-attributed,websocket,ref,browser.snapshot,665.825,18,461,false -cdb-aligned-websocket-attributed,websocket,ref,browser.batch,826.429,133,610,false -cdb-aligned-websocket-attributed,websocket,ref,browser.fill,102.426,56,56,false -cdb-aligned-websocket-attributed,websocket,ref,browser.click,96.088,31,57,false -cdb-aligned-websocket-attributed,websocket,ref,browser.snapshot,622.195,18,461,false -cdb-aligned-websocket-attributed,websocket,ref,browser.batch,791.579,133,610,false -cdb-aligned-websocket-attributed,websocket,ref,browser.fill,103.570,56,56,false -cdb-aligned-websocket-attributed,websocket,ref,browser.click,97.441,31,57,false -cdb-aligned-websocket-attributed,websocket,ref,browser.snapshot,665.735,18,461,false -cdb-aligned-websocket-attributed,websocket,ref,browser.batch,786.593,133,610,false -cdb-aligned-websocket-attributed,websocket,ref,browser.fill,104.148,56,56,false -cdb-aligned-websocket-attributed,websocket,ref,browser.click,97.323,31,57,false -cdb-aligned-websocket-attributed,websocket,ref,browser.snapshot,647.717,18,461,false -cdb-aligned-websocket-attributed,websocket,ref,browser.batch,865.300,133,610,false -cdb-aligned-websocket-attributed,websocket,ref,browser.fill,105.431,56,56,false -cdb-aligned-websocket-attributed,websocket,ref,browser.click,96.118,31,57,false -cdb-aligned-websocket-attributed,websocket,ref,browser.snapshot,616.847,18,461,false -cdb-aligned-websocket-attributed,websocket,ref,browser.batch,829.604,133,610,false -cdb-aligned-websocket-attributed,websocket,ref,browser.fill,103.847,56,56,false -cdb-aligned-websocket-attributed,websocket,ref,browser.click,94.106,31,57,false -cdb-aligned-websocket-attributed,websocket,ref,browser.snapshot,603.722,18,461,false -cdb-aligned-websocket-attributed,websocket,ref,browser.batch,848.320,133,610,false -cdb-aligned-websocket-attributed,websocket,ref,browser.fill,109.601,56,56,false -cdb-aligned-websocket-attributed,websocket,ref,browser.click,118.011,31,57,false -cdb-aligned-websocket-attributed,websocket,ref,browser.snapshot,668.020,18,461,false -cdb-aligned-websocket-attributed,websocket,ref,browser.batch,875.843,133,610,false -cdb-aligned-websocket-attributed,websocket,ref,browser.fill,100.785,56,56,false -cdb-aligned-websocket-attributed,websocket,ref,browser.click,96.035,31,57,false -cdb-aligned-websocket-attributed,websocket,ref,browser.snapshot,647.468,18,461,false -cdb-aligned-websocket-attributed,websocket,ref,browser.batch,822.678,133,610,false -cdb-aligned-websocket-attributed,websocket,ref,browser.fill,109.784,56,56,false -cdb-aligned-websocket-attributed,websocket,ref,browser.click,104.624,31,57,false -cdb-aligned-websocket-attributed,websocket,ref,browser.snapshot,953.441,18,461,false -cdb-aligned-websocket-attributed,websocket,ref,browser.batch,1258.872,133,610,false -cdb-aligned-websocket-attributed,websocket,ref,browser.fill,220.714,56,56,false -cdb-aligned-websocket-attributed,websocket,ref,browser.click,130.249,31,57,false -cdb-aligned-websocket-attributed,websocket,ref,browser.snapshot,732.507,18,461,false -cdb-aligned-websocket-attributed,websocket,ref,browser.batch,857.794,133,610,false -cdb-aligned-websocket-attributed,websocket,ref,browser.fill,97.840,56,56,false -cdb-aligned-websocket-attributed,websocket,ref,browser.click,94.655,31,57,false -cdb-aligned-websocket-attributed,websocket,ref,browser.snapshot,610.141,18,461,false -cdb-aligned-websocket-attributed,websocket,ref,browser.batch,771.345,133,610,false -cdb-aligned-websocket-attributed,websocket,ref,browser.fill,100.159,56,56,false -cdb-aligned-websocket-attributed,websocket,ref,browser.click,94.854,31,57,false -cdb-aligned-websocket-attributed,websocket,ref,browser.snapshot,612.751,18,461,false -cdb-aligned-websocket-attributed,websocket,ref,browser.batch,801.646,133,610,false -cdb-aligned-websocket-attributed,websocket,ref,browser.fill,108.043,56,56,false -cdb-aligned-websocket-attributed,websocket,ref,browser.click,96.888,31,57,false -cdb-aligned-websocket-attributed,websocket,ref,browser.snapshot,619.244,18,461,false -cdb-aligned-websocket-attributed,websocket,ref,browser.batch,800.156,133,610,false -cdb-aligned-websocket-attributed,websocket,ref,browser.fill,101.590,56,56,false -cdb-aligned-websocket-attributed,websocket,ref,browser.click,93.134,31,57,false -cdb-aligned-websocket-attributed,websocket,ref,browser.snapshot,631.900,18,461,false -cdb-aligned-websocket-attributed,websocket,ref,browser.batch,810.773,133,610,false -cdb-aligned-websocket-attributed,websocket,ref,browser.click:blocked,2032.906,31,172,true -cdb-aligned-websocket-attributed,websocket,ref,browser.click:blocked,2000.576,31,172,true -cdb-aligned-websocket-attributed,websocket,ref,browser.click:blocked,2005.371,31,172,true -cdb-aligned-websocket-attributed,websocket,ref,browser.click:blocked,2046.032,31,172,true -cdb-aligned-websocket-attributed,websocket,ref,browser.click:blocked,2035.559,31,172,true -cdb-aligned-websocket-attributed,websocket,ref,browser.click:blocked,2001.917,31,172,true -cdb-aligned-websocket-attributed,websocket,ref,browser.click:blocked,2000.423,31,172,true -cdb-aligned-websocket-attributed,websocket,ref,browser.click:blocked,2002.062,31,172,true -cdb-aligned-websocket-attributed,websocket,ref,browser.click:blocked,2001.126,31,172,true -cdb-aligned-websocket-attributed,websocket,ref,browser.click:blocked,2021.679,31,172,true -cdb-aligned-websocket-attributed,websocket,ref,browser.click:blocked,2001.973,31,172,true -cdb-aligned-websocket-attributed,websocket,ref,browser.click:blocked,2000.557,31,172,true -cdb-aligned-websocket-attributed,websocket,ref,browser.click:blocked,2002.112,31,172,true -cdb-aligned-websocket-attributed,websocket,ref,browser.click:blocked,2002.362,31,172,true -cdb-aligned-websocket-attributed,websocket,ref,browser.click:blocked,2002.473,31,172,true -cdb-aligned-websocket-attributed,websocket,ref,browser.click:blocked,2001.013,31,172,true -cdb-aligned-websocket-attributed,websocket,ref,browser.click:blocked,2002.183,31,172,true -cdb-aligned-websocket-attributed,websocket,ref,browser.click:blocked,2001.452,31,172,true -cdb-aligned-websocket-attributed,websocket,ref,browser.click:blocked,2002.190,31,172,true -cdb-aligned-websocket-attributed,websocket,ref,browser.click:blocked,2003.883,31,172,true -cdb-aligned-websocket-attributed,websocket,ref,browser.click:blocked,2002.507,31,172,true -cdb-aligned-websocket-attributed,websocket,ref,browser.click:blocked,2003.331,31,172,true -cdb-aligned-websocket-attributed,websocket,ref,browser.click:blocked,2000.338,31,172,true -cdb-aligned-websocket-attributed,websocket,ref,browser.click:blocked,1999.777,31,172,true -cdb-aligned-websocket-attributed,websocket,ref,browser.click:blocked,2001.484,31,172,true -cdb-aligned-websocket-attributed,websocket,ref,browser.click:blocked,2519.523,31,172,true -cdb-aligned-websocket-attributed,websocket,ref,browser.click:blocked,2017.995,31,172,true -cdb-aligned-websocket-attributed,websocket,ref,browser.click:blocked,2002.014,31,172,true -cdb-aligned-websocket-attributed,websocket,ref,browser.click:blocked,2001.229,31,172,true -cdb-aligned-websocket-attributed,websocket,ref,browser.click:blocked,2001.118,31,172,true -cdb-aligned-websocket-attributed,websocket,locator,browser.fill,212.468,118,56,false -cdb-aligned-websocket-attributed,websocket,locator,browser.click,180.729,97,57,false -cdb-aligned-websocket-attributed,websocket,locator,browser.snapshot,635.706,18,461,false -cdb-aligned-websocket-attributed,websocket,locator,browser.batch,910.591,261,610,false -cdb-aligned-websocket-attributed,websocket,locator,browser.fill,184.127,118,56,false -cdb-aligned-websocket-attributed,websocket,locator,browser.click,185.058,97,57,false -cdb-aligned-websocket-attributed,websocket,locator,browser.snapshot,639.311,18,461,false -cdb-aligned-websocket-attributed,websocket,locator,browser.batch,1145.180,261,610,false -cdb-aligned-websocket-attributed,websocket,locator,browser.fill,167.843,118,56,false -cdb-aligned-websocket-attributed,websocket,locator,browser.click,163.053,97,57,false -cdb-aligned-websocket-attributed,websocket,locator,browser.snapshot,667.861,18,461,false -cdb-aligned-websocket-attributed,websocket,locator,browser.batch,2027.368,261,610,false -cdb-aligned-websocket-attributed,websocket,locator,browser.fill,307.883,118,56,false -cdb-aligned-websocket-attributed,websocket,locator,browser.click,293.147,97,57,false -cdb-aligned-websocket-attributed,websocket,locator,browser.snapshot,880.519,18,461,false -cdb-aligned-websocket-attributed,websocket,locator,browser.batch,1522.202,261,610,false -cdb-aligned-websocket-attributed,websocket,locator,browser.fill,353.454,118,56,false -cdb-aligned-websocket-attributed,websocket,locator,browser.click,239.123,97,57,false -cdb-aligned-websocket-attributed,websocket,locator,browser.snapshot,976.090,18,461,false -cdb-aligned-websocket-attributed,websocket,locator,browser.batch,953.504,261,610,false -cdb-aligned-websocket-attributed,websocket,locator,browser.fill,361.063,118,56,false -cdb-aligned-websocket-attributed,websocket,locator,browser.click,248.461,97,57,false -cdb-aligned-websocket-attributed,websocket,locator,browser.snapshot,761.562,18,461,false -cdb-aligned-websocket-attributed,websocket,locator,browser.batch,1207.103,261,610,false -cdb-aligned-websocket-attributed,websocket,locator,browser.fill,347.164,118,56,false -cdb-aligned-websocket-attributed,websocket,locator,browser.click,181.381,97,57,false -cdb-aligned-websocket-attributed,websocket,locator,browser.snapshot,838.934,18,461,false -cdb-aligned-websocket-attributed,websocket,locator,browser.batch,1011.430,261,610,false -cdb-aligned-websocket-attributed,websocket,locator,browser.fill,183.826,118,56,false -cdb-aligned-websocket-attributed,websocket,locator,browser.click,180.173,97,57,false -cdb-aligned-websocket-attributed,websocket,locator,browser.snapshot,643.552,18,461,false -cdb-aligned-websocket-attributed,websocket,locator,browser.batch,952.165,261,610,false -cdb-aligned-websocket-attributed,websocket,locator,browser.fill,194.353,118,56,false -cdb-aligned-websocket-attributed,websocket,locator,browser.click,165.087,97,57,false -cdb-aligned-websocket-attributed,websocket,locator,browser.snapshot,689.891,18,461,false -cdb-aligned-websocket-attributed,websocket,locator,browser.batch,995.261,261,610,false -cdb-aligned-websocket-attributed,websocket,locator,browser.fill,167.704,118,56,false -cdb-aligned-websocket-attributed,websocket,locator,browser.click,155.255,97,57,false -cdb-aligned-websocket-attributed,websocket,locator,browser.snapshot,591.904,18,461,false -cdb-aligned-websocket-attributed,websocket,locator,browser.batch,823.274,261,610,false -cdb-aligned-websocket-attributed,websocket,locator,browser.fill,170.573,118,56,false -cdb-aligned-websocket-attributed,websocket,locator,browser.click,161.509,97,57,false -cdb-aligned-websocket-attributed,websocket,locator,browser.snapshot,580.790,18,461,false -cdb-aligned-websocket-attributed,websocket,locator,browser.batch,834.140,261,610,false -cdb-aligned-websocket-attributed,websocket,locator,browser.fill,157.249,118,56,false -cdb-aligned-websocket-attributed,websocket,locator,browser.click,162.396,97,57,false -cdb-aligned-websocket-attributed,websocket,locator,browser.snapshot,623.441,18,461,false -cdb-aligned-websocket-attributed,websocket,locator,browser.batch,966.743,261,610,false -cdb-aligned-websocket-attributed,websocket,locator,browser.fill,244.014,118,56,false -cdb-aligned-websocket-attributed,websocket,locator,browser.click,245.461,97,57,false -cdb-aligned-websocket-attributed,websocket,locator,browser.snapshot,865.012,18,461,false -cdb-aligned-websocket-attributed,websocket,locator,browser.batch,1011.413,261,610,false -cdb-aligned-websocket-attributed,websocket,locator,browser.fill,154.837,118,56,false -cdb-aligned-websocket-attributed,websocket,locator,browser.click,166.189,97,57,false -cdb-aligned-websocket-attributed,websocket,locator,browser.snapshot,579.487,18,461,false -cdb-aligned-websocket-attributed,websocket,locator,browser.batch,950.288,261,610,false -cdb-aligned-websocket-attributed,websocket,locator,browser.fill,167.188,118,56,false -cdb-aligned-websocket-attributed,websocket,locator,browser.click,158.727,97,57,false -cdb-aligned-websocket-attributed,websocket,locator,browser.snapshot,652.158,18,461,false -cdb-aligned-websocket-attributed,websocket,locator,browser.batch,976.318,261,610,false -cdb-aligned-websocket-attributed,websocket,locator,browser.fill,228.838,118,56,false -cdb-aligned-websocket-attributed,websocket,locator,browser.click,174.955,97,57,false -cdb-aligned-websocket-attributed,websocket,locator,browser.snapshot,655.447,18,461,false -cdb-aligned-websocket-attributed,websocket,locator,browser.batch,985.387,261,610,false -cdb-aligned-websocket-attributed,websocket,locator,browser.fill,180.837,118,56,false -cdb-aligned-websocket-attributed,websocket,locator,browser.click,191.460,97,57,false -cdb-aligned-websocket-attributed,websocket,locator,browser.snapshot,800.734,18,461,false -cdb-aligned-websocket-attributed,websocket,locator,browser.batch,1046.955,261,610,false -cdb-aligned-websocket-attributed,websocket,locator,browser.fill,173.226,118,56,false -cdb-aligned-websocket-attributed,websocket,locator,browser.click,169.411,97,57,false -cdb-aligned-websocket-attributed,websocket,locator,browser.snapshot,796.289,18,461,false -cdb-aligned-websocket-attributed,websocket,locator,browser.batch,1000.694,261,610,false -cdb-aligned-websocket-attributed,websocket,locator,browser.fill,235.809,118,56,false -cdb-aligned-websocket-attributed,websocket,locator,browser.click,1076.385,97,57,false -cdb-aligned-websocket-attributed,websocket,locator,browser.snapshot,2238.115,18,461,false -cdb-aligned-websocket-attributed,websocket,locator,browser.batch,1145.318,261,610,false -cdb-aligned-websocket-attributed,websocket,locator,browser.fill,194.956,118,56,false -cdb-aligned-websocket-attributed,websocket,locator,browser.click,196.893,97,57,false -cdb-aligned-websocket-attributed,websocket,locator,browser.snapshot,883.455,18,461,false -cdb-aligned-websocket-attributed,websocket,locator,browser.batch,952.873,261,610,false -cdb-aligned-websocket-attributed,websocket,locator,browser.fill,167.168,118,56,false -cdb-aligned-websocket-attributed,websocket,locator,browser.click,165.311,97,57,false -cdb-aligned-websocket-attributed,websocket,locator,browser.snapshot,570.640,18,461,false -cdb-aligned-websocket-attributed,websocket,locator,browser.batch,1144.160,261,610,false -cdb-aligned-websocket-attributed,websocket,locator,browser.fill,178.802,118,56,false -cdb-aligned-websocket-attributed,websocket,locator,browser.click,163.722,97,57,false -cdb-aligned-websocket-attributed,websocket,locator,browser.snapshot,674.131,18,461,false -cdb-aligned-websocket-attributed,websocket,locator,browser.batch,971.938,261,610,false -cdb-aligned-websocket-attributed,websocket,locator,browser.fill,207.860,118,56,false -cdb-aligned-websocket-attributed,websocket,locator,browser.click,186.514,97,57,false -cdb-aligned-websocket-attributed,websocket,locator,browser.snapshot,3866.946,18,461,false -cdb-aligned-websocket-attributed,websocket,locator,browser.batch,1162.962,261,610,false -cdb-aligned-websocket-attributed,websocket,locator,browser.fill,178.180,118,56,false -cdb-aligned-websocket-attributed,websocket,locator,browser.click,158.624,97,57,false -cdb-aligned-websocket-attributed,websocket,locator,browser.snapshot,647.610,18,461,false -cdb-aligned-websocket-attributed,websocket,locator,browser.batch,1121.507,261,610,false -cdb-aligned-websocket-attributed,websocket,locator,browser.fill,189.461,118,56,false -cdb-aligned-websocket-attributed,websocket,locator,browser.click,170.357,97,57,false -cdb-aligned-websocket-attributed,websocket,locator,browser.snapshot,634.346,18,461,false -cdb-aligned-websocket-attributed,websocket,locator,browser.batch,873.718,261,610,false -cdb-aligned-websocket-attributed,websocket,locator,browser.fill,204.656,118,56,false -cdb-aligned-websocket-attributed,websocket,locator,browser.click,179.513,97,57,false -cdb-aligned-websocket-attributed,websocket,locator,browser.snapshot,766.758,18,461,false -cdb-aligned-websocket-attributed,websocket,locator,browser.batch,953.841,261,610,false -cdb-aligned-websocket-attributed,websocket,locator,browser.fill,180.443,118,56,false -cdb-aligned-websocket-attributed,websocket,locator,browser.click,179.044,97,57,false -cdb-aligned-websocket-attributed,websocket,locator,browser.snapshot,771.431,18,461,false -cdb-aligned-websocket-attributed,websocket,locator,browser.batch,998.275,261,610,false -cdb-aligned-websocket-attributed,websocket,locator,browser.fill,229.101,118,56,false -cdb-aligned-websocket-attributed,websocket,locator,browser.click,192.172,97,57,false -cdb-aligned-websocket-attributed,websocket,locator,browser.snapshot,1026.040,18,461,false -cdb-aligned-websocket-attributed,websocket,locator,browser.batch,1541.581,261,610,false -cdb-aligned-websocket-attributed,websocket,locator,browser.fill,168.885,118,56,false -cdb-aligned-websocket-attributed,websocket,locator,browser.click,157.564,97,57,false -cdb-aligned-websocket-attributed,websocket,locator,browser.snapshot,775.105,18,461,false -cdb-aligned-websocket-attributed,websocket,locator,browser.batch,889.216,261,610,false -cdb-aligned-websocket-attributed,websocket,locator,browser.fill,160.774,118,56,false -cdb-aligned-websocket-attributed,websocket,locator,browser.click,149.569,97,57,false -cdb-aligned-websocket-attributed,websocket,locator,browser.snapshot,654.205,18,461,false -cdb-aligned-websocket-attributed,websocket,locator,browser.batch,1013.100,261,610,false -cdb-aligned-websocket-attributed,websocket,locator,browser.click:blocked,2001.837,97,172,true -cdb-aligned-websocket-attributed,websocket,locator,browser.click:blocked,2001.719,97,172,true -cdb-aligned-websocket-attributed,websocket,locator,browser.click:blocked,2001.144,97,172,true -cdb-aligned-websocket-attributed,websocket,locator,browser.click:blocked,2000.463,97,172,true -cdb-aligned-websocket-attributed,websocket,locator,browser.click:blocked,2001.226,97,172,true -cdb-aligned-websocket-attributed,websocket,locator,browser.click:blocked,2001.052,97,172,true -cdb-aligned-websocket-attributed,websocket,locator,browser.click:blocked,2002.434,97,172,true -cdb-aligned-websocket-attributed,websocket,locator,browser.click:blocked,2036.551,97,172,true -cdb-aligned-websocket-attributed,websocket,locator,browser.click:blocked,2573.759,97,170,true -cdb-aligned-devframe-attributed,devframe,ref,browser.fill,110.440,55,56,false -cdb-aligned-devframe-attributed,devframe,ref,browser.click,90.657,30,57,false -cdb-aligned-devframe-attributed,devframe,ref,browser.snapshot,721.808,18,456,false -cdb-aligned-devframe-attributed,devframe,ref,browser.batch,822.184,131,605,false -cdb-aligned-devframe-attributed,devframe,ref,browser.fill,93.056,55,56,false -cdb-aligned-devframe-attributed,devframe,ref,browser.click,88.470,30,57,false -cdb-aligned-devframe-attributed,devframe,ref,browser.snapshot,739.285,18,456,false -cdb-aligned-devframe-attributed,devframe,ref,browser.batch,957.950,131,605,false -cdb-aligned-devframe-attributed,devframe,ref,browser.fill,104.982,55,56,false -cdb-aligned-devframe-attributed,devframe,ref,browser.click,92.730,30,57,false -cdb-aligned-devframe-attributed,devframe,ref,browser.snapshot,2256.093,18,456,false -cdb-aligned-devframe-attributed,devframe,ref,browser.batch,1439.097,131,605,false -cdb-aligned-devframe-attributed,devframe,ref,browser.fill,280.129,55,56,false -cdb-aligned-devframe-attributed,devframe,ref,browser.click,462.282,30,57,false -cdb-aligned-devframe-attributed,devframe,ref,browser.snapshot,1209.900,18,456,false -cdb-aligned-devframe-attributed,devframe,ref,browser.batch,1009.113,131,605,false -cdb-aligned-devframe-attributed,devframe,ref,browser.fill,178.800,55,56,false -cdb-aligned-devframe-attributed,devframe,ref,browser.click,232.504,30,57,false -cdb-aligned-devframe-attributed,devframe,ref,browser.snapshot,1004.255,18,457,false -cdb-aligned-devframe-attributed,devframe,ref,browser.batch,797.820,132,610,false -cdb-aligned-devframe-attributed,devframe,ref,browser.fill,96.974,56,56,false -cdb-aligned-devframe-attributed,devframe,ref,browser.click,95.580,31,57,false -cdb-aligned-devframe-attributed,devframe,ref,browser.snapshot,670.709,18,461,false -cdb-aligned-devframe-attributed,devframe,ref,browser.batch,760.048,133,610,false -cdb-aligned-devframe-attributed,devframe,ref,browser.fill,113.023,56,56,false -cdb-aligned-devframe-attributed,devframe,ref,browser.click,131.826,31,57,false -cdb-aligned-devframe-attributed,devframe,ref,browser.snapshot,807.619,18,461,false -cdb-aligned-devframe-attributed,devframe,ref,browser.batch,801.404,133,610,false -cdb-aligned-devframe-attributed,devframe,ref,browser.fill,103.343,56,56,false -cdb-aligned-devframe-attributed,devframe,ref,browser.click,87.824,31,57,false -cdb-aligned-devframe-attributed,devframe,ref,browser.snapshot,602.885,18,461,false -cdb-aligned-devframe-attributed,devframe,ref,browser.batch,744.745,133,610,false -cdb-aligned-devframe-attributed,devframe,ref,browser.fill,94.633,56,56,false -cdb-aligned-devframe-attributed,devframe,ref,browser.click,85.401,31,57,false -cdb-aligned-devframe-attributed,devframe,ref,browser.snapshot,616.120,18,461,false -cdb-aligned-devframe-attributed,devframe,ref,browser.batch,741.163,133,610,false -cdb-aligned-devframe-attributed,devframe,ref,browser.fill,92.208,56,56,false -cdb-aligned-devframe-attributed,devframe,ref,browser.click,85.464,31,57,false -cdb-aligned-devframe-attributed,devframe,ref,browser.snapshot,581.989,18,461,false -cdb-aligned-devframe-attributed,devframe,ref,browser.batch,757.196,133,610,false -cdb-aligned-devframe-attributed,devframe,ref,browser.fill,98.010,56,56,false -cdb-aligned-devframe-attributed,devframe,ref,browser.click,87.499,31,57,false -cdb-aligned-devframe-attributed,devframe,ref,browser.snapshot,870.831,18,461,false -cdb-aligned-devframe-attributed,devframe,ref,browser.batch,787.311,133,610,false -cdb-aligned-devframe-attributed,devframe,ref,browser.fill,104.512,56,56,false -cdb-aligned-devframe-attributed,devframe,ref,browser.click,92.850,31,57,false -cdb-aligned-devframe-attributed,devframe,ref,browser.snapshot,615.687,18,461,false -cdb-aligned-devframe-attributed,devframe,ref,browser.batch,740.645,133,610,false -cdb-aligned-devframe-attributed,devframe,ref,browser.fill,91.623,56,56,false -cdb-aligned-devframe-attributed,devframe,ref,browser.click,85.994,31,57,false -cdb-aligned-devframe-attributed,devframe,ref,browser.snapshot,598.325,18,461,false -cdb-aligned-devframe-attributed,devframe,ref,browser.batch,778.416,133,610,false -cdb-aligned-devframe-attributed,devframe,ref,browser.fill,93.969,56,56,false -cdb-aligned-devframe-attributed,devframe,ref,browser.click,89.438,31,57,false -cdb-aligned-devframe-attributed,devframe,ref,browser.snapshot,669.772,18,461,false -cdb-aligned-devframe-attributed,devframe,ref,browser.batch,801.962,133,610,false -cdb-aligned-devframe-attributed,devframe,ref,browser.fill,100.117,56,56,false -cdb-aligned-devframe-attributed,devframe,ref,browser.click,89.396,31,57,false -cdb-aligned-devframe-attributed,devframe,ref,browser.snapshot,608.082,18,461,false -cdb-aligned-devframe-attributed,devframe,ref,browser.batch,760.556,133,610,false -cdb-aligned-devframe-attributed,devframe,ref,browser.fill,94.479,56,56,false -cdb-aligned-devframe-attributed,devframe,ref,browser.click,83.616,31,57,false -cdb-aligned-devframe-attributed,devframe,ref,browser.snapshot,626.669,18,461,false -cdb-aligned-devframe-attributed,devframe,ref,browser.batch,786.056,133,610,false -cdb-aligned-devframe-attributed,devframe,ref,browser.fill,97.418,56,56,false -cdb-aligned-devframe-attributed,devframe,ref,browser.click,187.402,31,57,false -cdb-aligned-devframe-attributed,devframe,ref,browser.snapshot,1063.383,18,461,false -cdb-aligned-devframe-attributed,devframe,ref,browser.batch,983.652,133,610,false -cdb-aligned-devframe-attributed,devframe,ref,browser.fill,112.036,56,56,false -cdb-aligned-devframe-attributed,devframe,ref,browser.click,95.147,31,57,false -cdb-aligned-devframe-attributed,devframe,ref,browser.snapshot,1141.074,18,461,false -cdb-aligned-devframe-attributed,devframe,ref,browser.batch,857.076,133,610,false -cdb-aligned-devframe-attributed,devframe,ref,browser.fill,107.072,56,56,false -cdb-aligned-devframe-attributed,devframe,ref,browser.click,90.396,31,57,false -cdb-aligned-devframe-attributed,devframe,ref,browser.snapshot,673.533,18,461,false -cdb-aligned-devframe-attributed,devframe,ref,browser.batch,806.993,133,610,false -cdb-aligned-devframe-attributed,devframe,ref,browser.fill,101.375,56,56,false -cdb-aligned-devframe-attributed,devframe,ref,browser.click,90.411,31,57,false -cdb-aligned-devframe-attributed,devframe,ref,browser.snapshot,698.120,18,461,false -cdb-aligned-devframe-attributed,devframe,ref,browser.batch,795.315,133,610,false -cdb-aligned-devframe-attributed,devframe,ref,browser.fill,94.162,56,56,false -cdb-aligned-devframe-attributed,devframe,ref,browser.click,90.896,31,57,false -cdb-aligned-devframe-attributed,devframe,ref,browser.snapshot,599.906,18,461,false -cdb-aligned-devframe-attributed,devframe,ref,browser.batch,750.698,133,610,false -cdb-aligned-devframe-attributed,devframe,ref,browser.fill,902.169,56,56,false -cdb-aligned-devframe-attributed,devframe,ref,browser.click,346.727,31,57,false -cdb-aligned-devframe-attributed,devframe,ref,browser.snapshot,1639.793,18,461,false -cdb-aligned-devframe-attributed,devframe,ref,browser.batch,1337.507,133,610,false -cdb-aligned-devframe-attributed,devframe,ref,browser.fill,233.633,56,56,false -cdb-aligned-devframe-attributed,devframe,ref,browser.click,90.823,31,57,false -cdb-aligned-devframe-attributed,devframe,ref,browser.snapshot,772.451,18,461,false -cdb-aligned-devframe-attributed,devframe,ref,browser.batch,763.123,133,610,false -cdb-aligned-devframe-attributed,devframe,ref,browser.fill,98.890,56,56,false -cdb-aligned-devframe-attributed,devframe,ref,browser.click,100.829,31,57,false -cdb-aligned-devframe-attributed,devframe,ref,browser.snapshot,631.734,18,461,false -cdb-aligned-devframe-attributed,devframe,ref,browser.batch,763.105,133,610,false -cdb-aligned-devframe-attributed,devframe,ref,browser.fill,88.664,56,56,false -cdb-aligned-devframe-attributed,devframe,ref,browser.click,84.387,31,57,false -cdb-aligned-devframe-attributed,devframe,ref,browser.snapshot,652.093,18,461,false -cdb-aligned-devframe-attributed,devframe,ref,browser.batch,868.200,133,610,false -cdb-aligned-devframe-attributed,devframe,ref,browser.fill,109.213,56,56,false -cdb-aligned-devframe-attributed,devframe,ref,browser.click,100.560,31,57,false -cdb-aligned-devframe-attributed,devframe,ref,browser.snapshot,831.211,18,461,false -cdb-aligned-devframe-attributed,devframe,ref,browser.batch,912.531,133,610,false -cdb-aligned-devframe-attributed,devframe,ref,browser.fill,108.509,56,56,false -cdb-aligned-devframe-attributed,devframe,ref,browser.click,90.987,31,57,false -cdb-aligned-devframe-attributed,devframe,ref,browser.snapshot,629.139,18,461,false -cdb-aligned-devframe-attributed,devframe,ref,browser.batch,778.698,133,610,false -cdb-aligned-devframe-attributed,devframe,ref,browser.fill,94.863,56,56,false -cdb-aligned-devframe-attributed,devframe,ref,browser.click,86.156,31,57,false -cdb-aligned-devframe-attributed,devframe,ref,browser.snapshot,583.434,18,461,false -cdb-aligned-devframe-attributed,devframe,ref,browser.batch,748.927,133,610,false -cdb-aligned-devframe-attributed,devframe,ref,browser.fill,97.644,56,56,false -cdb-aligned-devframe-attributed,devframe,ref,browser.click,88.126,31,57,false -cdb-aligned-devframe-attributed,devframe,ref,browser.snapshot,662.971,18,461,false -cdb-aligned-devframe-attributed,devframe,ref,browser.batch,757.949,133,610,false -cdb-aligned-devframe-attributed,devframe,ref,browser.fill,98.878,56,56,false -cdb-aligned-devframe-attributed,devframe,ref,browser.click,85.476,31,57,false -cdb-aligned-devframe-attributed,devframe,ref,browser.snapshot,630.770,18,461,false -cdb-aligned-devframe-attributed,devframe,ref,browser.batch,847.830,133,610,false -cdb-aligned-devframe-attributed,devframe,ref,browser.click:blocked,2004.121,31,197,true -cdb-aligned-devframe-attributed,devframe,ref,browser.click:blocked,2005.765,31,197,true -cdb-aligned-devframe-attributed,devframe,ref,browser.click:blocked,2004.410,31,197,true -cdb-aligned-devframe-attributed,devframe,ref,browser.click:blocked,2001.359,31,197,true -cdb-aligned-devframe-attributed,devframe,ref,browser.click:blocked,2003.766,31,197,true -cdb-aligned-devframe-attributed,devframe,ref,browser.click:blocked,2003.704,31,197,true -cdb-aligned-devframe-attributed,devframe,ref,browser.click:blocked,2016.182,31,197,true -cdb-aligned-devframe-attributed,devframe,ref,browser.click:blocked,2041.797,31,197,true -cdb-aligned-devframe-attributed,devframe,ref,browser.click:blocked,2038.724,31,197,true -cdb-aligned-devframe-attributed,devframe,ref,browser.click:blocked,2003.229,31,197,true -cdb-aligned-devframe-attributed,devframe,ref,browser.click:blocked,2004.247,31,197,true -cdb-aligned-devframe-attributed,devframe,ref,browser.click:blocked,2002.555,31,197,true -cdb-aligned-devframe-attributed,devframe,ref,browser.click:blocked,2003.154,31,197,true -cdb-aligned-devframe-attributed,devframe,ref,browser.click:blocked,2002.456,31,197,true -cdb-aligned-devframe-attributed,devframe,ref,browser.click:blocked,2003.901,31,197,true -cdb-aligned-devframe-attributed,devframe,ref,browser.click:blocked,2004.741,31,197,true -cdb-aligned-devframe-attributed,devframe,ref,browser.click:blocked,2002.583,31,197,true -cdb-aligned-devframe-attributed,devframe,ref,browser.click:blocked,2003.995,31,197,true -cdb-aligned-devframe-attributed,devframe,ref,browser.click:blocked,2003.132,31,197,true -cdb-aligned-devframe-attributed,devframe,ref,browser.click:blocked,2003.159,31,197,true -cdb-aligned-devframe-attributed,devframe,ref,browser.click:blocked,2002.592,31,197,true -cdb-aligned-devframe-attributed,devframe,ref,browser.click:blocked,2004.591,31,197,true -cdb-aligned-devframe-attributed,devframe,ref,browser.click:blocked,2001.895,31,197,true -cdb-aligned-devframe-attributed,devframe,ref,browser.click:blocked,2003.955,31,197,true -cdb-aligned-devframe-attributed,devframe,ref,browser.click:blocked,2002.784,31,197,true -cdb-aligned-devframe-attributed,devframe,ref,browser.click:blocked,2003.850,31,197,true -cdb-aligned-devframe-attributed,devframe,ref,browser.click:blocked,2000.425,31,197,true -cdb-aligned-devframe-attributed,devframe,ref,browser.click:blocked,2008.724,31,197,true -cdb-aligned-devframe-attributed,devframe,ref,browser.click:blocked,2002.980,31,197,true -cdb-aligned-devframe-attributed,devframe,ref,browser.click:blocked,2001.551,31,197,true -cdb-aligned-devframe-attributed,devframe,locator,browser.fill,303.877,118,56,false -cdb-aligned-devframe-attributed,devframe,locator,browser.click,368.204,97,57,false -cdb-aligned-devframe-attributed,devframe,locator,browser.snapshot,629.837,18,461,false -cdb-aligned-devframe-attributed,devframe,locator,browser.batch,1239.024,261,610,false -cdb-aligned-devframe-attributed,devframe,locator,browser.fill,356.901,118,56,false -cdb-aligned-devframe-attributed,devframe,locator,browser.click,311.688,97,57,false -cdb-aligned-devframe-attributed,devframe,locator,browser.snapshot,756.532,18,461,false -cdb-aligned-devframe-attributed,devframe,locator,browser.batch,1060.857,261,610,false -cdb-aligned-devframe-attributed,devframe,locator,browser.fill,243.242,118,56,false -cdb-aligned-devframe-attributed,devframe,locator,browser.click,294.455,97,57,false -cdb-aligned-devframe-attributed,devframe,locator,browser.snapshot,665.342,18,461,false -cdb-aligned-devframe-attributed,devframe,locator,browser.batch,1177.068,261,610,false -cdb-aligned-devframe-attributed,devframe,locator,browser.fill,360.744,118,56,false -cdb-aligned-devframe-attributed,devframe,locator,browser.click,285.470,97,57,false -cdb-aligned-devframe-attributed,devframe,locator,browser.snapshot,717.336,18,461,false -cdb-aligned-devframe-attributed,devframe,locator,browser.batch,1195.321,261,610,false -cdb-aligned-devframe-attributed,devframe,locator,browser.fill,293.167,118,56,false -cdb-aligned-devframe-attributed,devframe,locator,browser.click,317.048,97,57,false -cdb-aligned-devframe-attributed,devframe,locator,browser.snapshot,593.219,18,461,false -cdb-aligned-devframe-attributed,devframe,locator,browser.batch,2643.726,261,610,false -cdb-aligned-devframe-attributed,devframe,locator,browser.fill,429.455,118,56,false -cdb-aligned-devframe-attributed,devframe,locator,browser.click,295.728,97,57,false -cdb-aligned-devframe-attributed,devframe,locator,browser.snapshot,1635.764,18,461,false -cdb-aligned-devframe-attributed,devframe,locator,browser.batch,1458.671,261,610,false -cdb-aligned-devframe-attributed,devframe,locator,browser.fill,387.471,118,56,false -cdb-aligned-devframe-attributed,devframe,locator,browser.click,323.123,97,57,false -cdb-aligned-devframe-attributed,devframe,locator,browser.snapshot,666.384,18,461,false -cdb-aligned-devframe-attributed,devframe,locator,browser.batch,1064.660,261,610,false -cdb-aligned-devframe-attributed,devframe,locator,browser.fill,270.303,118,56,false -cdb-aligned-devframe-attributed,devframe,locator,browser.click,275.276,97,57,false -cdb-aligned-devframe-attributed,devframe,locator,browser.snapshot,680.216,18,461,false -cdb-aligned-devframe-attributed,devframe,locator,browser.batch,1473.879,261,610,false -cdb-aligned-devframe-attributed,devframe,locator,browser.fill,365.888,118,56,false -cdb-aligned-devframe-attributed,devframe,locator,browser.click,319.909,97,57,false -cdb-aligned-devframe-attributed,devframe,locator,browser.snapshot,632.264,18,461,false -cdb-aligned-devframe-attributed,devframe,locator,browser.batch,1119.088,261,610,false -cdb-aligned-devframe-attributed,devframe,locator,browser.fill,383.702,118,56,false -cdb-aligned-devframe-attributed,devframe,locator,browser.click,356.810,97,57,false -cdb-aligned-devframe-attributed,devframe,locator,browser.snapshot,666.709,18,461,false -cdb-aligned-devframe-attributed,devframe,locator,browser.batch,1158.986,261,610,false -cdb-aligned-devframe-attributed,devframe,locator,browser.fill,257.458,118,56,false -cdb-aligned-devframe-attributed,devframe,locator,browser.click,359.001,97,57,false -cdb-aligned-devframe-attributed,devframe,locator,browser.snapshot,623.196,18,461,false -cdb-aligned-devframe-attributed,devframe,locator,browser.batch,1388.237,261,610,false -cdb-aligned-devframe-attributed,devframe,locator,browser.fill,359.351,118,56,false -cdb-aligned-devframe-attributed,devframe,locator,browser.click,354.814,97,57,false -cdb-aligned-devframe-attributed,devframe,locator,browser.snapshot,737.102,18,461,false -cdb-aligned-devframe-attributed,devframe,locator,browser.batch,1128.534,261,610,false -cdb-aligned-devframe-attributed,devframe,locator,browser.fill,287.749,118,56,false -cdb-aligned-devframe-attributed,devframe,locator,browser.click,345.686,97,57,false -cdb-aligned-devframe-attributed,devframe,locator,browser.snapshot,672.203,18,461,false -cdb-aligned-devframe-attributed,devframe,locator,browser.batch,1172.923,261,610,false -cdb-aligned-devframe-attributed,devframe,locator,browser.fill,254.040,118,56,false -cdb-aligned-devframe-attributed,devframe,locator,browser.click,347.707,97,57,false -cdb-aligned-devframe-attributed,devframe,locator,browser.snapshot,1248.886,18,461,false -cdb-aligned-devframe-attributed,devframe,locator,browser.batch,1467.135,261,610,false -cdb-aligned-devframe-attributed,devframe,locator,browser.fill,315.415,118,56,false -cdb-aligned-devframe-attributed,devframe,locator,browser.click,294.564,97,57,false -cdb-aligned-devframe-attributed,devframe,locator,browser.snapshot,654.075,18,461,false -cdb-aligned-devframe-attributed,devframe,locator,browser.batch,1156.689,261,610,false -cdb-aligned-devframe-attributed,devframe,locator,browser.fill,258.816,118,56,false -cdb-aligned-devframe-attributed,devframe,locator,browser.click,265.421,97,57,false -cdb-aligned-devframe-attributed,devframe,locator,browser.snapshot,803.581,18,461,false -cdb-aligned-devframe-attributed,devframe,locator,browser.batch,1122.498,261,610,false -cdb-aligned-devframe-attributed,devframe,locator,browser.fill,267.739,118,56,false -cdb-aligned-devframe-attributed,devframe,locator,browser.click,270.004,97,57,false -cdb-aligned-devframe-attributed,devframe,locator,browser.snapshot,714.137,18,461,false -cdb-aligned-devframe-attributed,devframe,locator,browser.batch,1143.344,261,610,false -cdb-aligned-devframe-attributed,devframe,locator,browser.fill,289.001,118,56,false -cdb-aligned-devframe-attributed,devframe,locator,browser.click,322.555,97,57,false -cdb-aligned-devframe-attributed,devframe,locator,browser.snapshot,586.084,18,461,false -cdb-aligned-devframe-attributed,devframe,locator,browser.batch,1291.474,261,610,false -cdb-aligned-devframe-attributed,devframe,locator,browser.fill,312.110,118,56,false -cdb-aligned-devframe-attributed,devframe,locator,browser.click,320.035,97,57,false -cdb-aligned-devframe-attributed,devframe,locator,browser.snapshot,704.532,18,461,false -cdb-aligned-devframe-attributed,devframe,locator,browser.batch,1108.865,261,610,false -cdb-aligned-devframe-attributed,devframe,locator,browser.fill,290.103,118,56,false -cdb-aligned-devframe-attributed,devframe,locator,browser.click,268.501,97,57,false -cdb-aligned-devframe-attributed,devframe,locator,browser.snapshot,615.002,18,461,false -cdb-aligned-devframe-attributed,devframe,locator,browser.batch,1198.607,261,610,false -cdb-aligned-devframe-attributed,devframe,locator,browser.fill,402.995,118,56,false -cdb-aligned-devframe-attributed,devframe,locator,browser.click,472.892,97,57,false -cdb-aligned-devframe-attributed,devframe,locator,browser.snapshot,804.879,18,461,false -cdb-aligned-devframe-attributed,devframe,locator,browser.batch,1276.565,261,610,false -cdb-aligned-devframe-attributed,devframe,locator,browser.fill,348.945,118,56,false -cdb-aligned-devframe-attributed,devframe,locator,browser.click,361.841,97,57,false -cdb-aligned-devframe-attributed,devframe,locator,browser.snapshot,676.687,18,461,false -cdb-aligned-devframe-attributed,devframe,locator,browser.batch,1130.966,261,610,false -cdb-aligned-devframe-attributed,devframe,locator,browser.fill,394.405,118,56,false -cdb-aligned-devframe-attributed,devframe,locator,browser.click,268.268,97,57,false -cdb-aligned-devframe-attributed,devframe,locator,browser.snapshot,623.011,18,461,false -cdb-aligned-devframe-attributed,devframe,locator,browser.batch,1095.799,261,610,false -cdb-aligned-devframe-attributed,devframe,locator,browser.fill,359.405,118,56,false -cdb-aligned-devframe-attributed,devframe,locator,browser.click,346.578,97,57,false -cdb-aligned-devframe-attributed,devframe,locator,browser.snapshot,614.547,18,461,false -cdb-aligned-devframe-attributed,devframe,locator,browser.batch,1160.295,261,610,false -cdb-aligned-devframe-attributed,devframe,locator,browser.fill,360.734,118,56,false -cdb-aligned-devframe-attributed,devframe,locator,browser.click,263.217,97,57,false -cdb-aligned-devframe-attributed,devframe,locator,browser.snapshot,623.370,18,461,false -cdb-aligned-devframe-attributed,devframe,locator,browser.batch,1128.175,261,610,false -cdb-aligned-devframe-attributed,devframe,locator,browser.fill,505.619,118,56,false -cdb-aligned-devframe-attributed,devframe,locator,browser.click,818.185,97,57,false -cdb-aligned-devframe-attributed,devframe,locator,browser.snapshot,2276.488,18,461,false -cdb-aligned-devframe-attributed,devframe,locator,browser.batch,4551.811,261,610,false -cdb-aligned-devframe-attributed,devframe,locator,browser.fill,370.456,118,56,false -cdb-aligned-devframe-attributed,devframe,locator,browser.click,348.321,97,57,false -cdb-aligned-devframe-attributed,devframe,locator,browser.snapshot,660.404,18,461,false -cdb-aligned-devframe-attributed,devframe,locator,browser.batch,1236.730,261,610,false -cdb-aligned-devframe-attributed,devframe,locator,browser.fill,368.570,118,56,false -cdb-aligned-devframe-attributed,devframe,locator,browser.click,369.121,97,57,false -cdb-aligned-devframe-attributed,devframe,locator,browser.snapshot,615.118,18,461,false -cdb-aligned-devframe-attributed,devframe,locator,browser.batch,1245.401,261,610,false -cdb-aligned-devframe-attributed,devframe,locator,browser.fill,272.896,118,56,false -cdb-aligned-devframe-attributed,devframe,locator,browser.click,275.137,97,57,false -cdb-aligned-devframe-attributed,devframe,locator,browser.snapshot,934.405,18,461,false -cdb-aligned-devframe-attributed,devframe,locator,browser.batch,1233.582,261,610,false -cdb-aligned-devframe-attributed,devframe,locator,browser.fill,307.179,118,56,false -cdb-aligned-devframe-attributed,devframe,locator,browser.click,319.777,97,57,false -cdb-aligned-devframe-attributed,devframe,locator,browser.snapshot,578.714,18,461,false -cdb-aligned-devframe-attributed,devframe,locator,browser.batch,1263.108,261,610,false -cdb-aligned-devframe-attributed,devframe,locator,browser.click:blocked,2000.826,97,197,true -cdb-aligned-devframe-attributed,devframe,locator,browser.click:blocked,2497.063,97,197,true -cdb-aligned-devframe-attributed,devframe,locator,browser.click:blocked,2524.500,97,197,true -cdb-aligned-devframe-attributed,devframe,locator,browser.click:blocked,2178.690,97,197,true -cdb-aligned-devframe-attributed,devframe,locator,browser.click:blocked,2008.372,97,197,true -cdb-aligned-devframe-attributed,devframe,locator,browser.click:blocked,2101.277,97,197,true -cdb-aligned-devframe-attributed,devframe,locator,browser.click:blocked,2000.696,97,197,true -cdb-aligned-devframe-attributed,devframe,locator,browser.click:blocked,3346.516,97,197,true -cdb-aligned-devframe-attributed,devframe,locator,browser.click:blocked,2068.698,97,197,true -cdb-aligned-devframe-attributed,devframe,locator,browser.click:blocked,2005.411,97,197,true -cdb-aligned-devframe-attributed,devframe,locator,browser.click:blocked,2063.155,97,197,true -cdb-aligned-devframe-attributed,devframe,locator,browser.click:blocked,2001.028,97,197,true -cdb-aligned-devframe-attributed,devframe,locator,browser.click:blocked,2005.101,97,197,true -cdb-aligned-devframe-attributed,devframe,locator,browser.click:blocked,2011.003,97,197,true -cdb-aligned-devframe-attributed,devframe,locator,browser.click:blocked,2016.366,97,197,true -cdb-aligned-devframe-attributed,devframe,locator,browser.click:blocked,2007.517,97,197,true -cdb-aligned-devframe-attributed,devframe,locator,browser.click:blocked,2125.550,97,197,true -cdb-aligned-devframe-attributed,devframe,locator,browser.click:blocked,2001.821,97,197,true -cdb-aligned-devframe-attributed,devframe,locator,browser.click:blocked,2001.916,97,197,true -cdb-aligned-devframe-attributed,devframe,locator,browser.click:blocked,2025.673,97,197,true -cdb-aligned-devframe-attributed,devframe,locator,browser.click:blocked,2002.753,97,197,true -cdb-aligned-devframe-attributed,devframe,locator,browser.click:blocked,2001.677,97,197,true -cdb-aligned-devframe-attributed,devframe,locator,browser.click:blocked,2002.447,97,197,true -cdb-aligned-devframe-attributed,devframe,locator,browser.click:blocked,2002.644,97,197,true -cdb-aligned-devframe-attributed,devframe,locator,browser.click:blocked,2057.001,97,197,true -cdb-aligned-devframe-attributed,devframe,locator,browser.click:blocked,2010.517,97,197,true -cdb-aligned-devframe-attributed,devframe,locator,browser.click:blocked,2004.027,97,197,true -cdb-aligned-devframe-attributed,devframe,locator,browser.click:blocked,2101.537,97,197,true -cdb-aligned-devframe-attributed,devframe,locator,browser.click:blocked,2032.145,97,197,true -cdb-aligned-devframe-attributed,devframe,locator,browser.click:blocked,2014.032,97,197,true diff --git a/docs/benchmarks/2026-09-09-aligned.md b/docs/benchmarks/2026-09-09-aligned.md index 3e06a36..72299e0 100644 --- a/docs/benchmarks/2026-09-09-aligned.md +++ b/docs/benchmarks/2026-09-09-aligned.md @@ -10,7 +10,7 @@ All timings are milliseconds. The action gate is p95 ≤500 ms; the snapshot gat The first aligned attempt failed during warm-up because the benchmark reused references invalidated by its own snapshot. The harness now consumes the latest observation. That setup failure is retained in the diagnostic evidence; it produced no measured samples. -[Every measured sample](./2026-09-09-aligned.csv) is retained. [Earlier failed gates and lifecycle runs](./2026-09-09-lifecycle.md) remain unchanged evidence; the new measurements do not erase them. +[Earlier failed gates and lifecycle runs](./2026-09-09-lifecycle.md) remain unchanged evidence; the new measurements do not erase them. ## Baseline @@ -66,7 +66,7 @@ The final attributed WebSocket run stopped after 279 of 300 measurements when a The final attributed Devframe run completed all 300 measurements, but both snapshot gates failed at approximately 1,640 ms. Chrome debugger binding timings included a 2,390-ms `Accessibility.getFullAXTree`, a 1,308-ms `Accessibility.disable`, a 1,034-ms `Page.enable` and a 1,034-ms `Page.getFrameTree`. These measurements establish browser-boundary delays, not their underlying cause. CDB command round trips also include transport, serialization, authority and artifact work; the remaining time is not fully attributed. -A concurrent process inspection found an unrelated desktop renderer consuming about 569% CPU and 15.8% of system memory. Subsequent desktop activity also varied. This is observed contention, not proof that it explains every delay. No process or profile changes were made. Performance validation remains incomplete; further acceptance measurements require a settled environment. +A concurrent process inspection found an unrelated desktop renderer consuming about 569% CPU and 15.8% of system memory. Subsequent desktop activity also varied. This is observed contention, not proof that it explains every delay. No process or profile changes were made. Performance validation remains incomplete; further acceptance measurements run sequentially in manual CI, with logs and a job summary. Linux runner results establish a separate baseline from these macOS measurements. Load averages are shown for 1 / 5 / 15 minutes. Builds and other test suites did not overlap measured runs. Five- and fifteen-minute load averages can still include earlier live work. @@ -111,4 +111,4 @@ Workspace patches do not propagate to consumers of published CDB packages. See [ The independent 30-cycle matrix stopped in cycle 6 after 23 successful scenarios. Navigation, reconnect and cancellation each completed six times; revocation/reapproval completed five times, then its next click timed out before input. At failure, discovery had taken 1,150 ms and actionability 917 ms, with `Page.getLayoutMetrics` pending. Load rose from 12.6 / 12.0 / 11.3 to 44.7 / 18.8 / 13.8. -[All follow-up lifecycle samples](./2026-09-09-followup-lifecycle.csv) retain the failure. The intended 120-scenario gate is not satisfied. The surrounding Chromium integration checks reached this matrix successfully, including closed shadow roots, cross-origin frames, overlays, overlapping grants and ordinary shared RPC traffic. No crash was observed; the intermittent stall cause remains unresolved. +The intended 120-scenario gate is not satisfied. The surrounding Chromium integration checks reached this matrix successfully, including closed shadow roots, cross-origin frames, overlays, overlapping grants and ordinary shared RPC traffic. No crash was observed; the intermittent stall cause remains unresolved. diff --git a/docs/benchmarks/2026-09-09-followup-lifecycle.csv b/docs/benchmarks/2026-09-09-followup-lifecycle.csv deleted file mode 100644 index 6ba75d7..0000000 --- a/docs/benchmarks/2026-09-09-followup-lifecycle.csv +++ /dev/null @@ -1,25 +0,0 @@ -cycle,scenario,milliseconds,result -1,navigation,484.227,passed -1,reconnect,592.164,passed -1,cancellation,580.741,passed -1,revocation,505.744,passed -2,navigation,584.895,passed -2,reconnect,567.728,passed -2,cancellation,446.052,passed -2,revocation,477.833,passed -3,navigation,487.889,passed -3,reconnect,560.592,passed -3,cancellation,532.009,passed -3,revocation,1028.839,passed -4,navigation,1283.496,passed -4,reconnect,1224.433,passed -4,cancellation,1086.099,passed -4,revocation,1209.525,passed -5,navigation,1905.258,passed -5,reconnect,847.273,passed -5,cancellation,1321.633,passed -5,revocation,1682.765,passed -6,navigation,1135.055,passed -6,reconnect,910.539,passed -6,cancellation,1756.493,passed -6,revocation,4219.532,failed diff --git a/docs/benchmarks/2026-09-09-latency.csv b/docs/benchmarks/2026-09-09-latency.csv deleted file mode 100644 index d5621d9..0000000 --- a/docs/benchmarks/2026-09-09-latency.csv +++ /dev/null @@ -1,241 +0,0 @@ -transport,sample,scenario,milliseconds,error -devframe,0,browser.fill,372.2828749999999, -devframe,1,browser.click,295.201583, -devframe,2,browser.snapshot,885.8209159999997, -devframe,3,browser.batch,1230.2917500000003, -devframe,4,browser.fill,372.884250000001, -devframe,5,browser.click,307.55574999999953, -devframe,6,browser.snapshot,749.7271669999991, -devframe,7,browser.batch,1150.9781660000008, -devframe,8,browser.fill,335.2848749999994, -devframe,9,browser.click,328.7793329999986, -devframe,10,browser.snapshot,726.2948749999996, -devframe,11,browser.batch,1396.0505840000005, -devframe,12,browser.fill,975.4526249999999, -devframe,13,browser.click,418.50783299999966, -devframe,14,browser.snapshot,925.5630830000009, -devframe,15,browser.batch,1475.1597909999982, -devframe,16,browser.fill,367.60662500000035, -devframe,17,browser.click,349.8762080000015, -devframe,18,browser.snapshot,725.6705000000002, -devframe,19,browser.batch,1821.4395409999997, -devframe,20,browser.fill,347.7748329999995, -devframe,21,browser.click,325.8550409999989, -devframe,22,browser.snapshot,705.8833340000019, -devframe,23,browser.batch,1311.808917000002, -devframe,24,browser.fill,330.0451249999969, -devframe,25,browser.click,320.3681250000009, -devframe,26,browser.snapshot,1102.395958000001, -devframe,27,browser.batch,1104.0686249999999, -devframe,28,browser.fill,314.4805000000015, -devframe,29,browser.click,306.6389589999999, -devframe,30,browser.snapshot,654.0953330000011, -devframe,31,browser.batch,1479.8669169999994, -devframe,32,browser.fill,348.3762499999975, -devframe,33,browser.click,1130.6139169999988, -devframe,34,browser.snapshot,754.5785829999986, -devframe,35,browser.batch,1196.7371250000033, -devframe,36,browser.fill,372.015999999996, -devframe,37,browser.click,293.83283400000073, -devframe,38,browser.snapshot,624.9125420000055, -devframe,39,browser.batch,1326.222332999998, -devframe,40,browser.fill,296.06683399999747, -devframe,41,browser.click,340.12366699999984, -devframe,42,browser.snapshot,651.0039590000015, -devframe,43,browser.batch,1163.4714160000003, -devframe,44,browser.fill,296.01987499999814, -devframe,45,browser.click,494.3464160000003, -devframe,46,browser.snapshot,1852.451000000001, -devframe,47,browser.batch,1142.930290999997, -devframe,48,browser.fill,315.67149999999674, -devframe,49,browser.click,329.15550000000076, -devframe,50,browser.snapshot,829.6743750000023, -devframe,51,browser.batch,1127.732333, -devframe,52,browser.fill,327.34108299999934, -devframe,53,browser.click,351.3675409999996, -devframe,54,browser.snapshot,740.8344169999982, -devframe,55,browser.batch,1148.3505000000005, -devframe,56,browser.fill,325.45524999999907, -devframe,57,browser.click,316.7242499999993, -devframe,58,browser.snapshot,695.9982079999972, -devframe,59,browser.batch,1644.6100829999996, -devframe,60,browser.fill,273.2493749999994, -devframe,61,browser.click,299.5064169999969, -devframe,62,browser.snapshot,661.7077920000011, -devframe,63,browser.batch,1207.6912090000042, -devframe,64,browser.fill,310.24441600000137, -devframe,65,browser.click,285.47766599999886, -devframe,66,browser.snapshot,695.1679999999978, -devframe,67,browser.batch,2065.8635830000057, -devframe,68,browser.fill,332.5876250000001, -devframe,69,browser.click,330.72099999999773, -devframe,70,browser.snapshot,656.7946250000023, -devframe,71,browser.batch,1173.5899160000045, -devframe,72,browser.fill,307.02570900000137, -devframe,73,browser.click,362.74829200000386, -devframe,74,browser.snapshot,956.2126250000001, -devframe,75,browser.batch,3865.3280830000003, -devframe,76,browser.fill,1499.5701670000053, -devframe,77,browser.click,471.1782089999906, -devframe,78,browser.snapshot,2188.4195000000036, -devframe,79,browser.batch,1553.6526250000024, -devframe,80,browser.fill,341.908666000003, -devframe,81,browser.click,326.61516700001084, -devframe,82,browser.snapshot,976.8965000000026, -devframe,83,browser.batch,1538.1514999999927, -devframe,84,browser.fill,321.6036669999885, -devframe,85,browser.click,322.21016699999745, -devframe,86,browser.snapshot,1248.497000000003, -devframe,87,browser.batch,1306.725375000009, -devframe,88,browser.fill,361.6015000000043, -devframe,89,browser.click,606.4772499999963, -devframe,90,browser.snapshot,937.1247919999878, -devframe,91,browser.batch,1526.1002080000035, -devframe,92,browser.fill,300.5969580000092, -devframe,93,browser.click,317.8986670000013, -devframe,94,browser.snapshot,695.1366660000058, -devframe,95,browser.batch,1378.874874999994, -devframe,96,browser.fill,276.3425840000127, -devframe,97,browser.click,371.6782080000121, -devframe,98,browser.snapshot,2735.0215829999943, -devframe,99,browser.batch,1427.9537500000006, -devframe,100,browser.fill,296.02037500000733, -devframe,101,browser.click,291.3111249999929, -devframe,102,browser.snapshot,760.8910840000026, -devframe,103,browser.batch,2199.9709170000133, -devframe,104,browser.fill,316.20937499999127, -devframe,105,browser.click,599.6370840000018, -devframe,106,browser.snapshot,1292.0612919999985, -devframe,107,browser.batch,2219.3544589999947, -devframe,108,browser.fill,381.60958400000527, -devframe,109,browser.click,318.2050840000011, -devframe,110,browser.snapshot,649.8490829999937, -devframe,111,browser.batch,1110.6722080000036, -devframe,112,browser.fill,340.2784999999858, -devframe,113,browser.click,296.1295839999948, -devframe,114,browser.snapshot,637.4309999999969, -devframe,115,browser.batch,1215.7875420000055, -devframe,116,browser.fill,309.9054999999935, -devframe,117,browser.click,302.4016249999986, -devframe,118,browser.snapshot,668.7026249999908, -devframe,119,browser.batch,1498.0010000000038, -devframe,120,browser.click:blocked,2002.6224159999983, -devframe,121,browser.click:blocked,2006.345291000005, -devframe,122,browser.click:blocked,2001.1201250000013, -devframe,123,browser.click:blocked,2001.661540999994, -devframe,124,browser.click:blocked,2001.296000000002, -devframe,125,browser.click:blocked,2002.1808750000055, -devframe,126,browser.click:blocked,2003.1580000000104, -devframe,127,browser.click:blocked,2056.594333000001, -devframe,128,browser.click:blocked,2002.1652920000051, -devframe,129,browser.click:blocked,2000.7545419999951, -devframe,130,browser.click:blocked,2187.196292000008, -devframe,131,browser.click:blocked,2016.8204169999954, -devframe,132,browser.click:blocked,2184.8196670000034, -devframe,133,browser.click:blocked,2002.7432920000138, -devframe,134,browser.click:blocked,2003.2419579999987, -devframe,135,browser.click:blocked,2004.6015420000185, -devframe,136,browser.click:blocked,2003.3656669999764, -devframe,137,browser.click:blocked,2025.3184579999943, -devframe,138,browser.click:blocked,2283.9179170000134, -devframe,139,browser.click:blocked,2013.988000000012, -devframe,140,browser.click:blocked,2026.5623750000086, -devframe,141,browser.click:blocked,2004.4344169999822, -devframe,142,browser.click:blocked,2101.339000000007, -devframe,143,browser.click:blocked,2000.875, -devframe,144,browser.click:blocked,2002.302834000002, -devframe,145,browser.click:blocked,2002.0181250000023, -devframe,146,browser.click:blocked,2017.5597919999855, -devframe,147,browser.click:blocked,2290.174082999991, -devframe,148,browser.click:blocked,2001.0391250000102, -devframe,149,browser.click:blocked,2032.6541670000006, -websocket,0,browser.snapshot,1744.658374999999,False -websocket,1,browser.fill,347.6744579999995,False -websocket,2,browser.click,284.39687499999854,False -websocket,3,browser.snapshot,1965.347667,False -websocket,4,browser.fill,529.0651249999992,False -websocket,5,browser.click,432.63545799999883,False -websocket,6,browser.snapshot,1888.4782920000016,False -websocket,7,browser.fill,188.5514579999981,False -websocket,8,browser.click,169.6525419999998,False -websocket,9,browser.snapshot,1115.8272080000024,False -websocket,10,browser.fill,185.18408400000044,False -websocket,11,browser.click,163.2671249999985,False -websocket,12,browser.snapshot,1061.5348749999976,False -websocket,13,browser.fill,260.890707999999,False -websocket,14,browser.click,449.15708299999824,False -websocket,15,browser.snapshot,2315.700832999999,False -websocket,16,browser.fill,220.46716599999854,False -websocket,17,browser.click,151.10516700000153,False -websocket,18,browser.snapshot,926.9267499999987,False -websocket,19,browser.fill,148.13362499999857,False -websocket,20,browser.click,103.9340000000011,False -websocket,21,browser.snapshot,682.0189580000006,False -websocket,22,browser.fill,110.34120900000198,False -websocket,23,browser.click,102.79241700000057,False -websocket,24,browser.snapshot,678.1170409999977,False -websocket,25,browser.fill,107.73254199999792,False -websocket,26,browser.click,100.87183300000106,False -websocket,27,browser.snapshot,648.6391250000015,False -websocket,28,browser.fill,110.91125000000102,False -websocket,29,browser.click,102.1441670000022,False -websocket,30,browser.snapshot,715.8509159999994,False -websocket,31,browser.fill,126.86491599999863,False -websocket,32,browser.click,625.4940409999981,False -websocket,33,browser.snapshot,847.3919999999998,False -websocket,34,browser.fill,111.20866599999863,False -websocket,35,browser.click,348.1989169999979,False -websocket,36,browser.snapshot,675.336457999998,False -websocket,37,browser.fill,110.09754199999952,False -websocket,38,browser.click,101.74375000000146,False -websocket,39,browser.snapshot,698.0110000000022,False -websocket,40,browser.fill,121.95858299999963,False -websocket,41,browser.click,157.7307499999988,False -websocket,42,browser.snapshot,935.7615000000005,False -websocket,43,browser.fill,115.79779099999723,False -websocket,44,browser.click,100.42279100000087,False -websocket,45,browser.snapshot,697.3596660000039,False -websocket,46,browser.fill,118.68445799999608,False -websocket,47,browser.click,108.03749999999854,False -websocket,48,browser.snapshot,718.5740839999999,False -websocket,49,browser.fill,110.84404200000426,False -websocket,50,browser.click,122.67570800000249,False -websocket,51,browser.snapshot,713.7019579999978,False -websocket,52,browser.fill,126.12220800000068,False -websocket,53,browser.click,117.3492910000059,False -websocket,54,browser.snapshot,1216.9126660000038,False -websocket,55,browser.fill,140.5834590000013,False -websocket,56,browser.click,276.10608299999876,False -websocket,57,browser.snapshot,1332.8142090000038,False -websocket,58,browser.fill,107.97799999999552,False -websocket,59,browser.click,99.01779199999874,False -websocket,60,browser.snapshot,663.9896670000016,False -websocket,61,browser.fill,106.87354200000118,False -websocket,62,browser.click,99.30225000000064,False -websocket,63,browser.snapshot,670.1052909999999,False -websocket,64,browser.fill,168.42158400000335,False -websocket,65,browser.click,127.05783399999927,False -websocket,66,browser.snapshot,744.9471659999981,False -websocket,67,browser.fill,115.79125000000204,False -websocket,68,browser.click,115.71587500000169,False -websocket,69,browser.snapshot,725.476125000001,False -websocket,70,browser.fill,116.20245799999975,False -websocket,71,browser.click,104.46258300000045,False -websocket,72,browser.snapshot,669.9595830000035,False -websocket,73,browser.fill,109.82525000000169,False -websocket,74,browser.click,107.52516599999944,False -websocket,75,browser.snapshot,689.3825419999994,False -websocket,76,browser.fill,116.42087499999616,False -websocket,77,browser.click,125.02524999999878,False -websocket,78,browser.snapshot,730.819792000002,False -websocket,79,browser.fill,112.83883300000161,False -websocket,80,browser.click,114.01954199999454,False -websocket,81,browser.snapshot,670.2629579999993,False -websocket,82,browser.fill,107.11358299999847,False -websocket,83,browser.click,104.14087499999732,False -websocket,84,browser.snapshot,1126.5086670000019,False -websocket,85,browser.fill,126.3837910000002,False -websocket,86,browser.click,109.9945829999997,False -websocket,87,browser.snapshot,742.9184590000004,False -websocket,88,browser.fill,123.49979200000234,False -websocket,89,browser.click,109.56429199999548,False diff --git a/docs/benchmarks/2026-09-09-lifecycle.csv b/docs/benchmarks/2026-09-09-lifecycle.csv deleted file mode 100644 index e344cec..0000000 --- a/docs/benchmarks/2026-09-09-lifecycle.csv +++ /dev/null @@ -1,221 +0,0 @@ -run,cycle,scenario,milliseconds,error -1,0,navigation,529.8928749999995, -1,0,reconnect,570.7437090000003, -1,0,cancellation,503.53520799999933, -1,0,revocation,2032.4986669999998,"AssertionError [ERR_ASSERTION]: Expected values to be strictly deep-equal: -+ actual - expected - -+ [ -+ { -+ availability: 'available', -+ capabilities: { -+ level: 'interact' -+ }, -+ targetRef: 't1', -+ title: 'CDB public deep DOM fixture', -+ type: 'page', -+ url: 'http://cdb-root.test:56999/?profile=normal&shadow=closed&frames=cross-origin' -+ } -+ ] -- [] -" -2,0,navigation,809.4494999999988, -2,0,reconnect,597.996916, -2,0,cancellation,470.90104099999735, -2,0,revocation,657.044415999997, -2,1,navigation,584.5572920000013, -2,1,reconnect,607.9674169999998, -2,1,cancellation,575.3359999999993, -2,1,revocation,498.8061669999988, -2,2,navigation,582.8690419999984, -2,2,reconnect,633.9129579999972, -2,2,cancellation,446.27683400000024, -2,2,revocation,466.963416999999, -2,3,navigation,495.0878329999978, -2,3,reconnect,630.3752499999973, -2,3,cancellation,496.0032089999986, -2,3,revocation,464.6810829999995, -2,4,navigation,557.5289169999996, -2,4,reconnect,550.255000000001, -2,4,cancellation,504.8969159999979, -2,4,revocation,473.49566699999923, -2,5,navigation,524.9513340000012, -2,5,reconnect,594.1804579999989, -2,5,cancellation,538.2508750000015, -2,5,revocation,567.2530000000006, -2,6,navigation,619.6551660000005, -2,6,reconnect,658.3662919999988, -2,6,cancellation,421.5407500000001, -2,6,revocation,482.6445420000018, -2,7,navigation,498.9461670000019, -2,7,reconnect,562.4247920000016, -2,7,cancellation,488.70929199999955, -2,7,revocation,459.21687499999825, -2,8,navigation,771.9752500000031, -2,8,reconnect,982.1749589999963, -2,8,cancellation,480.18208400000003, -2,8,revocation,458.58899999999994, -2,9,navigation,555.5794580000002, -2,9,reconnect,584.6011249999938, -2,9,cancellation,835.5297499999942, -2,9,revocation,450.096749999997, -2,10,navigation,607.8306250000023, -2,10,reconnect,552.6204160000052, -2,10,cancellation,448.69941700000345, -2,10,revocation,476.57474999999977, -2,11,navigation,447.080833, -2,11,reconnect,578.8883750000023, -2,11,cancellation,418.4232499999998, -2,11,revocation,10147.949208000005,AssertionError [ERR_ASSERTION]: Lifecycle state did not settle within 10 seconds. -3,0,navigation,610.7144160000007, -3,0,reconnect,766.5124169999999, -3,0,cancellation,729.1168749999997, -3,0,revocation,578.8984170000003, -3,1,navigation,610.1754579999997, -3,1,reconnect,639.6232080000009, -3,1,cancellation,452.1203750000004, -3,1,revocation,472.42420800000036, -3,2,navigation,537.8957499999997, -3,2,reconnect,620.057667000001, -3,2,cancellation,504.69462499999645, -3,2,revocation,664.3996670000015, -3,3,navigation,763.1090409999997, -3,3,reconnect,821.0622919999987, -3,3,cancellation,685.4374160000007, -3,3,revocation,639.1363749999982, -3,4,navigation,1503.728415999998, -3,4,reconnect,1824.1104579999992, -3,4,cancellation,851.6594160000022, -3,4,revocation,512.9981249999983, -3,5,navigation,663.9302920000009, -3,5,reconnect,687.5470000000023, -3,5,cancellation,966.8957090000004, -3,5,revocation,850.4676250000011, -3,6,navigation,1081.8857080000016, -3,6,reconnect,904.3157919999976, -3,6,cancellation,907.979540999997, -3,6,revocation,1179.9177500000005, -3,7,navigation,1007.9408330000006, -3,7,reconnect,1948.1131670000032, -3,7,cancellation,856.7285829999964, -3,7,revocation,3706.669750000001,"AssertionError [ERR_ASSERTION]: {""content"":[{""type"":""text"",""text"":""{\""code\"":\""MCP_ACTION_TIMEOUT\"",\""retryable\"":true,\""name\"":\""BrokerError\"",\""message\"":\""The element action exceeded its total deadline.\""}""}],""isError"":true}" -4,0,navigation,840.5522500000006, -4,0,reconnect,613.8751659999998, -4,0,cancellation,406.25287500000013, -4,0,revocation,580.9712500000005, -4,1,navigation,1035.4930839999997, -4,1,reconnect,556.6568330000009, -4,1,cancellation,454.0032500000016, -4,1,revocation,515.974000000002, -4,2,navigation,1141.3940829999992, -4,2,reconnect,547.7874589999992, -4,2,cancellation,441.2112909999996, -4,2,revocation,456.3913750000029, -4,3,navigation,481.79870799999844, -4,3,reconnect,533.2465419999971, -4,3,cancellation,448.8767079999998, -4,3,revocation,445.0839589999996, -4,4,navigation,467.38887500000055, -4,4,reconnect,564.445958999997, -4,4,cancellation,476.5116250000028, -4,4,revocation,442.97920800000065, -4,5,navigation,440.40604200000234, -4,5,reconnect,555.7663339999999, -4,5,cancellation,466.0645420000001, -4,5,revocation,372.3630839999969, -4,6,navigation,470.66541700000016, -4,6,reconnect,565.6133749999972, -4,6,cancellation,661.6829580000012, -4,6,revocation,905.8379590000004, -4,7,navigation,483.35275000000183, -4,7,reconnect,563.7919580000016, -4,7,cancellation,447.692417000002, -4,7,revocation,484.77487499999916, -4,8,navigation,461.7535409999982, -4,8,reconnect,601.1364579999972, -4,8,cancellation,475.97095799999806, -4,8,revocation,421.68858399999954, -4,9,navigation,479.75120900000184, -4,9,reconnect,562.9543749999939, -4,9,cancellation,524.4302910000042, -4,9,revocation,433.34520899999916, -4,10,navigation,536.872832999994, -4,10,reconnect,542.5917079999999, -4,10,cancellation,446.8356669999994, -4,10,revocation,454.43162499999744, -4,11,navigation,482.5273340000058, -4,11,reconnect,555.3129170000029, -4,11,cancellation,449.026208999996, -4,11,revocation,386.9897920000003, -4,12,navigation,459.3313750000016, -4,12,reconnect,613.0302919999958, -4,12,cancellation,478.3731669999979, -4,12,revocation,480.9335829999982, -4,13,navigation,640.1185830000031, -4,13,reconnect,579.1991660000058, -4,13,cancellation,444.59275000000343, -4,13,revocation,441.83141700000124, -4,14,navigation,466.1395829999965, -4,14,reconnect,577.3650840000046, -4,14,cancellation,463.84808400000475, -4,14,revocation,357.27283299999544, -4,15,navigation,461.580958999999, -4,15,reconnect,548.2259169999961, -4,15,cancellation,439.5142500000002, -4,15,revocation,417.14800000000105, -4,16,navigation,439.3299580000021, -4,16,reconnect,545.1488329999993, -4,16,cancellation,452.66941699999734, -4,16,revocation,387.4641249999986, -4,17,navigation,475.19341699999495, -4,17,reconnect,763.211000000003, -4,17,cancellation,472.66116599999805, -4,17,revocation,871.4312500000015, -4,18,navigation,717.788542000002, -4,18,reconnect,554.7275840000002, -4,18,cancellation,446.1434999999983, -4,18,revocation,444.8478749999995, -4,19,navigation,452.5534590000025, -4,19,reconnect,559.7667920000022, -4,19,cancellation,424.00483299999905, -4,19,revocation,381.98379099999875, -4,20,navigation,455.3364169999986, -4,20,reconnect,562.4774580000012, -4,20,cancellation,445.31087500000285, -4,20,revocation,378.50850000000355, -4,21,navigation,468.294167, -4,21,reconnect,561.1938749999972, -4,21,cancellation,453.0898329999982, -4,21,revocation,375.6490830000039, -4,22,navigation,517.2823750000025, -4,22,reconnect,592.3987080000006, -4,22,cancellation,434.72745900000155, -4,22,revocation,423.90404199999466, -4,23,navigation,450.7939170000027, -4,23,reconnect,599.7050410000011, -4,23,cancellation,1324.3795830000017, -4,23,revocation,518.8442919999943, -4,24,navigation,596.5919589999976, -4,24,reconnect,574.5035419999986, -4,24,cancellation,776.609292000001, -4,24,revocation,1636.4512499999983, -4,25,navigation,1313.766791999995, -4,25,reconnect,788.9881250000035, -4,25,cancellation,437.35641600000963, -4,25,revocation,505.1249170000083, -4,26,navigation,685.5883749999921, -4,26,reconnect,604.414042000004, -4,26,cancellation,426.9273339999927, -4,26,revocation,1128.7142500000045, -4,27,navigation,528.9595420000114, -4,27,reconnect,631.538292000012, -4,27,cancellation,647.5146249999962, -4,27,revocation,494.4516670000012, -4,28,navigation,455.2708330000023, -4,28,reconnect,619.6287089999969, -4,28,cancellation,488.17804199999955, -4,28,revocation,467.6162919999915, -4,29,navigation,490.4511249999923, -4,29,reconnect,604.9181249999965, -4,29,cancellation,442.97474999999395, -4,29,revocation,453.1534580000007, diff --git a/docs/benchmarks/2026-09-09-lifecycle.md b/docs/benchmarks/2026-09-09-lifecycle.md index 1de4f6f..aa5877d 100644 --- a/docs/benchmarks/2026-09-09-lifecycle.md +++ b/docs/benchmarks/2026-09-09-lifecycle.md @@ -19,7 +19,7 @@ Each of 30 cycles reloads and acts, reconnects and acts, cancels a covered actio | Cancel, shared RPC read and action | 30 | 0 | 777 ms | | Revoke, shared RPC read, reapprove and action | 30 | 0 | 1,129 ms | -These are complete lifecycle scenarios, not warm single-action measurements. Machine load averages were 26.3/20.6/18.3 before and 22.2/19.9/18.2 afterward. [All lifecycle samples](./2026-09-09-lifecycle.csv) include the preliminary failures: +These are complete lifecycle scenarios, not warm single-action measurements. Machine load averages were 26.3/20.6/18.3 before and 22.2/19.9/18.2 afterward. The preliminary runs included these failures: - Run 1 revoked a scope left empty by the earlier overlap test. The diagnostic setup now clears those earlier scopes. - Run 2 completed 11 cycles before its rapid reapproval reached the existing two-second request rate limit. The test service now disables this throttle, and the diagnostic surfaces an early access-request error immediately. @@ -30,7 +30,7 @@ Opt-in `cdb.mcp.action` diagnostics now include pending and failed commands, the ## Performance gates -Both benchmarks ran sequentially after the lifecycle matrix, using their existing warmups and 30 samples. [Raw measurements](./2026-09-09-latency.csv) retain these failed gates. +Both benchmarks ran sequentially after the lifecycle matrix, using their existing warmups and 30 samples. The failed gates are summarized below. | Transport and scenario | p95 | Required gate | Result | | --- | ---: | ---: | --- | diff --git a/tests/e2e/README.md b/tests/e2e/README.md index fe16046..02a2598 100644 --- a/tests/e2e/README.md +++ b/tests/e2e/README.md @@ -49,14 +49,12 @@ The [preserved MCP baseline](measurements/mcp-baseline-811909f.json), revision ` These isolated Chromium results do not establish behavior in a user's main Chrome profile. That requires the same public workflow with the extension installed and the intended tab/window scope approved in that profile. -### Devframe workflow measurements +### Aligned transport and lifecycle diagnostics -Run the same public closed-shadow, three-level cross-origin fixture over the shared Devframe RPC provider connection: +Run **Browser diagnostics** manually in GitHub Actions on the desired branch or tag. It builds once, then measures WebSocket, Devframe and lifecycle scenarios sequentially with one worker. Failed suites do not skip the remaining suites. Full measurements and opt-in phase/command diagnostics go to the logs; the job summary includes completed sample counts and p95 values. No artifacts are uploaded or committed. -```sh -CDB_DEVFRAME_PERFORMANCE_GATE=1 CDB_DEVFRAME_BENCHMARK_OUTPUT=/tmp/cdb-devframe.json pnpm exec vitest run --project extension-e2e tests/e2e/devframe-native.test.ts -``` +Both transports use the same normal closed-shadow fixture with three cross-origin frame levels, five warm-ups and 30 samples for each reference/locator scenario. Fill, click, snapshot, batch with observation, individual workflows and blocked clicks are reported separately. Action p95 must be at most 500 ms and snapshot p95 at most 1,000 ms. Partial runs remain incomplete even when their available samples meet a latency target. The lifecycle suite requires all 120 scenarios across 30 cycles of navigation, reconnect, cancellation and revocation. -The default is 30 warm samples each for fill, click, compact snapshot, fill/click batch with observation, and a persistent parent overlay. `CDB_DEVFRAME_BENCHMARK_SAMPLES` accepts 5–50 for short diagnostic probes or longer runs. Reports separate blocked actions from successful actions, include individual workflow p95, catalogue/argument/response character counts, and collect the optional `cdb.mcp.action` phase diagnostics. Characters divided by four is only a token estimate. This benchmark uses exact role/name locators; the WebSocket benchmark's default fresh references measure a different discovery cost and cannot isolate transport differences. +Linux CI measurements establish a separate baseline from the historical macOS reports. Compare matching browser versions, fixtures and runner environments. Browser scheduling and transport overhead remain included; model time and application loading are excluded. Failed performance gates and unexplained stalls remain follow-ups for the experimental release. -Both harnesses record starting and ending OS load averages. The WebSocket report separates individual command round trips, direct `chrome.debugger.sendCommand` durations, and artifact HTTP reads. Chrome durations include browser scheduling and CDP execution; subtracting them from command round trips also includes provider/broker serialization and processing, so the difference is not network latency alone. `CDB_DEVFRAME_PERFORMANCE_GATE=1` enforces the same 500 ms action and 1,000 ms snapshot p95 budgets after saving the complete report. +For a targeted diagnostic outside CI, the shared runner accepts `CDB_WORKFLOW_BENCHMARK_SAMPLES` from 5 to 50 and `CDB_WORKFLOW_PERFORMANCE_GATE=1`. Enable either `CDB_WEBSOCKET_BENCHMARK_OUTPUT` for `native-agent-benchmark.test.ts` or `CDB_DEVFRAME_BENCHMARK_OUTPUT` for `devframe-native.test.ts`. Enable `CDB_LIFECYCLE_DIAGNOSTICS_OUTPUT` separately for the latter's lifecycle matrix. Output paths are JSON files; do not commit generated measurements. diff --git a/tests/e2e/fixtures/report-validation.ts b/tests/e2e/fixtures/report-validation.ts new file mode 100644 index 0000000..9575172 --- /dev/null +++ b/tests/e2e/fixtures/report-validation.ts @@ -0,0 +1,40 @@ +import { appendFile, readFile } from 'node:fs/promises'; +import { basename } from 'node:path'; +import { styleText } from 'node:util'; + +interface ValidationReport { + environment: Record; + samples?: number; + cycles?: number; + summaries?: Record; + measurements: { error?: unknown }[]; +} + +/** Print full diagnostics to CI logs and bounded summaries to the job page. */ +async function main(): Promise { + for (const filename of process.argv.slice(2)) { + let summary = `\n### ${basename(filename)}\n\n`; + try { + const content = await readFile(filename, 'utf8'); + const report = JSON.parse(content) as ValidationReport; + console.info(styleText('cyan', '📊 [browser-diagnostics]'), filename); + process.stdout.write(`${content}\n`); + summary += `Environment: ${JSON.stringify(report.environment)}\n\n`; + if (report.summaries !== undefined) { + summary += '| Scenario | Completed samples | p95 ms |\n| --- | ---: | ---: |\n'; + for (const [scenario, result] of Object.entries(report.summaries)) summary += `| ${scenario} | ${result.samples}/${report.samples} | ${result.p95Milliseconds?.toFixed(1) ?? 'unavailable'} |\n`; + summary += '\nTargets: successful action p95 ≤500 ms; snapshot p95 ≤1,000 ms. Partial samples do not establish a passing gate.\n'; + } else { + const completed = report.measurements.filter(measurement => measurement.error === undefined).length; + summary += `Lifecycle scenarios completed: ${completed}/${(report.cycles ?? 30) * 4}. Recorded failures: ${report.measurements.length - completed}.\n`; + } + } catch (error) { + console.error(styleText('red', '❌ [browser-diagnostics]'), filename, error); + summary += 'Report unavailable. This suite did not establish a passing result; inspect its test logs.\n'; + process.exitCode = 1; + } + if (process.env.GITHUB_STEP_SUMMARY !== undefined) await appendFile(process.env.GITHUB_STEP_SUMMARY, summary); + } +} + +void main(); diff --git a/tests/e2e/fixtures/workflow-benchmark.ts b/tests/e2e/fixtures/workflow-benchmark.ts index f77aeed..acde9eb 100644 --- a/tests/e2e/fixtures/workflow-benchmark.ts +++ b/tests/e2e/fixtures/workflow-benchmark.ts @@ -32,7 +32,8 @@ export async function benchmarkAgentWorkflow(agent: Client, chromiumVersion: str actionMetrics.push(measurement); }; const measurements: { selector: string; name: string; milliseconds: number; argumentsCharacters: number; responseCharacters: number; error: boolean }[] = []; - const summaries: Record = {}; + const summaries: Record = {}; + const workflows: Record = { ref: [], locator: [] }; const percentile95 = (values: number[]): number | undefined => values.sort((left, right) => left - right)[Math.ceil(values.length * 0.95) - 1]; const worker = page.context().serviceWorkers()[0]; if (worker === undefined) throw new Error('The benchmark extension worker is unavailable.'); @@ -75,14 +76,13 @@ export async function benchmarkAgentWorkflow(agent: Client, chromiumVersion: str fill: { ...locate('textbox', 'Deep value'), text: 'Benchmark value' }, click: { ...locate('button', 'Save deep value') }, }); - const workflows: number[] = []; for (let sample = -5; sample < samples; sample += 1) { const started = performance.now(); const individual = actions(); await call('browser.fill', { targetRef: 't1', ...individual.fill }, sample >= 0); await call('browser.click', { targetRef: 't1', ...individual.click }, sample >= 0); snapshot = await call('browser.snapshot', { targetRef: 't1' }, sample >= 0); - if (sample >= 0) workflows.push(performance.now() - started); + if (sample >= 0) workflows[selector]!.push(performance.now() - started); const next = actions(); const batch = { targetRef: 't1', observe: true, actions: [{ action: 'fill', ...next.fill }, { action: 'click', ...next.click }] }; snapshot = (JSON.parse(await call('browser.batch', batch, sample >= 0)) as { observation: string }).observation; @@ -93,12 +93,18 @@ export async function benchmarkAgentWorkflow(agent: Client, chromiumVersion: str } finally { await page.locator('#parent-overlay').evaluate(element => element.remove()); } - for (const name of ['browser.fill', 'browser.click', 'browser.snapshot', 'browser.batch', 'browser.click:blocked']) summaries[`${selector}:${name}`] = { p95Milliseconds: percentile95(measurements.filter(measurement => measurement.selector === selector && measurement.name === name).map(measurement => measurement.milliseconds)) }; - summaries[`${selector}:individual-workflow`] = { p95Milliseconds: percentile95(workflows) }; } } finally { diagnostics.unsubscribe(collect); snapshotDiagnostics.unsubscribe(collectSnapshot); + /** Summarize completed calls even when a later action interrupts the run. */ + for (const selector of ['ref', 'locator']) { + for (const name of ['browser.fill', 'browser.click', 'browser.snapshot', 'browser.batch', 'browser.click:blocked']) { + const durations = measurements.filter(measurement => measurement.selector === selector && measurement.name === name && (name.endsWith(':blocked') || !measurement.error)).map(measurement => measurement.milliseconds); + summaries[`${selector}:${name}`] = { samples: durations.length, p95Milliseconds: percentile95(durations) }; + } + summaries[`${selector}:individual-workflow`] = { samples: workflows[selector]!.length, p95Milliseconds: percentile95(workflows[selector]!) }; + } const chromeCommands = await worker.evaluate(() => { const workerGlobal = globalThis as unknown as BenchmarkWorker; workerGlobal.restoreCdbBenchmarkCommands(); diff --git a/tests/e2e/workflow-report.test.ts b/tests/e2e/workflow-report.test.ts new file mode 100644 index 0000000..63cafe5 --- /dev/null +++ b/tests/e2e/workflow-report.test.ts @@ -0,0 +1,49 @@ +import type { Page } from 'playwright'; + +import { mkdtemp, readFile, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +import { Client } from '@modelcontextprotocol/client'; +import { expect, it, vi } from 'vitest'; + +import { benchmarkAgentWorkflow } from './fixtures/workflow-benchmark.js'; + +it('retains completed workflow summaries when the subsequent blocked-action phase fails', async () => { + expect.assertions(4); + const directory = await mkdtemp(join(tmpdir(), 'cdb-report-')); + const output = join(directory, 'report.json'); + const observation = 'textbox "Deep value" [ref=e1]\nbutton "Save deep value" [ref=e2]'; + const worker = { evaluate: vi.fn().mockResolvedValue([]) }; + let covered = false; + const page = { + context: () => ({ serviceWorkers: () => [worker] }), + locator: () => ({ click: async () => { + covered = true; + }, evaluate: async () => {} }), + } as unknown as Page; + const client = new Client({ name: 'report regression', version: '1.0.0' }); + vi.spyOn(client, 'callTool').mockImplementation(async (request) => { + if (covered) throw new Error('Fixture transport interrupted'); + return { content: [{ type: 'text', text: request.name === 'browser.batch' ? JSON.stringify({ observation }) : observation }] }; + }); + vi.stubEnv('CDB_DEVFRAME_BENCHMARK_OUTPUT', output); + vi.stubEnv('CDB_WORKFLOW_BENCHMARK_SAMPLES', '5'); + try { + await expect(benchmarkAgentWorkflow(client, 'fixture', page)).rejects.toThrow('Fixture transport interrupted'); + const report = JSON.parse(await readFile(output, 'utf8')) as { summaries: unknown; measurements: unknown[] }; + expect(report.summaries).toMatchObject({ + 'ref:browser.fill': { samples: 5 }, + 'ref:browser.snapshot': { samples: 5 }, + 'ref:individual-workflow': { samples: 5 }, + 'ref:browser.click:blocked': { samples: 0 }, + 'locator:browser.fill': { samples: 0 }, + }); + expect(report.measurements).toHaveLength(20); + expect(worker.evaluate).toHaveBeenCalledTimes(2); + } finally { + vi.restoreAllMocks(); + vi.unstubAllEnvs(); + await rm(directory, { recursive: true, force: true }); + } +}); From 85f75ef897a4e5aae45e5cfeec1dd778d5e01bbf Mon Sep 17 00:00:00 2001 From: dvcolomban Date: Sat, 12 Sep 2026 14:58:23 +0200 Subject: [PATCH 6/6] fix(websocket): retain requests during host session setup --- packages/websocket/src/node.ts | 11 ++++- .../test/authenticated-websocket.test.ts | 48 ++++++++++++++++++- 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/packages/websocket/src/node.ts b/packages/websocket/src/node.ts index 65fca5a..3639bc6 100644 --- a/packages/websocket/src/node.ts +++ b/packages/websocket/src/node.ts @@ -367,11 +367,16 @@ function createBrokerClientConnection( webSocket: WebSocket, ): AuthenticatedConnection { const listeners = new Set>(); + const listenerReady = Promise.withResolvers(); const closed = createClosedPromise(webSocket); let pendingMessages = 0; let pendingReceive = Promise.resolve(); let receiveFailed = false; - webSocket.once('close', () => listeners.clear()); + webSocket.once('close', () => { + receiveFailed = true; + listeners.clear(); + listenerReady.resolve(); + }); webSocket.on('message', (data, isBinary) => { if (receiveFailed) { return; @@ -396,6 +401,9 @@ function createBrokerClientConnection( if ('issues' in result) { throw new Error('Invalid client-plane message'); } + /** Hosts may await session storage before attaching their first listener. */ + await listenerReady.promise; + if (receiveFailed) return; for (const listener of listeners) { listener(result.value); } @@ -411,6 +419,7 @@ function createBrokerClientConnection( }, onMessage(listener) { listeners.add(listener); + listenerReady.resolve(); return () => listeners.delete(listener); }, async send(message) { diff --git a/packages/websocket/test/authenticated-websocket.test.ts b/packages/websocket/test/authenticated-websocket.test.ts index 285f5b2..8938280 100644 --- a/packages/websocket/test/authenticated-websocket.test.ts +++ b/packages/websocket/test/authenticated-websocket.test.ts @@ -4,9 +4,12 @@ import type { AgentAuthenticationTranscript } from '../src/authentication.js'; import type { AgentAuthenticationAdapter, AuthenticatedAgentConnection, + AuthenticatedClientConnection, StandaloneAuthenticatedWebSocketBridge, } from '../src/node.js'; +import { setTimeout as delay } from 'node:timers/promises'; + import { afterEach, expect, it } from 'vitest'; import { WebSocket } from 'ws'; @@ -46,6 +49,7 @@ async function createTestBridge(options: { connection: AuthenticatedAgentConnection<{ readonly id: string; readonly role: 'agent' }>, ) => void; readonly onClientMessage?: () => void; + readonly onClientConnection?: (connection: AuthenticatedClientConnection<{ readonly id: string; readonly role: 'client' }>) => void; readonly onClientPrincipal?: (principalId: string) => void; readonly originPolicy?: (origin: string | undefined) => boolean | Promise; readonly pairingTimeoutMilliseconds?: number; @@ -82,7 +86,9 @@ async function createTestBridge(options: { onAgentConnection(connection) { options.onAgentConnection?.(connection); }, - onClientConnection({ connection, principal }) { + onClientConnection(client) { + if (options.onClientConnection !== undefined) return options.onClientConnection(client); + const { connection, principal } = client; options.onClientPrincipal?.(principal.id); connection.onMessage((message) => { options.onClientMessage?.(); @@ -128,6 +134,46 @@ async function createTestBridge(options: { return bridge; } +it('delivers the initial client request after asynchronous host session setup', async () => { + expect.assertions(1); + const connected = Promise.withResolvers>(); + const bridge = await createTestBridge({ onClientConnection: connected.resolve }); + const client = await connectNodeClientWebSocket({ endpoint: `ws://${bridge.host}:${bridge.port}/cdb/client`, authorization: 'Bearer valid-client-token' }); + const server = await connected.promise; + const request = { kind: 'request', method: 'targets.list', parameters: {}, protocolVersion: 1, requestId: crypto.randomUUID() } as const; + const nextRequest = { ...request, requestId: crypto.randomUUID() }; + try { + await client.send(request); + await client.send(nextRequest); + /** Allow receipt while the embedding host is still awaiting its session store. */ + await delay(50); + const received = new Promise((resolve) => { + const messages: unknown[] = []; + server.connection.onMessage((message) => { + messages.push(message); + if (messages.length === 2) resolve(messages); + }); + }); + await expect(waitForTestStage(received, 'delivering the initial requests')).resolves.toEqual([request, nextRequest]); + } finally { + client.close(); + await client.closed; + } +}); + +it('bounds client messages waiting for the first host listener', async () => { + expect.assertions(1); + const bridge = await createTestBridge({ onClientConnection() {} }); + const client = await connectNodeClientWebSocket({ endpoint: `ws://${bridge.host}:${bridge.port}/cdb/client`, authorization: 'Bearer valid-client-token' }); + try { + for (let index = 0; index < 33; index += 1) await client.send({ kind: 'request', method: 'targets.list', parameters: {}, protocolVersion: 1, requestId: crypto.randomUUID() }); + await expect(waitForTestStage(client.closed, 'closing the overflowing connection')).resolves.toMatchObject({ code: 1008 }); + } finally { + client.close(); + await client.closed; + } +}); + async function waitForClientMessage( connection: { onMessage: (listener: (message: BrokerToClientMessage) => void) => () => void }, ): Promise {