mirror of
https://github.com/anomalyco/opencode.git
synced 2026-09-08 05:14:38 +00:00
feat(desktop): add tabbed browser automation and diagnostics
This commit is contained in:
parent
0a7d65feef
commit
c6b2d4a572
25 changed files with 2710 additions and 343 deletions
169
bun.lock
169
bun.lock
|
|
@ -417,6 +417,8 @@
|
|||
"electron-store": "11.0.2",
|
||||
"electron-updater": "6.8.9",
|
||||
"electron-window-state": "^5.0.3",
|
||||
"lighthouse": "13.4.1",
|
||||
"puppeteer-core": "25.9.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@brendonovich/vite-plugin-opencode": "0.1.3",
|
||||
|
|
@ -435,6 +437,7 @@
|
|||
"@types/node": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"app-builder-lib": "26.15.7",
|
||||
"devtools-protocol": "0.0.1687809",
|
||||
"drizzle-orm": "catalog:",
|
||||
"effect": "catalog:",
|
||||
"electron": "42.10.1",
|
||||
|
|
@ -1841,6 +1844,16 @@
|
|||
|
||||
"@fontsource/inter": ["@fontsource/inter@5.3.0", "", {}, "sha512-RofMylZmjlJEfELXeNHFWBRcSs75rGU/6bV2S2jfnvv/3rPXPGe0LgUJTklcHZ9lM4OZmAVFhcJPnACfb91A3g=="],
|
||||
|
||||
"@formatjs/ecma402-abstract": ["@formatjs/ecma402-abstract@2.3.6", "", { "dependencies": { "@formatjs/fast-memoize": "2.2.7", "@formatjs/intl-localematcher": "0.6.2", "decimal.js": "^10.4.3", "tslib": "^2.8.0" } }, "sha512-HJnTFeRM2kVFVr5gr5kH1XP6K0JcJtE7Lzvtr3FS/so5f1kpsqqqxy5JF+FRaO6H2qmcMfAUIox7AJteieRtVw=="],
|
||||
|
||||
"@formatjs/fast-memoize": ["@formatjs/fast-memoize@2.2.7", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ=="],
|
||||
|
||||
"@formatjs/icu-messageformat-parser": ["@formatjs/icu-messageformat-parser@2.11.4", "", { "dependencies": { "@formatjs/ecma402-abstract": "2.3.6", "@formatjs/icu-skeleton-parser": "1.8.16", "tslib": "^2.8.0" } }, "sha512-7kR78cRrPNB4fjGFZg3Rmj5aah8rQj9KPzuLsmcSn4ipLXQvC04keycTI1F7kJYDwIXtT2+7IDEto842CfZBtw=="],
|
||||
|
||||
"@formatjs/icu-skeleton-parser": ["@formatjs/icu-skeleton-parser@1.8.16", "", { "dependencies": { "@formatjs/ecma402-abstract": "2.3.6", "tslib": "^2.8.0" } }, "sha512-H13E9Xl+PxBd8D5/6TVUluSpxGNvFSlN/b3coUp0e0JpuWXXnQDiavIpY3NnvSp4xhEMoXyyBvVfdFX8jglOHQ=="],
|
||||
|
||||
"@formatjs/intl-localematcher": ["@formatjs/intl-localematcher@0.6.2", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA=="],
|
||||
|
||||
"@gar/promise-retry": ["@gar/promise-retry@1.0.3", "", {}, "sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA=="],
|
||||
|
||||
"@graphql-typed-document-node/core": ["@graphql-typed-document-node/core@3.2.0", "", { "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ=="],
|
||||
|
|
@ -2227,6 +2240,8 @@
|
|||
|
||||
"@opentelemetry/exporter-trace-otlp-http": ["@opentelemetry/exporter-trace-otlp-http@0.214.0", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/otlp-exporter-base": "0.214.0", "@opentelemetry/otlp-transformer": "0.214.0", "@opentelemetry/resources": "2.6.1", "@opentelemetry/sdk-trace-base": "2.6.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-kIN8nTBMgV2hXzV/a20BCFilPZdAIMYYJGSgfMMRm/Xa+07y5hRDS2Vm12A/z8Cdu3Sq++ZvJfElokX2rkgGgw=="],
|
||||
|
||||
"@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.220.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.220.0", "import-in-the-middle": "^3.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-xQx3E2WxP1mDvKzxLxX+CTCtNLa560YJZ3087qYHerl2YmiKpv7AH+dAy7vmx+eVrZ5BwhfWUAVoKOoxCNHcpw=="],
|
||||
|
||||
"@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.214.0", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/otlp-transformer": "0.214.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-u1Gdv0/E9wP+apqWf7Wv2npXmgJtxsW2XL0TEv9FZloTZRuMBKmu8cYVXwS4Hm3q/f/3FuCnPTgiwYvIqRSpRg=="],
|
||||
|
||||
"@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.214.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.214.0", "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/sdk-logs": "0.214.0", "@opentelemetry/sdk-metrics": "2.6.1", "@opentelemetry/sdk-trace-base": "2.6.1", "protobufjs": "^7.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-DSaYcuBRh6uozfsWN3R8HsN0yDhCuWP7tOFdkUOVaWD1KVJg8m4qiLUsg/tNhTLS9HUYUcwNpwL2eroLtsZZ/w=="],
|
||||
|
|
@ -2237,6 +2252,8 @@
|
|||
|
||||
"@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-9t9hJHX15meBy2NmTJxL+NJfXmnausR2xUDvE19XQce0Qi/GBtDGamU8nS1RMbdgDmhgpm3VaOu2+fiS/SfTpQ=="],
|
||||
|
||||
"@opentelemetry/sdk-trace": ["@opentelemetry/sdk-trace@2.10.0", "", { "dependencies": { "@opentelemetry/core": "2.10.0", "@opentelemetry/resources": "2.10.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-MfQGq3GRmTh5fM/y+OjaO0vj6+luCB1XO2gfXCalKCfgKw0eHL++sm75DNweC6ohlp+aFvACqeE0fYayqdRaoQ=="],
|
||||
|
||||
"@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-r86ut4T1e8vNwB35CqCcKd45yzqH6/6Wzvpk2/cZB8PsPLlZFTvrh8yfOS3CYZYcUmAx4hHTZJ8AO8Dj8nrdhw=="],
|
||||
|
||||
"@opentelemetry/sdk-trace-node": ["@opentelemetry/sdk-trace-node@2.6.1", "", { "dependencies": { "@opentelemetry/context-async-hooks": "2.6.1", "@opentelemetry/core": "2.6.1", "@opentelemetry/sdk-trace-base": "2.6.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-Hh2i4FwHWRFhnO2Q/p6svMxy8MPsNCG0uuzUY3glqm0rwM0nQvbTO1dXSp9OqQoTKXcQzaz9q1f65fsurmOhNw=="],
|
||||
|
|
@ -2509,6 +2526,8 @@
|
|||
|
||||
"@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.1", "", { "os": "win32", "cpu": "x64" }, "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA=="],
|
||||
|
||||
"@paulirish/trace_engine": ["@paulirish/trace_engine@0.0.65", "", { "dependencies": { "legacy-javascript": "latest", "third-party-web": "latest" } }, "sha512-Qsm6F5C8xf6ZzQXbQc2+wcpe6sggfs/gvc/ytqSurdvYg3kyW0ECHCqE0CWBKZpqgjVfPNX9c7SCS3r2nEIRGg=="],
|
||||
|
||||
"@peculiar/asn1-schema": ["@peculiar/asn1-schema@2.9.0", "", { "dependencies": { "@peculiar/utils": "^2.0.2", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-AKvPMOM7LfK0uFe1m7o7+veOa8xQGPqsqOrKi3QKgCElzwjGp39mbhr2g7mt/v/mXQHiIvJmDj5cJS173x8Q9Q=="],
|
||||
|
||||
"@peculiar/json-schema": ["@peculiar/json-schema@1.1.12", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w=="],
|
||||
|
|
@ -2565,6 +2584,8 @@
|
|||
|
||||
"@protobufjs/utf8": ["@protobufjs/utf8@1.1.2", "", {}, "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug=="],
|
||||
|
||||
"@puppeteer/browsers": ["@puppeteer/browsers@3.2.1", "", { "dependencies": { "modern-tar": "^0.8.0", "yargs": "^18.0.0" }, "peerDependencies": { "proxy-agent": ">=8.0.1", "yauzl": "^2.10.0 || ^3.4.0" }, "optionalPeers": ["proxy-agent", "yauzl"], "bin": { "browsers": "lib/main-cli.js" } }, "sha512-KDz+3qDRdBAlRlMjmKyj6dEs33YHTk/xRHEENSXq6TNnhgoU15ruSHtEBeVF6OZ9tBDY55Se4P0nFMNsipzU9A=="],
|
||||
|
||||
"@radix-ui/colors": ["@radix-ui/colors@1.0.1", "", {}, "sha512-xySw8f0ZVsAEP+e7iLl3EvcBXX7gsIlC1Zso/sPBW9gIWerBTgz6axrjU+MZ39wD+WFi5h5zdWpsg3+hwt2Qsg=="],
|
||||
|
||||
"@radix-ui/primitive": ["@radix-ui/primitive@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" } }, "sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw=="],
|
||||
|
|
@ -2763,10 +2784,18 @@
|
|||
|
||||
"@sentry/feedback": ["@sentry/feedback@10.71.0", "", { "dependencies": { "@sentry/core": "10.71.0" } }, "sha512-4+zMAmn1DcbYBtFE0pWt6zo8vWoBHTuP/UhtCMTCoGB4sVYvmwRxv9Hc9OpRHSzE9kSl8beD0zKFxuoVHizKQQ=="],
|
||||
|
||||
"@sentry/node": ["@sentry/node@10.72.0", "", { "dependencies": { "@opentelemetry/api": "^1.9.1", "@opentelemetry/instrumentation": "^0.220.0", "@opentelemetry/sdk-trace-base": "^2.9.0", "@sentry/conventions": "^0.16.0", "@sentry/core": "10.72.0", "@sentry/node-core": "10.72.0", "@sentry/opentelemetry": "10.72.0", "@sentry/server-utils": "10.72.0", "import-in-the-middle": "^3.0.0" } }, "sha512-eQHQFxSX26MhG/nB+tAY4QRslnPvJse7pww/hd3zXAqNULRa5lFtjSLALcHx/KUVDCZdTPdUVZEj4nGidcHrow=="],
|
||||
|
||||
"@sentry/node-core": ["@sentry/node-core@10.72.0", "", { "dependencies": { "@sentry/conventions": "^0.16.0", "@sentry/core": "10.72.0", "@sentry/opentelemetry": "10.72.0", "import-in-the-middle": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/core": "^1.30.1 || ^2.1.0", "@opentelemetry/exporter-trace-otlp-http": ">=0.57.0 <1", "@opentelemetry/instrumentation": ">=0.57.1 <1", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/core", "@opentelemetry/exporter-trace-otlp-http", "@opentelemetry/instrumentation", "@opentelemetry/sdk-trace-base"] }, "sha512-xYuYWmWEWnN8h3YHa7mds212ANFgrxfrbmLvVg0p0wf9m6MFjLowOTmjaiK0XW20sM/veSelicre650bx8UFtQ=="],
|
||||
|
||||
"@sentry/opentelemetry": ["@sentry/opentelemetry@10.72.0", "", { "dependencies": { "@sentry/conventions": "^0.16.0", "@sentry/core": "10.72.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/core": "^1.30.1 || ^2.1.0", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0" } }, "sha512-ZVbAM1rGU/awN7cH/jvC87WpQw0NOJx5id6dKnnefStXpP/kUnZXYhtX9gfBnofYvJWa5I5VUSeuKCLUcKL75w=="],
|
||||
|
||||
"@sentry/replay": ["@sentry/replay@10.71.0", "", { "dependencies": { "@sentry/browser-utils": "10.71.0", "@sentry/core": "10.71.0" } }, "sha512-ytEgVg7isvavQL6hgsgYeuSCkcA3EyOKm9lMLQOZTLkiUCtFT/ItWwGNhzS46vQ6wsTv3Uc0Y1JlcJHSFKe/Kg=="],
|
||||
|
||||
"@sentry/replay-canvas": ["@sentry/replay-canvas@10.71.0", "", { "dependencies": { "@sentry/core": "10.71.0", "@sentry/replay": "10.71.0" } }, "sha512-YTqesxBh9arExI49LrTm4Y2/xPX40q2GWi0gWRw6lNfYtyNz7/ZfHi8/1N6JEc8dldTslqFhII6ZFecUyU9iaA=="],
|
||||
|
||||
"@sentry/server-utils": ["@sentry/server-utils@10.72.0", "", { "dependencies": { "@sentry/conventions": "^0.16.0", "@sentry/core": "10.72.0" } }, "sha512-CWpHMYW81RDBSVBdnxulGUvvBhkBb/OpSr72ubSe1UkKTcgT0NDMCWxE3dLFXqSxzT4DaF5UlUVv9ii5Sse53w=="],
|
||||
|
||||
"@sentry/solid": ["@sentry/solid@10.71.0", "", { "dependencies": { "@sentry/browser": "10.71.0", "@sentry/conventions": "^0.16.0", "@sentry/core": "10.71.0" }, "peerDependencies": { "@solidjs/router": ">=0.13.4 <2.0.0-0", "@tanstack/solid-router": "^1.132.27", "solid-js": "^1.8.4" }, "optionalPeers": ["@solidjs/router", "@tanstack/solid-router"] }, "sha512-/vVy9GmB/RejmomWEMZ+P3xlwrhfpIYJ3F0+UcTE836uuH92r8CSnmucdGQqke8c/6ullQFjZrLd/dTDPXj1aA=="],
|
||||
|
||||
"@sentry/vite-plugin": ["@sentry/vite-plugin@5.4.0", "", { "dependencies": { "@sentry/bundler-plugins": "^10.64.0" } }, "sha512-fFJgCxs5hDyAm9BbZJ+LbA+LK2tjX5OoD0v0ARU4StR6KQmGUduoPs69yJ9AfqZ0om3Rlp5JDliiwFcNkasORA=="],
|
||||
|
|
@ -3333,6 +3362,8 @@
|
|||
|
||||
"ansi-align": ["ansi-align@3.0.1", "", { "dependencies": { "string-width": "^4.1.0" } }, "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w=="],
|
||||
|
||||
"ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="],
|
||||
|
||||
"ansi-regex": ["ansi-regex@6.3.0", "", {}, "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ=="],
|
||||
|
||||
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
|
|
@ -3561,10 +3592,16 @@
|
|||
|
||||
"chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="],
|
||||
|
||||
"chrome-launcher": ["chrome-launcher@1.2.1", "", { "dependencies": { "@types/node": "*", "escape-string-regexp": "^4.0.0", "is-wsl": "^2.2.0", "lighthouse-logger": "^2.0.1" }, "bin": { "print-chrome-path": "bin/print-chrome-path.cjs" } }, "sha512-qmFR5PLMzHyuNJHwOloHPAHhbaNglkfeV/xDtt5b7xiFFyU1I+AZZX0PYseMuhenJSSirgxELYIbswcoc+5H4A=="],
|
||||
|
||||
"chromium-bidi": ["chromium-bidi@17.0.2", "", { "dependencies": { "mitt": "^3.0.1", "zod": "^3.24.1" }, "peerDependencies": { "devtools-protocol": "*" } }, "sha512-5v9GQFhTktFvotn/OFNJBmKLKRAb6n9r0bVCwf7sHgWc3/JryK0bj1nn93L3pHFrfgcsu6Be6EWsDi+1XHTGDg=="],
|
||||
|
||||
"chromium-pickle-js": ["chromium-pickle-js@0.2.0", "", {}, "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw=="],
|
||||
|
||||
"ci-info": ["ci-info@4.3.1", "", {}, "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA=="],
|
||||
|
||||
"cjs-module-lexer": ["cjs-module-lexer@2.2.1", "", {}, "sha512-Ca8swihM+/4yKecYHY52kgJd300hi2lADU/a1RxNTRe+RJ9jvqQlESpbz9DnG9mowez8qwXHB8qYdIUw9e+F5Q=="],
|
||||
|
||||
"classnames": ["classnames@2.3.2", "", {}, "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw=="],
|
||||
|
||||
"clean-css": ["clean-css@5.3.3", "", { "dependencies": { "source-map": "~0.6.0" } }, "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg=="],
|
||||
|
|
@ -3621,6 +3658,8 @@
|
|||
|
||||
"config-chain": ["config-chain@1.1.13", "", { "dependencies": { "ini": "^1.3.4", "proto-list": "~1.2.1" } }, "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ=="],
|
||||
|
||||
"configstore": ["configstore@7.1.0", "", { "dependencies": { "atomically": "^2.0.3", "dot-prop": "^9.0.0", "graceful-fs": "^4.2.11", "xdg-basedir": "^5.1.0" } }, "sha512-N4oog6YJWbR9kGyXvS7jEykLDXIE2C0ILYqNBZBp9iwiJpoCBWYsuAdW6PPFn6w06jjnC+3JstVvWHO4cZqvRg=="],
|
||||
|
||||
"consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="],
|
||||
|
||||
"content-disposition": ["content-disposition@1.1.0", "", {}, "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g=="],
|
||||
|
|
@ -3657,6 +3696,8 @@
|
|||
|
||||
"crypto-random-string": ["crypto-random-string@2.0.0", "", {}, "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA=="],
|
||||
|
||||
"csp_evaluator": ["csp_evaluator@1.1.8", "", {}, "sha512-EwOnfYuNbTytvbMKsLixTrRgnjOa0WZCxGy8A9nnSYAicrdwn+T/epU/yjgymmOxlgKnvH+8wXt+7p/8ak5Feg=="],
|
||||
|
||||
"css-select": ["css-select@5.2.2", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="],
|
||||
|
||||
"css-selector-parser": ["css-selector-parser@3.3.0", "", {}, "sha512-Y2asgMGFqJKF4fq4xHDSlFYIkeVfRsm69lQC1q9kbEsH5XtnINTMrweLkjYMeaUgiXBy/uvKeO/a1JHTNnmB2g=="],
|
||||
|
|
@ -3761,6 +3802,8 @@
|
|||
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"decimal.js": ["decimal.js@10.6.0", "", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="],
|
||||
|
||||
"decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="],
|
||||
|
||||
"decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="],
|
||||
|
|
@ -3809,6 +3852,8 @@
|
|||
|
||||
"devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
|
||||
|
||||
"devtools-protocol": ["devtools-protocol@0.0.1687809", "", {}, "sha512-t7kSb+UKYdyxcmqy98U2P86ne6rCve3DIJ4+lp54i/hhClgFuG35JrLj23rO14Jw3sWDsp7i+22QhNU0+/qk8g=="],
|
||||
|
||||
"dfa": ["dfa@1.2.0", "", {}, "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q=="],
|
||||
|
||||
"diacritics": ["diacritics@1.3.0", "", {}, "sha512-wlwEkqcsaxvPJML+rDh/2iS824jbREk6DUMUKkEaSlxdYHeS43cClJtsWglvw2RfeXGm6ohKDqsXteJ5sP5enA=="],
|
||||
|
|
@ -3913,6 +3958,8 @@
|
|||
|
||||
"enhanced-resolve": ["enhanced-resolve@5.24.5", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A=="],
|
||||
|
||||
"enquirer": ["enquirer@2.4.1", "", { "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" } }, "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ=="],
|
||||
|
||||
"entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
|
||||
|
||||
"env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="],
|
||||
|
|
@ -3957,7 +4004,7 @@
|
|||
|
||||
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
|
||||
|
||||
"escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
|
||||
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
||||
|
||||
"esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="],
|
||||
|
||||
|
|
@ -4259,6 +4306,8 @@
|
|||
|
||||
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
|
||||
|
||||
"http-link-header": ["http-link-header@1.1.4", "", {}, "sha512-xT3GPW6/ZbGuw4UvwHqErSCEjNUlwbQJuZn9/q5U4WEKfp2kENVCAlousG1zLxHeaQ/ffOHUNpWamvkbBW0eNw=="],
|
||||
|
||||
"http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="],
|
||||
|
||||
"http2-wrapper": ["http2-wrapper@1.0.3", "", { "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.0.0" } }, "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg=="],
|
||||
|
|
@ -4283,8 +4332,12 @@
|
|||
|
||||
"ignore-walk": ["ignore-walk@8.0.0", "", { "dependencies": { "minimatch": "^10.0.3" } }, "sha512-FCeMZT4NiRQGh+YkeKMtWrOmBgWjHjMJ26WQWrRQyoyzqevdaGSakUaJW5xQYmjLlUVk2qUnCjYVBax9EKKg8A=="],
|
||||
|
||||
"image-ssim": ["image-ssim@0.2.0", "", {}, "sha512-W7+sO6/yhxy83L0G7xR8YAc5Z5QFtYEXXRV6EaE8tuYBZJnA3gVgp3q7X7muhLZVodeb9UfvjSbwt9VJwjIYAg=="],
|
||||
|
||||
"immer": ["immer@11.1.4", "", {}, "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw=="],
|
||||
|
||||
"import-in-the-middle": ["import-in-the-middle@3.3.3", "", { "dependencies": { "cjs-module-lexer": "^2.2.0", "es-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" } }, "sha512-AiohS3H80sXO6owEltjGX+glb7qXaDhBoJb9XcQVH4UI207xu/bDLUcadVKp7Qe576reg9yr/PXZjV5qx8gfbA=="],
|
||||
|
||||
"import-local": ["import-local@3.2.0", "", { "dependencies": { "pkg-dir": "^4.2.0", "resolve-cwd": "^3.0.0" }, "bin": { "import-local-fixture": "fixtures/cli.js" } }, "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA=="],
|
||||
|
||||
"import-meta-resolve": ["import-meta-resolve@4.2.0", "", {}, "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg=="],
|
||||
|
|
@ -4303,6 +4356,8 @@
|
|||
|
||||
"internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="],
|
||||
|
||||
"intl-messageformat": ["intl-messageformat@10.7.18", "", { "dependencies": { "@formatjs/ecma402-abstract": "2.3.6", "@formatjs/fast-memoize": "2.2.7", "@formatjs/icu-messageformat-parser": "2.11.4", "tslib": "^2.8.0" } }, "sha512-m3Ofv/X/tV8Y3tHXLohcuVuhWKo7BBq62cqY15etqmLxg2DZ34AGGgQDeR+SCta2+zICb1NX83af0GJmbQ1++g=="],
|
||||
|
||||
"ip-address": ["ip-address@10.5.0", "", {}, "sha512-R5SnVLJmgYYvf2F2ZgwSBnelz5G4q5AxIC277GDfUaNbrZKNANcBC7RHqYYePlszf4kBolVkJauG0ZjHHFh55g=="],
|
||||
|
||||
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
|
||||
|
|
@ -4421,12 +4476,16 @@
|
|||
|
||||
"jose": ["jose@6.0.11", "", {}, "sha512-QxG7EaliDARm1O1S8BGakqncGT9s25bKL1WSf6/oa17Tkqwi8D2ZNglqCF+DsYF88/rV66Q/Q2mFAy697E1DUg=="],
|
||||
|
||||
"jpeg-js": ["jpeg-js@0.4.4", "", {}, "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg=="],
|
||||
|
||||
"js-base64": ["js-base64@3.7.7", "", {}, "sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw=="],
|
||||
|
||||
"js-beautify": ["js-beautify@1.15.4", "", { "dependencies": { "config-chain": "^1.1.13", "editorconfig": "^1.0.4", "glob": "^10.4.2", "js-cookie": "^3.0.5", "nopt": "^7.2.1" }, "bin": { "js-beautify": "js/bin/js-beautify.js", "css-beautify": "js/bin/css-beautify.js", "html-beautify": "js/bin/html-beautify.js" } }, "sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA=="],
|
||||
|
||||
"js-cookie": ["js-cookie@3.0.8", "", {}, "sha512-yeJd4aNAdYZQjaon2bpD/Gb0B/omw7HQOsynXXcOiWVCacbBcPlgn8S/d1X6blFSaHao7ozqtW7NZW19xpCtIw=="],
|
||||
|
||||
"js-library-detector": ["js-library-detector@6.7.0", "", {}, "sha512-c80Qupofp43y4cJ7+8TTDN/AsDwLi5oOm/plBrWI+iQt485vKXCco+yVmOwEgdo9VOdsYTuV0UlTeetVPTriXA=="],
|
||||
|
||||
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||
|
||||
"js-yaml": ["js-yaml@3.15.1", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag=="],
|
||||
|
|
@ -4499,8 +4558,16 @@
|
|||
|
||||
"leac": ["leac@0.6.0", "", {}, "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg=="],
|
||||
|
||||
"legacy-javascript": ["legacy-javascript@0.0.1", "", {}, "sha512-lPyntS4/aS7jpuvOlitZDFifBCb4W8L/3QU0PLbUTUj+zYah8rfVjYic88yG7ZKTxhS5h9iz7duT8oUXKszLhg=="],
|
||||
|
||||
"leven": ["leven@3.1.0", "", {}, "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A=="],
|
||||
|
||||
"lighthouse": ["lighthouse@13.4.1", "", { "dependencies": { "@paulirish/trace_engine": "0.0.65", "@sentry/node": "^10.0.0", "axe-core": "^4.12.1", "chrome-launcher": "^1.2.1", "configstore": "^7.0.0", "csp_evaluator": "1.1.8", "devtools-protocol": "0.0.1663043", "enquirer": "^2.3.6", "http-link-header": "^1.1.1", "intl-messageformat": "^10.5.3", "jpeg-js": "^0.4.4", "js-library-detector": "^6.7.0", "lighthouse-logger": "^2.0.2", "lighthouse-stack-packs": "1.12.3", "lodash-es": "^4.17.21", "lookup-closest-locale": "6.2.0", "open": "^8.4.0", "puppeteer-core": "^25.3.0", "robots-parser": "^3.0.1", "speedline-core": "^1.4.3", "third-party-web": "^0.29.2", "tldts-icann": "^7.4.9", "web-features": "^3.34.0", "ws": "^7.0.0", "yargs": "^17.3.1", "yargs-parser": "^21.0.0" }, "bin": { "lighthouse": "cli/index.js", "smokehouse": "cli/test/smokehouse/frontends/smokehouse-bin.js", "chrome-debug": "core/scripts/manual-chrome-launcher.js" } }, "sha512-fDu8lt3QLK/lTqIxtp1HkzQNJ32rsFHhbadYOepcMZFLgA8oINhxutMbMv8XXnpTOvZ0TXCo4JCk1LDTWaRLnA=="],
|
||||
|
||||
"lighthouse-logger": ["lighthouse-logger@2.0.2", "", { "dependencies": { "debug": "^4.4.1", "marky": "^1.2.2" } }, "sha512-vWl2+u5jgOQuZR55Z1WM0XDdrJT6mzMP8zHUct7xTlWhuQs+eV0g+QL0RQdFjT54zVmbhLCP8vIVpy1wGn/gCg=="],
|
||||
|
||||
"lighthouse-stack-packs": ["lighthouse-stack-packs@1.12.3", "", {}, "sha512-d8IsOpE83kbANgnM+Tp8+x6HcMpX9o2ITBiUERssgzAIFdZCQzs/f4k6D0DLQTE59enml9mbAOU52Wu35exWtg=="],
|
||||
|
||||
"lightningcss": ["lightningcss@1.33.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.33.0", "lightningcss-darwin-arm64": "1.33.0", "lightningcss-darwin-x64": "1.33.0", "lightningcss-freebsd-x64": "1.33.0", "lightningcss-linux-arm-gnueabihf": "1.33.0", "lightningcss-linux-arm64-gnu": "1.33.0", "lightningcss-linux-arm64-musl": "1.33.0", "lightningcss-linux-x64-gnu": "1.33.0", "lightningcss-linux-x64-musl": "1.33.0", "lightningcss-win32-arm64-msvc": "1.33.0", "lightningcss-win32-x64-msvc": "1.33.0" } }, "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA=="],
|
||||
|
||||
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.33.0", "", { "os": "android", "cpu": "arm64" }, "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg=="],
|
||||
|
|
@ -4547,6 +4614,8 @@
|
|||
|
||||
"longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="],
|
||||
|
||||
"lookup-closest-locale": ["lookup-closest-locale@6.2.0", "", {}, "sha512-/c2kL+Vnp1jnV6K6RpDTHK3dgg0Tu2VVp+elEiJpjfS1UyY7AjOYHohRug6wT0OpoX2qFgNORndE9RqesfVxWQ=="],
|
||||
|
||||
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
|
||||
|
||||
"loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="],
|
||||
|
|
@ -4579,6 +4648,8 @@
|
|||
|
||||
"marked-shiki": ["marked-shiki@1.2.1", "", { "peerDependencies": { "marked": ">=7.0.0", "shiki": ">=1.0.0" } }, "sha512-yHxYQhPY5oYaIRnROn98foKhuClark7M373/VpLxiy5TrDu9Jd/LsMwo8w+U91Up4oDb9IXFrP0N1MFRz8W/DQ=="],
|
||||
|
||||
"marky": ["marky@1.3.0", "", {}, "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ=="],
|
||||
|
||||
"matcher": ["matcher@3.0.0", "", { "dependencies": { "escape-string-regexp": "^4.0.0" } }, "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng=="],
|
||||
|
||||
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||
|
|
@ -4739,8 +4810,14 @@
|
|||
|
||||
"minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="],
|
||||
|
||||
"mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="],
|
||||
|
||||
"mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="],
|
||||
|
||||
"modern-tar": ["modern-tar@0.8.4", "", {}, "sha512-gN54ddmyzEg10orwZ2u4OOv+bjpMWdIl5jIkodK97bMq8QBSL5c0D7YX0lT1Ooz+99S7+PvFbnxzdjgHo1r41g=="],
|
||||
|
||||
"module-details-from-path": ["module-details-from-path@1.0.4", "", {}, "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w=="],
|
||||
|
||||
"motion": ["motion@12.34.5", "", { "dependencies": { "framer-motion": "^12.34.5", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-N06NLJ9IeBHeielRqIvYvjPfXuRdyTxa+9++BgpGa+hY2D7TcMkI6QzV3jaRuv0aZRXgMa7cPy9YcBUBisPzAQ=="],
|
||||
|
||||
"motion-dom": ["motion-dom@12.43.0", "", { "dependencies": { "motion-utils": "^12.39.0" } }, "sha512-azKON4d9S65PEoFUiQTMTgPheEmzf2QngdRc50AKfJp9Q9mmcBVw22c8eMq9k8kxOFHdL7+WZY7N/5F/lwiDag=="],
|
||||
|
|
@ -5051,6 +5128,8 @@
|
|||
|
||||
"pupa": ["pupa@3.3.0", "", { "dependencies": { "escape-goat": "^4.0.0" } }, "sha512-LjgDO2zPtoXP2wJpDjZrGdojii1uqO0cnwKoIoUzkfS98HDmbeiGmYiXo3lXeFlq2xvne1QFQhwYXSUCLKtEuA=="],
|
||||
|
||||
"puppeteer-core": ["puppeteer-core@25.9.0", "", { "dependencies": { "@puppeteer/browsers": "3.2.1", "chromium-bidi": "17.0.2", "devtools-protocol": "0.0.1666840", "typed-query-selector": "^2.12.2", "webdriver-bidi-protocol": "0.4.2", "ws": "^8.21.3" } }, "sha512-U61rCwSMha62CA/Opy6tCx2Fx+ck7ouiKnbpEApzSoLYMoEu9F71nuFpHL55vmIt33/GYm6eKZVhH2ev0nAIeg=="],
|
||||
|
||||
"pure-rand": ["pure-rand@8.4.2", "", {}, "sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng=="],
|
||||
|
||||
"pvtsutils": ["pvtsutils@1.3.6", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg=="],
|
||||
|
|
@ -5177,6 +5256,8 @@
|
|||
|
||||
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
|
||||
|
||||
"require-in-the-middle": ["require-in-the-middle@8.0.1", "", { "dependencies": { "debug": "^4.3.5", "module-details-from-path": "^1.0.3" } }, "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ=="],
|
||||
|
||||
"resedit": ["resedit@1.7.2", "", { "dependencies": { "pe-library": "^0.4.1" } }, "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA=="],
|
||||
|
||||
"reselect": ["reselect@4.1.8", "", {}, "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ=="],
|
||||
|
|
@ -5211,6 +5292,8 @@
|
|||
|
||||
"roarr": ["roarr@2.15.4", "", { "dependencies": { "boolean": "^3.0.1", "detect-node": "^2.0.4", "globalthis": "^1.0.1", "json-stringify-safe": "^5.0.1", "semver-compare": "^1.0.0", "sprintf-js": "^1.1.2" } }, "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A=="],
|
||||
|
||||
"robots-parser": ["robots-parser@3.0.1", "", {}, "sha512-s+pyvQeIKIZ0dx5iJiQk1tPLJAWln39+MI5jtM8wnyws+G5azk+dMnMX0qfbqNetKKNgcWWOdi0sfm+FbQbgdQ=="],
|
||||
|
||||
"robust-predicates": ["robust-predicates@3.0.3", "", {}, "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA=="],
|
||||
|
||||
"rolldown": ["rolldown@1.2.5", "", { "dependencies": { "@oxc-project/types": "=0.146.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm-eabi": "1.2.5", "@rolldown/binding-android-arm64": "1.2.5", "@rolldown/binding-darwin-arm64": "1.2.5", "@rolldown/binding-darwin-x64": "1.2.5", "@rolldown/binding-freebsd-x64": "1.2.5", "@rolldown/binding-linux-arm-gnueabihf": "1.2.5", "@rolldown/binding-linux-arm64-gnu": "1.2.5", "@rolldown/binding-linux-arm64-musl": "1.2.5", "@rolldown/binding-linux-ppc64-gnu": "1.2.5", "@rolldown/binding-linux-s390x-gnu": "1.2.5", "@rolldown/binding-linux-x64-gnu": "1.2.5", "@rolldown/binding-linux-x64-musl": "1.2.5", "@rolldown/binding-openharmony-arm64": "1.2.5", "@rolldown/binding-win32-arm64-msvc": "1.2.5", "@rolldown/binding-win32-x64-msvc": "1.2.5" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-VD2IE5PUG4Oj8zz2VGykiYd5wbnjdIiSsNQb8Qu5B+noEp+A78mu2iVvpp27g8es14Tk9rofNs5Tku9iQCS4fA=="],
|
||||
|
|
@ -5369,6 +5452,8 @@
|
|||
|
||||
"spdx-license-ids": ["spdx-license-ids@3.0.23", "", {}, "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw=="],
|
||||
|
||||
"speedline-core": ["speedline-core@1.4.3", "", { "dependencies": { "@types/node": "*", "image-ssim": "^0.2.0", "jpeg-js": "^0.4.1" } }, "sha512-DI7/OuAUD+GMpR6dmu8lliO2Wg5zfeh+/xsdyJZCzd8o5JgFUjCeLsBDuZjIQJdwXS3J0L/uZYrELKYqx+PXog=="],
|
||||
|
||||
"sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="],
|
||||
|
||||
"sqlstring": ["sqlstring@2.3.3", "", {}, "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg=="],
|
||||
|
|
@ -5503,6 +5588,8 @@
|
|||
|
||||
"thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="],
|
||||
|
||||
"third-party-web": ["third-party-web@0.29.2", "", {}, "sha512-fegtha91tq2DHphyoiBXVHjVi2YG9zFaRnboT9C28tO1en9Y3wJsfspuy40F+u5wl3hHVbw7cnd1b67kEGHb8g=="],
|
||||
|
||||
"tiny-async-pool": ["tiny-async-pool@1.3.0", "", { "dependencies": { "semver": "^5.5.0" } }, "sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA=="],
|
||||
|
||||
"tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="],
|
||||
|
|
@ -5525,6 +5612,10 @@
|
|||
|
||||
"titleize": ["titleize@4.0.0", "", {}, "sha512-ZgUJ1K83rhdu7uh7EHAC2BgY5DzoX8V5rTvoWI4vFysggi6YjLe5gUXABPWAU7VkvGP7P/0YiWq+dcPeYDsf1g=="],
|
||||
|
||||
"tldts-core": ["tldts-core@7.4.11", "", {}, "sha512-CW3WN2rIIE/Of21mulhgnGOwoDyEFNygyIBOONSdyAuSATgMMUCpLeUlB+E8sAwA5xRV9hYPl+kyZ9citHCaKg=="],
|
||||
|
||||
"tldts-icann": ["tldts-icann@7.4.11", "", { "dependencies": { "tldts-core": "^7.4.11" } }, "sha512-1p+NDJ7FUYCliESmsQl9EW5Um8JIyFheiy6Y6ZoHho27d+TLAGFa0gMf1VOVy02vNY1aQo6xeKiaGj45+7P+PA=="],
|
||||
|
||||
"tmp": ["tmp@0.2.7", "", {}, "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw=="],
|
||||
|
||||
"tmp-promise": ["tmp-promise@3.0.3", "", { "dependencies": { "tmp": "^0.2.0" } }, "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ=="],
|
||||
|
|
@ -5587,6 +5678,8 @@
|
|||
|
||||
"typed-array-length": ["typed-array-length@1.0.8", "", { "dependencies": { "call-bind": "^1.0.9", "for-each": "^0.3.5", "gopd": "^1.2.0", "is-typed-array": "^1.1.15", "possible-typed-array-names": "^1.1.0", "reflect.getprototypeof": "^1.0.10" } }, "sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g=="],
|
||||
|
||||
"typed-query-selector": ["typed-query-selector@2.12.2", "", {}, "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ=="],
|
||||
|
||||
"typesafe-path": ["typesafe-path@0.2.2", "", {}, "sha512-OJabfkAg1WLZSqJAJ0Z6Sdt3utnbzr/jh+NAHoyWHJe8CMSy79Gm085094M9nvTPy22KzTVn5Zq5mbapCI/hPA=="],
|
||||
|
||||
"typescript": ["typescript@5.8.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="],
|
||||
|
|
@ -5747,6 +5840,8 @@
|
|||
|
||||
"walk-up-path": ["walk-up-path@4.0.0", "", {}, "sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A=="],
|
||||
|
||||
"web-features": ["web-features@3.36.0", "", {}, "sha512-B5W9HbyXT76soRG7Fn3AELZkuDQpmpSxszSmJZLIPUHYxIGkufNz0vcoEhfqnUF1HAfaducWIBaaKuusq9Am2g=="],
|
||||
|
||||
"web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="],
|
||||
|
||||
"web-streams-polyfill": ["web-streams-polyfill@4.0.0-beta.3", "", {}, "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug=="],
|
||||
|
|
@ -5755,6 +5850,8 @@
|
|||
|
||||
"webcrypto-core": ["webcrypto-core@1.9.2", "", { "dependencies": { "@peculiar/asn1-schema": "^2.7.0", "@peculiar/json-schema": "^1.1.12", "@peculiar/utils": "^2.0.2", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-gsXecm82UQNlTBURJGuqOWy1Ww08S3kZUcr3aOJS02Pk0xLtkfeUAVC0u0xhgdonFme80edSJUIJyuvL/7250Q=="],
|
||||
|
||||
"webdriver-bidi-protocol": ["webdriver-bidi-protocol@0.4.2", "", {}, "sha512-VSV+fzfChirL3e7jay2yUC7B4HQCGtEWEg/MSSQbK+qWbqeGlRLlXTzPpYr3XGUvbpDHumWZBJxgesg4N7dbtA=="],
|
||||
|
||||
"webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
|
||||
|
||||
"webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="],
|
||||
|
|
@ -5831,6 +5928,8 @@
|
|||
|
||||
"wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="],
|
||||
|
||||
"xdg-basedir": ["xdg-basedir@5.1.0", "", {}, "sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ=="],
|
||||
|
||||
"xml-naming": ["xml-naming@0.3.0", "", {}, "sha512-ghig2TBE/H11aOVgmahA3MhimvkBr6JIYknH/Dhdk10nXwdbIqBJsbfMxpvFPG8bAw77gN29aQWvKpmVoPlvPQ=="],
|
||||
|
||||
"xmlbuilder": ["xmlbuilder@15.1.1", "", {}, "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg=="],
|
||||
|
|
@ -6303,6 +6402,12 @@
|
|||
|
||||
"@opencode-ai/www/wrangler": ["wrangler@4.110.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.5.0", "@cloudflare/unenv-preset": "2.16.1", "blake3-wasm": "2.1.5", "esbuild": "0.28.1", "miniflare": "4.20260708.1", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20260708.1" }, "optionalDependencies": { "fsevents": "2.3.3" }, "peerDependencies": { "@cloudflare/workers-types": "^5.20260708.1" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js", "cf-wrangler": "bin/cf-wrangler.js" } }, "sha512-xZeXKYi7hxQRF5anL+v77RkufJNpF9f3Eqeyqq2QBsETpLZgh0Agj0jJ6JPtkbgn6ukZdh8OK5egsGPWIditgg=="],
|
||||
|
||||
"@opentelemetry/instrumentation/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.220.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-CmVa4ImJ+ynfrPMNaAXHET6Bhb44SwzmfyVJFq9ni2jgXJR/l7C6gfVFddNmHP+ZOkP9cf4f9DBe68qVLTHc9w=="],
|
||||
|
||||
"@opentelemetry/sdk-trace/@opentelemetry/core": ["@opentelemetry/core@2.10.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-/wNZ8twnEQQA4HoHu22+vcsdru6pWPWxW+7w+FlxT6Id7PE/WIbZmVKkte+PF72e0F2dnImFeHD2syyE1Mw6MQ=="],
|
||||
|
||||
"@opentelemetry/sdk-trace/@opentelemetry/resources": ["@opentelemetry/resources@2.10.0", "", { "dependencies": { "@opentelemetry/core": "2.10.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-q6MMm2zhggzsHVNbabYwut+a6nbuQQe3URUoxaojM/8K1IBfwwPzvxIjNi2/lI1TFe+fMHMW9MWhrtDLEXEnkA=="],
|
||||
|
||||
"@opentui/core/diff": ["diff@9.0.0", "", {}, "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw=="],
|
||||
|
||||
"@opentui/core/marked": ["marked@17.0.1", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg=="],
|
||||
|
|
@ -6335,6 +6440,8 @@
|
|||
|
||||
"@protobuf-ts/plugin/typescript": ["typescript@3.9.10", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q=="],
|
||||
|
||||
"@puppeteer/browsers/yargs": ["yargs@18.1.0", "", { "dependencies": { "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "string-width": "^8.2.1", "y18n": "^5.0.5", "yargs-parser": "^22.0.0" } }, "sha512-2rAgRKu54VsHkqI0/tYkmluGXHD4KW7yZoycuqDQ15QOTnc2VVfy0nN/1eMhnQLO00A+dwtK20xuCnc1YGeUyg=="],
|
||||
|
||||
"@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
||||
|
||||
"@sentry/bundler-plugins/dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="],
|
||||
|
|
@ -6345,6 +6452,18 @@
|
|||
|
||||
"@sentry/cli/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
|
||||
"@sentry/node/@opentelemetry/api": ["@opentelemetry/api@1.9.1", "", {}, "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q=="],
|
||||
|
||||
"@sentry/node/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.10.0", "", { "dependencies": { "@opentelemetry/core": "2.10.0", "@opentelemetry/resources": "2.10.0", "@opentelemetry/sdk-trace": "2.10.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-GuYQQT7QD2EeO8lcZLRQzcbOyhqAzL+6WWTKTU9mSUBYBazkEDl+VrQcXQhbB08OWM9anD1aHleVadzulpOaUQ=="],
|
||||
|
||||
"@sentry/node/@sentry/core": ["@sentry/core@10.72.0", "", { "dependencies": { "@sentry/conventions": "^0.16.0" } }, "sha512-UJMHZfbjP4qk+g4AQhZmzosMdICC2D9p0/hLrm1LofPsp+WBfcSnv9jsY1a9TfmpS4WCAmTx8nANYTIXifcBjA=="],
|
||||
|
||||
"@sentry/node-core/@sentry/core": ["@sentry/core@10.72.0", "", { "dependencies": { "@sentry/conventions": "^0.16.0" } }, "sha512-UJMHZfbjP4qk+g4AQhZmzosMdICC2D9p0/hLrm1LofPsp+WBfcSnv9jsY1a9TfmpS4WCAmTx8nANYTIXifcBjA=="],
|
||||
|
||||
"@sentry/opentelemetry/@sentry/core": ["@sentry/core@10.72.0", "", { "dependencies": { "@sentry/conventions": "^0.16.0" } }, "sha512-UJMHZfbjP4qk+g4AQhZmzosMdICC2D9p0/hLrm1LofPsp+WBfcSnv9jsY1a9TfmpS4WCAmTx8nANYTIXifcBjA=="],
|
||||
|
||||
"@sentry/server-utils/@sentry/core": ["@sentry/core@10.72.0", "", { "dependencies": { "@sentry/conventions": "^0.16.0" } }, "sha512-UJMHZfbjP4qk+g4AQhZmzosMdICC2D9p0/hLrm1LofPsp+WBfcSnv9jsY1a9TfmpS4WCAmTx8nANYTIXifcBjA=="],
|
||||
|
||||
"@shikijs/transformers/@shikijs/core": ["@shikijs/core@3.20.0", "", { "dependencies": { "@shikijs/types": "3.20.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-f2ED7HYV4JEk827mtMDwe/yQ25pRiXZmtHjWF8uzZKuKiEsJR7Ce1nuQ+HhV9FzDcbIo4ObBCD9GPTzNuy9S1g=="],
|
||||
|
||||
"@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.20.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-lhYAATn10nkZcBQ0BlzSbJA3wcmL5MXUUF8d2Zzon6saZDlToKaiRX60n2+ZaHJCmXEcZRWNzn+k9vplr8Jhsw=="],
|
||||
|
|
@ -6453,6 +6572,10 @@
|
|||
|
||||
"builder-util/js-yaml": ["js-yaml@4.3.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ=="],
|
||||
|
||||
"chrome-launcher/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="],
|
||||
|
||||
"chromium-bidi/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||
|
||||
"clean-css/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||
|
||||
"cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
|
|
@ -6471,6 +6594,8 @@
|
|||
|
||||
"config-chain/ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="],
|
||||
|
||||
"configstore/dot-prop": ["dot-prop@9.0.0", "", { "dependencies": { "type-fest": "^4.18.2" } }, "sha512-1gxPBJpI/pcjQhKgIU91II6Wkay+dLcN3M6rf2uwP8hRur3HtQXjVrdAK3sjC0piaEuxzMwjXChcETiJl47lAQ=="],
|
||||
|
||||
"cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
|
||||
"csso/css-tree": ["css-tree@2.2.1", "", { "dependencies": { "mdn-data": "2.0.28", "source-map-js": "^1.0.1" } }, "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA=="],
|
||||
|
|
@ -6515,6 +6640,8 @@
|
|||
|
||||
"electron-winstaller/fs-extra": ["fs-extra@7.0.1", "", { "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw=="],
|
||||
|
||||
"enquirer/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"esast-util-from-js/acorn": ["acorn@8.18.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="],
|
||||
|
||||
"esbuild-plugin-copy/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
|
||||
|
|
@ -6559,6 +6686,8 @@
|
|||
|
||||
"htmlparser2/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
|
||||
|
||||
"import-in-the-middle/es-module-lexer": ["es-module-lexer@2.3.2", "", {}, "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw=="],
|
||||
|
||||
"js-beautify/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="],
|
||||
|
||||
"js-beautify/nopt": ["nopt@7.2.1", "", { "dependencies": { "abbrev": "^2.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w=="],
|
||||
|
|
@ -6567,10 +6696,16 @@
|
|||
|
||||
"lazystream/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
|
||||
|
||||
"matcher/escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
||||
"lighthouse/devtools-protocol": ["devtools-protocol@0.0.1663043", "", {}, "sha512-33aOY3ZnBP1dgZsshgaL+/XlsQleiFZgyUaDtdZkEa1nbZhVY1MoDeWjk+wxg25fU924l1ZJfoGNmjjeA/5s1w=="],
|
||||
|
||||
"lighthouse/open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="],
|
||||
|
||||
"lighthouse/ws": ["ws@7.5.13", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA=="],
|
||||
|
||||
"md-to-react-email/marked": ["marked@7.0.4", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-t8eP0dXRJMtMvBojtkcsA7n48BkauktUKzfkPSCq85ZMTJ0v76Rke4DYz01omYpPTUh4p/f7HePgRo3ebG8+QQ=="],
|
||||
|
||||
"mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
|
||||
|
||||
"mermaid/marked": ["marked@16.4.2", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA=="],
|
||||
|
||||
"micromark-extension-mdxjs/acorn": ["acorn@8.18.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="],
|
||||
|
|
@ -6635,6 +6770,10 @@
|
|||
|
||||
"prompts/kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="],
|
||||
|
||||
"puppeteer-core/devtools-protocol": ["devtools-protocol@0.0.1666840", "", {}, "sha512-gCcO42XCHKEs7Ag0S7aGYsnJ7hlgrO3qderYqeiY0Eqk+0GFfuvT13IA0hHreJTa2KCdDVyGMeOhdMNmrrTjVg=="],
|
||||
|
||||
"puppeteer-core/ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="],
|
||||
|
||||
"readdir-glob/minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="],
|
||||
|
||||
"recast/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||
|
|
@ -6703,6 +6842,8 @@
|
|||
|
||||
"unplugin/acorn": ["acorn@8.18.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="],
|
||||
|
||||
"unused-filename/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
|
||||
|
||||
"unused-filename/path-exists": ["path-exists@5.0.0", "", {}, "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ=="],
|
||||
|
||||
"unzipper/fs-extra": ["fs-extra@11.3.1", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g=="],
|
||||
|
|
@ -7221,6 +7362,8 @@
|
|||
|
||||
"@opencode-ai/www/wrangler/workerd": ["workerd@1.20260708.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260708.1", "@cloudflare/workerd-darwin-arm64": "1.20260708.1", "@cloudflare/workerd-linux-64": "1.20260708.1", "@cloudflare/workerd-linux-arm64": "1.20260708.1", "@cloudflare/workerd-windows-64": "1.20260708.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-WAK+Kt/VVCSldH2qSr8lx46XCJ4Q+bdlHNaFqUtOHthBEIB8C1N8HVW+VOLrxDoTCk0NGNv0zajnBeQK4JOB9w=="],
|
||||
|
||||
"@opentelemetry/instrumentation/@opentelemetry/api-logs/@opentelemetry/api": ["@opentelemetry/api@1.9.1", "", {}, "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q=="],
|
||||
|
||||
"@opentui/solid/@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"@oxc-resolver/binding-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="],
|
||||
|
|
@ -7229,10 +7372,20 @@
|
|||
|
||||
"@pierre/trees/react-dom/scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
||||
|
||||
"@puppeteer/browsers/yargs/cliui": ["cliui@9.0.1", "", { "dependencies": { "string-width": "^7.2.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w=="],
|
||||
|
||||
"@puppeteer/browsers/yargs/string-width": ["string-width@8.2.2", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg=="],
|
||||
|
||||
"@puppeteer/browsers/yargs/yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="],
|
||||
|
||||
"@sentry/cli/https-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="],
|
||||
|
||||
"@sentry/cli/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
|
||||
"@sentry/node/@opentelemetry/sdk-trace-base/@opentelemetry/core": ["@opentelemetry/core@2.10.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-/wNZ8twnEQQA4HoHu22+vcsdru6pWPWxW+7w+FlxT6Id7PE/WIbZmVKkte+PF72e0F2dnImFeHD2syyE1Mw6MQ=="],
|
||||
|
||||
"@sentry/node/@opentelemetry/sdk-trace-base/@opentelemetry/resources": ["@opentelemetry/resources@2.10.0", "", { "dependencies": { "@opentelemetry/core": "2.10.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-q6MMm2zhggzsHVNbabYwut+a6nbuQQe3URUoxaojM/8K1IBfwwPzvxIjNi2/lI1TFe+fMHMW9MWhrtDLEXEnkA=="],
|
||||
|
||||
"@solidjs/start/shiki/@shikijs/core": ["@shikijs/core@1.29.2", "", { "dependencies": { "@shikijs/engine-javascript": "1.29.2", "@shikijs/engine-oniguruma": "1.29.2", "@shikijs/types": "1.29.2", "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.4" } }, "sha512-vju0lY9r27jJfOY4Z7+Rt/nIOjzJpZ3y+nYpqtUZInVoXQ/TJZcfGnNOGnKjFdVZb8qexiCuSlZRKcGfhhTTZQ=="],
|
||||
|
||||
"@solidjs/start/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@1.29.2", "", { "dependencies": { "@shikijs/types": "1.29.2", "@shikijs/vscode-textmate": "^10.0.1", "oniguruma-to-es": "^2.2.0" } }, "sha512-iNEZv4IrLYPv64Q6k7EPpOCE/nuvGiKl7zxdq0WFuRPF5PAE9PRo2JGq/d8crLusM59BRemJ4eOqrFrC4wiQ+A=="],
|
||||
|
|
@ -7401,6 +7554,8 @@
|
|||
|
||||
"builder-util/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
||||
|
||||
"chrome-launcher/is-wsl/is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="],
|
||||
|
||||
"cliui/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
|
@ -7437,6 +7592,8 @@
|
|||
|
||||
"electron/@electron/get/undici": ["undici@7.29.0", "", {}, "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw=="],
|
||||
|
||||
"enquirer/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"esbuild-plugin-copy/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
|
||||
|
||||
"filelist/minimatch/brace-expansion": ["brace-expansion@2.1.4", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg=="],
|
||||
|
|
@ -7459,6 +7616,12 @@
|
|||
|
||||
"lazystream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
|
||||
|
||||
"lighthouse/open/define-lazy-prop": ["define-lazy-prop@2.0.0", "", {}, "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og=="],
|
||||
|
||||
"lighthouse/open/is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="],
|
||||
|
||||
"lighthouse/open/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.0.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.0.4" }, "os": "darwin", "cpu": "x64" }, "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q=="],
|
||||
|
|
@ -8203,6 +8366,8 @@
|
|||
|
||||
"@opencode-ai/www/wrangler/workerd/@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260708.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bC/aSAwLy16Vjo24i9XU3aWH+eRgz7NeR5xPKavGbembO18ZywYTQbXh14eXtY6fAqN3RzRG8psijTdhX4xydA=="],
|
||||
|
||||
"@puppeteer/browsers/yargs/cliui/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
|
||||
|
||||
"@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es": ["oniguruma-to-es@2.3.0", "", { "dependencies": { "emoji-regex-xs": "^1.0.0", "regex": "^5.1.1", "regex-recursion": "^5.1.1" } }, "sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g=="],
|
||||
|
||||
"ansi-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
|
|
|||
|
|
@ -2,11 +2,17 @@ import type { Browser } from "@opencode-ai/schema/browser"
|
|||
|
||||
export type BrowserPaneEndpoint = Readonly<{ url: string; username?: string; password?: string }>
|
||||
export type BrowserPaneTarget = Readonly<{ sessionID: string; endpoint: BrowserPaneEndpoint }>
|
||||
export type BrowserPaneLayout = { visible: boolean; bounds?: { x: number; y: number; width: number; height: number } }
|
||||
export type BrowserPaneLayout = {
|
||||
tabID: Browser.TabID
|
||||
visible: boolean
|
||||
bounds?: { x: number; y: number; width: number; height: number }
|
||||
}
|
||||
|
||||
export type BrowserPaneCommand = Browser.Action
|
||||
export type BrowserPaneState = Browser.State | null
|
||||
export type BrowserPaneEvent = { type: "open" } | { type: "state"; state: BrowserPaneState; error?: string }
|
||||
export type BrowserPaneEvent =
|
||||
| { type: "focus"; tabID: Browser.TabID }
|
||||
| { type: "state"; state: BrowserPaneState; error?: string }
|
||||
|
||||
export type BrowserPaneRegistration = {
|
||||
setLayout(layout?: BrowserPaneLayout): void
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
import { createEffect, createMemo, onCleanup } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { batch, createEffect, createMemo, on, onCleanup } from "solid-js"
|
||||
import type { Browser } from "@opencode-ai/schema/browser"
|
||||
import { createStore, reconcile } from "solid-js/store"
|
||||
import { useLanguage } from "@/runtime/i18n/language"
|
||||
import type { BrowserPaneCommand, BrowserPaneRegistration, BrowserPaneState } from "@/runtime/platform/browser-pane"
|
||||
import { usePlatform } from "@/runtime/platform/platform"
|
||||
import { useServer } from "@/runtime/server/current"
|
||||
import { useSettings } from "@/settings/model"
|
||||
import type { SessionModel } from "../model"
|
||||
import { SESSION_BROWSER_TAB } from "../helpers"
|
||||
import { isSessionBrowserTab, sessionBrowserTab } from "../helpers"
|
||||
|
||||
export function createSessionBrowser(session: SessionModel) {
|
||||
const platform = usePlatform()
|
||||
|
|
@ -30,12 +31,44 @@ export function createSessionBrowser(session: SessionModel) {
|
|||
!server.health?.incompatible &&
|
||||
!state.unsupported,
|
||||
)
|
||||
const opened = () => state.registration !== undefined && session.layout.tabs().all().includes(SESSION_BROWSER_TAB)
|
||||
const open = () => {
|
||||
const browserTabs = createMemo(() => state.browser?.tabs.filter((tab) => session.layout.tabs().all().includes(sessionBrowserTab(tab.id))) ?? [])
|
||||
const opened = () => state.registration !== undefined && browserTabs().length > 0
|
||||
const focus = (tabID: Browser.TabID) => {
|
||||
session.layout.view().reviewPanel.open()
|
||||
void session.layout.tabs().open(SESSION_BROWSER_TAB)
|
||||
session.layout.tabs().setActive(SESSION_BROWSER_TAB)
|
||||
const tabs = session.layout.tabs()
|
||||
const key = sessionBrowserTab(tabID)
|
||||
if (!tabs.all().includes(key)) tabs.setAll([...tabs.all(), key])
|
||||
tabs.setActive(key)
|
||||
}
|
||||
const command = (command: BrowserPaneCommand) => {
|
||||
setState("error", undefined)
|
||||
const owner = session.ownership.capture()
|
||||
void state.registration?.command(command).catch(() => {
|
||||
if (owner.current()) setState("error", language.t("common.requestFailed"))
|
||||
})
|
||||
}
|
||||
createEffect(
|
||||
on(
|
||||
() => session.layout.tabs().active(),
|
||||
(active) => {
|
||||
const tab = state.browser?.tabs.find((tab) => sessionBrowserTab(tab.id) === active)
|
||||
if (tab && tab.id !== state.browser?.focusedTabID) command({ type: "tabs.focus", tabID: tab.id })
|
||||
},
|
||||
),
|
||||
)
|
||||
createEffect(
|
||||
on(
|
||||
() => session.layout.tabs().all(),
|
||||
(current, previous) => {
|
||||
previous
|
||||
?.filter((key) => isSessionBrowserTab(key) && !current.includes(key))
|
||||
.forEach((key) => {
|
||||
const tab = state.browser?.tabs.find((tab) => sessionBrowserTab(tab.id) === key)
|
||||
if (tab) command({ type: "tabs.close", tabID: tab.id })
|
||||
})
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
createEffect(() => {
|
||||
const sessionID = session.identity.sessionID()
|
||||
|
|
@ -51,7 +84,7 @@ export function createSessionBrowser(session: SessionModel) {
|
|||
if (registration) return
|
||||
registration = pane.register(target, (event) =>
|
||||
owner.run(() => {
|
||||
if (event.type === "open") return open()
|
||||
if (event.type === "focus") return focus(event.tabID)
|
||||
if (event.error === "browser.pane.unsupported") return setState("unsupported", true)
|
||||
// The desktop dropped the attachment (server restart, attach race).
|
||||
// Re-register so the agent's browser tool comes back without a reload.
|
||||
|
|
@ -63,7 +96,20 @@ export function createSessionBrowser(session: SessionModel) {
|
|||
return
|
||||
}
|
||||
if (event.state) attempts = 0
|
||||
setState({ browser: event.state, error: event.error })
|
||||
batch(() => {
|
||||
const known = new Set(state.browser?.tabs.map((tab) => sessionBrowserTab(tab.id)) ?? [])
|
||||
setState("browser", reconcile(event.state))
|
||||
setState("error", event.error ? language.t("common.requestFailed") : undefined)
|
||||
const tabs = session.layout.tabs()
|
||||
const ids = event.state?.tabs.map((tab) => sessionBrowserTab(tab.id)) ?? []
|
||||
tabs
|
||||
.all()
|
||||
.filter((key) => isSessionBrowserTab(key) && !ids.includes(key))
|
||||
.forEach(tabs.close)
|
||||
const current = tabs.all()
|
||||
const added = ids.filter((key) => !known.has(key) && !current.includes(key))
|
||||
if (added.length) tabs.setAll([...current, ...added])
|
||||
})
|
||||
}),
|
||||
)
|
||||
setState({ registration, browser: null, error: undefined })
|
||||
|
|
@ -84,17 +130,14 @@ export function createSessionBrowser(session: SessionModel) {
|
|||
available,
|
||||
opened,
|
||||
state: () => state.browser,
|
||||
tabs: browserTabs,
|
||||
active: () =>
|
||||
browserTabs().find((tab) => sessionBrowserTab(tab.id) === session.layout.tabs().active()) ??
|
||||
browserTabs().find((tab) => tab.id === state.browser?.focusedTabID) ?? browserTabs()[0],
|
||||
error: () => state.error,
|
||||
registration: () => state.registration,
|
||||
close: () => session.layout.tabs().close(SESSION_BROWSER_TAB),
|
||||
open,
|
||||
command(command: BrowserPaneCommand) {
|
||||
setState("error", undefined)
|
||||
const owner = session.ownership.capture()
|
||||
void state.registration?.command(command).catch((error: unknown) => {
|
||||
if (!owner.current()) return
|
||||
setState("error", error instanceof Error ? error.message : language.t("common.requestFailed"))
|
||||
})
|
||||
},
|
||||
close: (tabID: Browser.TabID) => session.layout.tabs().close(sessionBrowserTab(tabID)),
|
||||
open: () => command({ type: "tabs.open" }),
|
||||
command,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ export function SessionBrowserPane(props: {
|
|||
const platform = usePlatform()
|
||||
const language = useLanguage()
|
||||
const dialog = useDialog()
|
||||
const state = props.browser.state
|
||||
const state = props.browser.active
|
||||
const button = { variant: "ghost", size: "large" } as const
|
||||
const [store, setStore] = createStore({
|
||||
address: "",
|
||||
|
|
@ -41,6 +41,11 @@ export function SessionBrowserPane(props: {
|
|||
const measure = () => {
|
||||
frame = undefined
|
||||
if (!surface) return
|
||||
const tab = state()
|
||||
if (!tab) {
|
||||
props.registration.setLayout()
|
||||
return
|
||||
}
|
||||
const rect = surface.getBoundingClientRect()
|
||||
const zoom = platform.webviewZoom?.() ?? 1
|
||||
const left = Math.round(rect.left * zoom)
|
||||
|
|
@ -48,10 +53,11 @@ export function SessionBrowserPane(props: {
|
|||
const right = Math.round(rect.right * zoom)
|
||||
const bottom = Math.round(rect.bottom * zoom)
|
||||
const visible = props.visible && store.visible && !dialog.active && !covered(rect)
|
||||
const next = `${visible}:${left}:${top}:${right}:${bottom}`
|
||||
const next = `${tab.id}:${visible}:${left}:${top}:${right}:${bottom}`
|
||||
if (next !== layout) {
|
||||
layout = next
|
||||
props.registration.setLayout({
|
||||
tabID: tab.id,
|
||||
visible,
|
||||
bounds: { x: left, y: top, width: Math.max(0, right - left), height: Math.max(0, bottom - top) },
|
||||
})
|
||||
|
|
@ -65,8 +71,15 @@ export function SessionBrowserPane(props: {
|
|||
|
||||
createEffect(() => !store.editing && setStore("address", state()?.url ?? ""))
|
||||
createEffect(
|
||||
on([() => platform.webviewZoom?.(), () => dialog.active, () => store.visible, () => props.visible], () =>
|
||||
schedule(300),
|
||||
on(
|
||||
[
|
||||
() => platform.webviewZoom?.(),
|
||||
() => dialog.active,
|
||||
() => store.visible,
|
||||
() => props.visible,
|
||||
() => state()?.id,
|
||||
],
|
||||
() => schedule(300),
|
||||
),
|
||||
)
|
||||
createResizeObserver(() => surface, schedule.bind(null, 0))
|
||||
|
|
@ -91,7 +104,10 @@ export function SessionBrowserPane(props: {
|
|||
{...button}
|
||||
disabled={!state()?.[direction === "back" ? "canGoBack" : "canGoForward"]}
|
||||
aria-label={language.t(direction === "back" ? "common.goBack" : "common.goForward")}
|
||||
onClick={() => props.browser.command({ type: direction })}
|
||||
onClick={() => {
|
||||
const tab = state()
|
||||
if (tab) props.browser.command({ type: direction, tabID: tab.id })
|
||||
}}
|
||||
icon={<Icon name={direction === "back" ? "chevron-left" : "chevron-right"} size="small" />}
|
||||
/>
|
||||
)}
|
||||
|
|
@ -100,7 +116,10 @@ export function SessionBrowserPane(props: {
|
|||
{...button}
|
||||
disabled={!state()}
|
||||
aria-label={language.t(state()?.loading ? "prompt.action.stop" : "error.page.action.reload")}
|
||||
onClick={() => props.browser.command(state()?.loading ? { type: "stop" } : { type: "reload" })}
|
||||
onClick={() => {
|
||||
const tab = state()
|
||||
if (tab) props.browser.command({ type: tab.loading ? "stop" : "reload", tabID: tab.id })
|
||||
}}
|
||||
icon={
|
||||
<Show when={state()?.loading} fallback={<Icon name="reset" size="small" />}>
|
||||
<Loader />
|
||||
|
|
@ -111,7 +130,9 @@ export function SessionBrowserPane(props: {
|
|||
class="min-w-0 flex-1"
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault()
|
||||
if (store.address.trim()) props.browser.command({ type: "navigate", url: store.address })
|
||||
const tab = state()
|
||||
if (tab && store.address.trim())
|
||||
props.browser.command({ type: "navigate", tabID: tab.id, url: store.address })
|
||||
}}
|
||||
>
|
||||
<input
|
||||
|
|
|
|||
|
|
@ -36,7 +36,8 @@ import { useSettings } from "@/settings/model"
|
|||
import { createFileTabListSync } from "@/session/files/file-tab-scroll"
|
||||
import {
|
||||
SESSION_OPEN_FILE_TAB,
|
||||
SESSION_BROWSER_TAB,
|
||||
isSessionBrowserTab,
|
||||
sessionBrowserTab,
|
||||
createOpenSessionFileTab,
|
||||
createSessionTabs,
|
||||
shouldShowFileTree,
|
||||
|
|
@ -224,7 +225,7 @@ export function SessionSidePanel(props: {
|
|||
})
|
||||
const fileBrowserVisible = createMemo(() => {
|
||||
const active = activeTab()
|
||||
return active !== "review" && active !== "context" && active !== "empty" && active !== SESSION_BROWSER_TAB
|
||||
return active !== "review" && active !== "context" && active !== "empty" && !isSessionBrowserTab(active)
|
||||
})
|
||||
const openFileKeybind = createMemo(() => command.keybindParts("file.open"))
|
||||
const closeTabKeybind = createMemo(() => command.keybindParts("file.close"))
|
||||
|
|
@ -411,28 +412,34 @@ export function SessionSidePanel(props: {
|
|||
</Show>
|
||||
)}
|
||||
</For>
|
||||
<Show when={props.browser.opened()}>
|
||||
<Tabs.Trigger
|
||||
value={SESSION_BROWSER_TAB}
|
||||
id={browserTabID}
|
||||
aria-controls={activeTab() === SESSION_BROWSER_TAB ? browserTabPanelID : undefined}
|
||||
onMiddleClick={props.browser.close}
|
||||
closeButton={
|
||||
<Tooltip value={language.t("common.closeTab")} placement="bottom" gutter={10}>
|
||||
<Tabs.CloseButton
|
||||
onClick={props.browser.close}
|
||||
aria-label={language.t("common.closeTab")}
|
||||
/>
|
||||
</Tooltip>
|
||||
}
|
||||
hideCloseButton
|
||||
>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<Icon name="window-cursor" size="small" />
|
||||
<span>{language.t("session.tab.browser")}</span>
|
||||
</div>
|
||||
</Tabs.Trigger>
|
||||
</Show>
|
||||
<For each={props.browser.tabs()}>
|
||||
{(tab) => (
|
||||
<Tabs.Trigger
|
||||
value={sessionBrowserTab(tab.id)}
|
||||
id={`${browserTabID}-${tab.id}`}
|
||||
aria-controls={
|
||||
activeTab() === sessionBrowserTab(tab.id) ? browserTabPanelID : undefined
|
||||
}
|
||||
onMiddleClick={() => props.browser.close(tab.id)}
|
||||
closeButton={
|
||||
<Tooltip value={language.t("common.closeTab")} placement="bottom" gutter={10}>
|
||||
<Tabs.CloseButton
|
||||
onClick={() => props.browser.close(tab.id)}
|
||||
aria-label={language.t("common.closeTab")}
|
||||
/>
|
||||
</Tooltip>
|
||||
}
|
||||
hideCloseButton
|
||||
>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<Icon name="window-cursor" size="small" />
|
||||
<span class="max-w-40 truncate">
|
||||
{tab.title || language.t("session.tab.browser")}
|
||||
</span>
|
||||
</div>
|
||||
</Tabs.Trigger>
|
||||
)}
|
||||
</For>
|
||||
<div class="h-full shrink-0 sticky end-0 z-10 flex items-center justify-center bg-v2-background-bg-base">
|
||||
<Tooltip value={language.t("session.tab.add")} placement="bottom" class="flex items-center">
|
||||
<Menu appearance="standard" modal={false} placement="bottom-end" gutter={4}>
|
||||
|
|
@ -527,18 +534,20 @@ export function SessionSidePanel(props: {
|
|||
<div
|
||||
id={browserTabPanelID}
|
||||
role="tabpanel"
|
||||
aria-labelledby={browserTabID}
|
||||
aria-labelledby={
|
||||
props.browser.active() ? `${browserTabID}-${props.browser.active()?.id}` : undefined
|
||||
}
|
||||
data-slot="tabs-content"
|
||||
class="h-full min-h-0 overflow-hidden"
|
||||
classList={{ hidden: activeTab() !== SESSION_BROWSER_TAB }}
|
||||
inert={activeTab() !== SESSION_BROWSER_TAB || undefined}
|
||||
classList={{ hidden: !isSessionBrowserTab(activeTab()) }}
|
||||
inert={!isSessionBrowserTab(activeTab()) || undefined}
|
||||
>
|
||||
<Show when={props.browser.registration()} keyed>
|
||||
{(registration) => (
|
||||
<SessionBrowserPane
|
||||
registration={registration}
|
||||
browser={props.browser}
|
||||
visible={activeTab() === SESSION_BROWSER_TAB}
|
||||
visible={isSessionBrowserTab(activeTab())}
|
||||
/>
|
||||
)}
|
||||
</Show>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { createMemo, createRoot } from "solid-js"
|
|||
import { createStore } from "solid-js/store"
|
||||
import {
|
||||
SESSION_BROWSER_TAB,
|
||||
sessionBrowserTab,
|
||||
SESSION_OPEN_FILE_TAB,
|
||||
createOpenReviewFile,
|
||||
createOpenSessionFileTab,
|
||||
|
|
@ -252,4 +253,22 @@ describe("createSessionTabs", () => {
|
|||
dispose()
|
||||
})
|
||||
})
|
||||
|
||||
test("keeps multiple browser tabs distinct from file tabs", () => {
|
||||
createRoot((dispose) => {
|
||||
const first = sessionBrowserTab("first")
|
||||
const second = sessionBrowserTab("second")
|
||||
const result = createSessionTabs({
|
||||
tabs: () => ({ active: () => second, all: () => [first, "file://src/a.ts", second] }),
|
||||
pathFromTab: (tab) => (tab.startsWith("file://") ? tab.slice(7) : undefined),
|
||||
normalizeTab: (tab) => tab,
|
||||
browser: () => true,
|
||||
})
|
||||
expect(result.panelTabs()).toEqual(["file://src/a.ts"])
|
||||
expect(result.activeTab()).toBe(second)
|
||||
expect(result.activeFileTab()).toBeUndefined()
|
||||
expect(result.closableTab()).toBe(second)
|
||||
dispose()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -2,9 +2,14 @@ import { batch, createMemo, onCleanup, onMount, type Accessor } from "solid-js"
|
|||
import { createStore } from "solid-js/store"
|
||||
import { makeEventListener } from "@solid-primitives/event-listener"
|
||||
import { same } from "@/runtime/persistence/equality"
|
||||
import { SESSION_BROWSER_TAB, SESSION_OPEN_FILE_TAB } from "@/shell/state/session-tabs"
|
||||
import { isSessionBrowserTab, SESSION_OPEN_FILE_TAB } from "@/shell/state/session-tabs"
|
||||
|
||||
export { SESSION_BROWSER_TAB, SESSION_OPEN_FILE_TAB } from "@/shell/state/session-tabs"
|
||||
export {
|
||||
SESSION_BROWSER_TAB,
|
||||
SESSION_OPEN_FILE_TAB,
|
||||
sessionBrowserTab,
|
||||
isSessionBrowserTab,
|
||||
} from "@/shell/state/session-tabs"
|
||||
|
||||
const emptyTabs: string[] = []
|
||||
|
||||
|
|
@ -45,7 +50,7 @@ export const createSessionTabs = (input: TabsInput) => {
|
|||
.tabs()
|
||||
.all()
|
||||
.flatMap((tab) => {
|
||||
if (tab === "context" || tab === "review" || tab === SESSION_BROWSER_TAB) return []
|
||||
if (tab === "context" || tab === "review" || isSessionBrowserTab(tab)) return []
|
||||
if (tab === SESSION_OPEN_FILE_TAB && !fileBrowser()) return []
|
||||
const value = input.pathFromTab(tab) ? input.normalizeTab(tab) : tab
|
||||
if (seen.has(value)) return []
|
||||
|
|
@ -63,7 +68,7 @@ export const createSessionTabs = (input: TabsInput) => {
|
|||
const active = input.tabs().active()
|
||||
if (active === "context") return active
|
||||
if (active === SESSION_OPEN_FILE_TAB && openFileOpen()) return active
|
||||
if (active === SESSION_BROWSER_TAB && browser()) return active
|
||||
if (active && isSessionBrowserTab(active) && browser()) return active
|
||||
if (active === "review" && review()) return active
|
||||
if (active && input.pathFromTab(active)) return input.normalizeTab(active)
|
||||
|
||||
|
|
@ -82,7 +87,7 @@ export const createSessionTabs = (input: TabsInput) => {
|
|||
const active = activeTab()
|
||||
if (active === "context") return active
|
||||
if (active === SESSION_OPEN_FILE_TAB && openFileOpen()) return active
|
||||
if (active === SESSION_BROWSER_TAB && browser()) return active
|
||||
if (active && isSessionBrowserTab(active) && browser()) return active
|
||||
if (!openedTabs().includes(active)) return
|
||||
return active
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
export const SESSION_OPEN_FILE_TAB = "open-file"
|
||||
export const SESSION_BROWSER_TAB = "browser"
|
||||
export const sessionBrowserTab = (tabID: string) => `${SESSION_BROWSER_TAB}:${tabID}`
|
||||
export const isSessionBrowserTab = (tab: string | undefined) =>
|
||||
!!tab && (tab === SESSION_BROWSER_TAB || tab.startsWith(`${SESSION_BROWSER_TAB}:`))
|
||||
|
||||
export type SessionTabs = {
|
||||
active?: string
|
||||
|
|
|
|||
|
|
@ -28,7 +28,9 @@
|
|||
"electron-log": "^5",
|
||||
"electron-store": "11.0.2",
|
||||
"electron-updater": "6.8.9",
|
||||
"electron-window-state": "^5.0.3"
|
||||
"electron-window-state": "^5.0.3",
|
||||
"lighthouse": "13.4.1",
|
||||
"puppeteer-core": "25.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@brendonovich/vite-plugin-opencode": "0.1.3",
|
||||
|
|
@ -47,6 +49,7 @@
|
|||
"@types/node": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"app-builder-lib": "26.15.7",
|
||||
"devtools-protocol": "0.0.1687809",
|
||||
"drizzle-orm": "catalog:",
|
||||
"effect": "catalog:",
|
||||
"electron": "42.10.1",
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -15,10 +15,13 @@ type Entry = {
|
|||
win: BrowserWindow
|
||||
abort: AbortController
|
||||
registered: PromiseWithResolvers<void>
|
||||
requests: Map<string, AbortController>
|
||||
report?: (event: Extract<BrowserPaneEvent["event"], { type: "state" }>) => void
|
||||
requests: Map<string, { abort: AbortController; tabID?: Browser.TabID }>
|
||||
report?: (event: BrowserPaneEvent["event"]) => void
|
||||
cleanup?: () => void
|
||||
page?: BrowserPage
|
||||
pages: Map<Browser.TabID, BrowserPage>
|
||||
focusedTabID: Browser.TabID | null
|
||||
partition: string
|
||||
lastState?: string
|
||||
}
|
||||
|
||||
export function createBrowserPane() {
|
||||
|
|
@ -39,6 +42,9 @@ export function createBrowserPane() {
|
|||
abort: new AbortController(),
|
||||
registered: Promise.withResolvers(),
|
||||
requests: new Map(),
|
||||
pages: new Map(),
|
||||
focusedTabID: null,
|
||||
partition: `opencode-browser-${crypto.randomUUID()}`,
|
||||
}
|
||||
// "unsupported" means the server has no browser plugin; the renderer stops retrying.
|
||||
let reason: "browser.pane.unsupported" | undefined
|
||||
|
|
@ -49,6 +55,7 @@ export function createBrowserPane() {
|
|||
win.webContents.once("destroyed", stop)
|
||||
win.webContents.on("did-start-navigation", navigate)
|
||||
entry.cleanup = () => {
|
||||
if (win.isDestroyed()) return
|
||||
win.webContents.off("destroyed", stop)
|
||||
win.webContents.off("did-start-navigation", navigate)
|
||||
}
|
||||
|
|
@ -80,9 +87,10 @@ export function createBrowserPane() {
|
|||
entry.report = (event) => {
|
||||
Queue.offerUnsafe(
|
||||
outbound,
|
||||
rpc
|
||||
.state({ ...attachment, state: event.state }, options)
|
||||
.pipe(Effect.tap(() => Effect.sync(() => publish(entry, event)))),
|
||||
(event.type === "state"
|
||||
? rpc.state({ ...attachment, state: event.state ?? { tabs: [], focusedTabID: null } }, options)
|
||||
: Effect.void
|
||||
).pipe(Effect.andThen(Effect.sync(() => publish(entry, event)))),
|
||||
)
|
||||
}
|
||||
const receive = client.event.subscribe().pipe(
|
||||
|
|
@ -92,40 +100,61 @@ export function createBrowserPane() {
|
|||
yield* Deferred.succeed(connected, undefined)
|
||||
return
|
||||
}
|
||||
if (event.type !== "rpc.experimental.browser.control") return
|
||||
const message = yield* Schema.decodeUnknownEffect(Browser.Control)(event.data)
|
||||
if (message.connectionID !== attachment.connectionID) return
|
||||
if (message.type === "attached") return entry.registered.resolve()
|
||||
if (message.type === "cancel") return entry.requests.get(message.requestID)?.abort()
|
||||
const abort = new AbortController()
|
||||
entry.requests.set(message.requestID, abort)
|
||||
yield* Effect.promise(async () => {
|
||||
const outcome: Browser.Outcome = await execute(entry, message.command, abort.signal).then(
|
||||
(result) => ({ type: "success" as const, result }),
|
||||
(error: unknown) => ({
|
||||
type: "failure" as const,
|
||||
message: (error instanceof Error ? error.message : String(error)).slice(0, 1_024),
|
||||
if (
|
||||
event.type !== "rpc.experimental.browser.control" ||
|
||||
event.data.connectionID !== attachment.connectionID
|
||||
)
|
||||
return
|
||||
const message = yield* Schema.decodeUnknownEffect(Browser.Control)(event.data).pipe(
|
||||
Effect.tapError(() =>
|
||||
Effect.sync(() => {
|
||||
reason = "browser.pane.unsupported"
|
||||
}),
|
||||
)
|
||||
Queue.offerUnsafe(
|
||||
outbound,
|
||||
rpc.result(
|
||||
{
|
||||
...attachment,
|
||||
requestID: message.requestID,
|
||||
outcome: Schema.encodeSync(Browser.Outcome)(outcome),
|
||||
},
|
||||
options,
|
||||
),
|
||||
)
|
||||
}).pipe(
|
||||
),
|
||||
)
|
||||
if (message.type === "attached") return entry.registered.resolve()
|
||||
if (message.type === "cancel") return entry.requests.get(message.requestID)?.abort.abort()
|
||||
const abort = new AbortController()
|
||||
entry.requests.set(message.requestID, { abort })
|
||||
yield* rpc.command({ ...attachment, requestID: message.requestID }, options).pipe(
|
||||
Effect.flatMap((command) =>
|
||||
Effect.promise(async () => {
|
||||
entry.requests.set(message.requestID, {
|
||||
abort,
|
||||
...("tabID" in command.action ? { tabID: command.action.tabID } : {}),
|
||||
})
|
||||
const outcome: Browser.Outcome = await execute(entry, command, abort.signal).then(
|
||||
(result) => ({ type: "success" as const, result }),
|
||||
(error: unknown) => ({
|
||||
type: "failure" as const,
|
||||
code: "operation_failed",
|
||||
message: (error instanceof Error ? error.message : String(error)).slice(0, 1_024),
|
||||
}),
|
||||
)
|
||||
Queue.offerUnsafe(
|
||||
outbound,
|
||||
rpc.result(
|
||||
{
|
||||
...attachment,
|
||||
requestID: message.requestID,
|
||||
outcome: Schema.encodeSync(Browser.Outcome)(outcome),
|
||||
},
|
||||
options,
|
||||
),
|
||||
)
|
||||
}),
|
||||
),
|
||||
Effect.ensuring(
|
||||
Effect.sync(() => {
|
||||
abort.abort()
|
||||
entry.requests.delete(message.requestID)
|
||||
}),
|
||||
),
|
||||
Effect.catchCause(() => Effect.sync(stop)),
|
||||
Effect.catchCause((cause) =>
|
||||
abort.signal.aborted
|
||||
? Effect.void
|
||||
: Effect.logError("Browser command failed", cause).pipe(Effect.andThen(Effect.sync(stop))),
|
||||
),
|
||||
Effect.forkScoped,
|
||||
)
|
||||
}),
|
||||
|
|
@ -134,14 +163,15 @@ export function createBrowserPane() {
|
|||
yield* Effect.raceAllFirst([
|
||||
receive,
|
||||
Stream.fromQueue(outbound).pipe(Stream.runForEach((send) => send)),
|
||||
Deferred.await(connected).pipe(Effect.andThen(rpc.attach(attachment, options))),
|
||||
Deferred.await(connected).pipe(Effect.andThen(rpc.attach({ ...attachment, version: 2 }, options))),
|
||||
])
|
||||
}).pipe(
|
||||
Effect.scoped,
|
||||
Effect.tapError((error) =>
|
||||
Effect.sync(() => {
|
||||
const type = error instanceof Object && "type" in error ? error.type : undefined
|
||||
if (type === "rpc.unavailable" || type === "rpc.method_not_found") reason = "browser.pane.unsupported"
|
||||
if (type === "rpc.unavailable" || type === "rpc.method_not_found" || type === "rpc.invalid_input")
|
||||
reason = "browser.pane.unsupported"
|
||||
}),
|
||||
),
|
||||
Effect.ensuring(Effect.sync(stop)),
|
||||
|
|
@ -156,23 +186,23 @@ export function createBrowserPane() {
|
|||
},
|
||||
layout(win: BrowserWindow, bindingID: string, value?: BrowserPaneLayout) {
|
||||
const entry = owned(win, bindingID)
|
||||
if (!value) return closePage(entry)
|
||||
if (!value) return entry.pages.forEach((page) => page.view.setVisible(false))
|
||||
const page = entry.pages.get(value.tabID)
|
||||
if (!page) return
|
||||
const bounds = value.bounds
|
||||
if (!value.visible || !bounds || bounds.width <= 0 || bounds.height <= 0) {
|
||||
entry.page?.view.setVisible(false)
|
||||
page.view.setVisible(false)
|
||||
return
|
||||
}
|
||||
const page = create(entry)
|
||||
entry.pages.forEach((other) => {
|
||||
if (other !== page) other.view.setVisible(false)
|
||||
})
|
||||
page.view.setBounds(bounds)
|
||||
page.view.setVisible(true)
|
||||
},
|
||||
async command(win: BrowserWindow, bindingID: string, command: BrowserPaneCommand) {
|
||||
const entry = owned(win, bindingID)
|
||||
await execute(
|
||||
entry,
|
||||
{ action: command, generation: entry.page?.state().generation ?? 0 },
|
||||
new AbortController().signal,
|
||||
)
|
||||
await execute(entry, { action: command, files: [] }, new AbortController().signal)
|
||||
},
|
||||
async close(win: BrowserWindow, bindingID: string) {
|
||||
close(owned(win, bindingID))
|
||||
|
|
@ -198,57 +228,117 @@ export function createBrowserPane() {
|
|||
function close(entry: Entry, reason = "browser.pane.registration.closed") {
|
||||
if (entries.get(entry.bindingID) !== entry) return
|
||||
entry.report = undefined
|
||||
closePage(entry, reason)
|
||||
entry.requests.forEach((request) => request.abort.abort())
|
||||
entry.requests.clear()
|
||||
entry.pages.forEach((page) => {
|
||||
void page.dispose().catch(() => undefined)
|
||||
})
|
||||
entry.pages.clear()
|
||||
entry.focusedTabID = null
|
||||
publishState(entry, reason)
|
||||
entries.delete(entry.bindingID)
|
||||
entry.registered.reject(new Error("browser.pane.registration.closed"))
|
||||
entry.cleanup?.()
|
||||
entry.abort.abort()
|
||||
}
|
||||
|
||||
function closePage(entry: Entry, error?: string) {
|
||||
entry.requests.forEach((request) => request.abort())
|
||||
entry.requests.clear()
|
||||
entry.page?.dispose()
|
||||
entry.page = undefined
|
||||
async function closePage(entry: Entry, tabID: Browser.TabID, error?: string) {
|
||||
const page = entry.pages.get(tabID)
|
||||
if (!page) throw new Error("Browser tab is unavailable.")
|
||||
const focused = entry.focusedTabID === tabID
|
||||
entry.requests.forEach((request) => {
|
||||
if (request.tabID === tabID) request.abort.abort()
|
||||
})
|
||||
entry.pages.delete(tabID)
|
||||
if (focused) entry.focusedTabID = entry.pages.keys().next().value ?? null
|
||||
await page.dispose()
|
||||
publishState(entry, error)
|
||||
}
|
||||
|
||||
function publishState(entry: Entry, error?: string) {
|
||||
const event = {
|
||||
type: "state" as const,
|
||||
state: entry.page?.state() ?? null,
|
||||
state: { tabs: Array.from(entry.pages.values(), (page) => page.state()), focusedTabID: entry.focusedTabID },
|
||||
...(error === undefined ? {} : { error }),
|
||||
}
|
||||
const next = JSON.stringify(event)
|
||||
if (entry.lastState === next) return
|
||||
entry.lastState = next
|
||||
report(entry, event)
|
||||
}
|
||||
|
||||
function report(entry: Entry, event: BrowserPaneEvent["event"]) {
|
||||
if (entry.report) return entry.report(event)
|
||||
publish(entry, event)
|
||||
}
|
||||
|
||||
function create(entry: Entry) {
|
||||
if (entry.page) return entry.page
|
||||
function create(entry: Entry, initialize = true, popupOptions?: Electron.BrowserWindowConstructorOptions) {
|
||||
const id = Browser.TabID.make(`tab_${crypto.randomUUID()}`)
|
||||
const fail = () => {
|
||||
if (entry.page === page) closePage(entry, "page_crashed")
|
||||
if (entry.pages.has(id)) void closePage(entry, id, "page_crashed").catch(() => undefined)
|
||||
}
|
||||
const page = createBrowserPage(
|
||||
entry.win,
|
||||
(error) => {
|
||||
if (entry.page === page) publishState(entry, error)
|
||||
},
|
||||
const page = createBrowserPage(entry.win, {
|
||||
id,
|
||||
partition: entry.partition,
|
||||
initialize,
|
||||
popupOptions,
|
||||
fail,
|
||||
)
|
||||
entry.page = page
|
||||
publish: (error) => {
|
||||
if (entry.pages.has(id)) publishState(entry, error)
|
||||
},
|
||||
popup: (popupOptions) => {
|
||||
const popup = create(entry, false, popupOptions)
|
||||
focus(entry, popup.state().id)
|
||||
return popup.contents
|
||||
},
|
||||
})
|
||||
entry.pages.set(id, page)
|
||||
void page.ready
|
||||
.then(() => {
|
||||
if (entry.page === page) publishState(entry)
|
||||
if (entry.pages.get(id) === page) publishState(entry)
|
||||
})
|
||||
.catch(fail)
|
||||
return page
|
||||
}
|
||||
|
||||
async function execute(entry: Entry, command: Browser.Command, signal: AbortSignal) {
|
||||
if (command.action.type === "open") publish(entry, { type: "open" })
|
||||
const page = command.action.type === "open" ? create(entry) : entry.page
|
||||
if (!page) throw new Error("not_attached")
|
||||
const action = command.action
|
||||
const state = () => ({
|
||||
tabs: Array.from(entry.pages.values(), (page) => page.state()),
|
||||
focusedTabID: entry.focusedTabID,
|
||||
})
|
||||
if (signal.aborted) throw new Error("Browser request was cancelled.")
|
||||
if (action.type === "tabs.list") return { value: state(), files: [] }
|
||||
if (action.type === "tabs.open") {
|
||||
const page = create(entry)
|
||||
if (action.focus !== false) focus(entry, page.state().id)
|
||||
await page.ready
|
||||
await page.execute(
|
||||
{ action: { type: "navigate", tabID: page.state().id, url: action.url ?? "about:blank" }, files: [] },
|
||||
signal,
|
||||
)
|
||||
publishState(entry)
|
||||
return { value: page.state(), files: [] }
|
||||
}
|
||||
const page = entry.pages.get(action.tabID)
|
||||
if (!page) throw new Error("Browser tab is unavailable. Call browser.tabs.list.")
|
||||
if (action.type === "tabs.focus") {
|
||||
focus(entry, action.tabID)
|
||||
return { value: page.state(), files: [] }
|
||||
}
|
||||
if (action.type === "tabs.close") {
|
||||
await closePage(entry, action.tabID)
|
||||
return { value: state(), files: [] }
|
||||
}
|
||||
await page.ready
|
||||
return page.execute(command, signal)
|
||||
const result = await page.execute(command, signal)
|
||||
publishState(entry)
|
||||
return result
|
||||
}
|
||||
|
||||
function focus(entry: Entry, tabID: Browser.TabID) {
|
||||
entry.focusedTabID = tabID
|
||||
publishState(entry)
|
||||
report(entry, { type: "focus", tabID })
|
||||
}
|
||||
}
|
||||
|
|
|
|||
193
packages/desktop/src/main/browser/analysis.ts
Normal file
193
packages/desktop/src/main/browser/analysis.ts
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
import { Schema } from "effect"
|
||||
|
||||
const number = Schema.Finite
|
||||
const Trace = Schema.Struct({
|
||||
traceEvents: Schema.Array(
|
||||
Schema.Struct({
|
||||
name: Schema.optionalKey(Schema.String),
|
||||
ph: Schema.optionalKey(Schema.String),
|
||||
pid: Schema.optionalKey(number),
|
||||
tid: Schema.optionalKey(number),
|
||||
ts: Schema.optionalKey(number),
|
||||
dur: Schema.optionalKey(number),
|
||||
}),
|
||||
),
|
||||
})
|
||||
const Cpu = Schema.Struct({
|
||||
startTime: number,
|
||||
endTime: number,
|
||||
nodes: Schema.Array(
|
||||
Schema.Struct({
|
||||
id: number,
|
||||
callFrame: Schema.Struct({ functionName: Schema.String, url: Schema.String, lineNumber: number }),
|
||||
}),
|
||||
),
|
||||
samples: Schema.optionalKey(Schema.Array(number)),
|
||||
timeDeltas: Schema.optionalKey(Schema.Array(number)),
|
||||
})
|
||||
const Heap = Schema.Struct({
|
||||
snapshot: Schema.Struct({
|
||||
meta: Schema.Struct({
|
||||
node_fields: Schema.Array(Schema.String),
|
||||
node_types: Schema.Array(Schema.Union([Schema.String, Schema.Array(Schema.String)])),
|
||||
edge_fields: Schema.Array(Schema.String),
|
||||
edge_types: Schema.Array(Schema.Union([Schema.String, Schema.Array(Schema.String)])),
|
||||
}),
|
||||
}),
|
||||
nodes: Schema.Array(number),
|
||||
edges: Schema.Array(number),
|
||||
strings: Schema.Array(Schema.String),
|
||||
})
|
||||
|
||||
export function analyzeTrace(value: unknown, limit = 100) {
|
||||
const trace = Schema.decodeUnknownSync(Trace)(value)
|
||||
const events = new Map<string, { name: string; count: number; totalMs: number; maxMs: number }>()
|
||||
const longTasks: number[] = []
|
||||
trace.traceEvents.forEach((event) => {
|
||||
if (event.ph !== "X" || !event.name || event.dur === undefined) return
|
||||
const duration = event.dur / 1000
|
||||
const item = events.get(event.name) ?? { name: event.name.slice(0, 2_048), count: 0, totalMs: 0, maxMs: 0 }
|
||||
item.count++
|
||||
item.totalMs += duration
|
||||
item.maxMs = Math.max(item.maxMs, duration)
|
||||
events.set(event.name, item)
|
||||
if ((event.name === "RunTask" || event.name === "ThreadControllerImpl::RunTask") && duration > 50)
|
||||
longTasks.push(duration)
|
||||
})
|
||||
return {
|
||||
metrics: [
|
||||
{ name: "recordedEvents", value: trace.traceEvents.length, unit: "count" },
|
||||
{ name: "longTasks", value: longTasks.length, unit: "count" },
|
||||
{ name: "longTaskBlocking", value: longTasks.reduce((sum, duration) => sum + duration - 50, 0), unit: "ms" },
|
||||
],
|
||||
events: Array.from(events.values())
|
||||
.sort((a, b) => b.totalMs - a.totalMs)
|
||||
.slice(0, limit),
|
||||
insights: [
|
||||
"Event durations can overlap and must not be summed into total page time.",
|
||||
longTasks.length
|
||||
? `${longTasks.length} recorded tasks exceeded 50 ms; inspect their stacks in the exported trace.`
|
||||
: "No tasks over 50 ms were observed in the retained trace. This is not proof that the page has no long tasks.",
|
||||
"This renderer-process trace does not calculate LCP, INP, CLS, or Lighthouse performance scores.",
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
export function analyzeCpu(value: unknown, limit = 100) {
|
||||
const profile = Schema.decodeUnknownSync(Cpu)(value)
|
||||
const times = new Map<number, number>()
|
||||
profile.samples?.forEach((id, index) =>
|
||||
times.set(id, (times.get(id) ?? 0) + (profile.timeDeltas?.[index] ?? 0) / 1000),
|
||||
)
|
||||
return {
|
||||
durationMs: Math.max(0, (profile.endTime - profile.startTime) / 1000),
|
||||
functions: profile.nodes
|
||||
.map((node) => ({
|
||||
name: node.callFrame.functionName.slice(0, 2_048),
|
||||
url: node.callFrame.url.slice(0, 100_000),
|
||||
line: Math.max(0, node.callFrame.lineNumber + 1),
|
||||
selfMs: times.get(node.id) ?? 0,
|
||||
}))
|
||||
.filter((node) => node.selfMs > 0)
|
||||
.sort((a, b) => b.selfMs - a.selfMs)
|
||||
.slice(0, limit),
|
||||
}
|
||||
}
|
||||
|
||||
export function parseHeap(value: unknown) {
|
||||
const heap = Schema.decodeUnknownSync(Heap)(value)
|
||||
const fields = heap.snapshot.meta.node_fields
|
||||
const edgeFields = heap.snapshot.meta.edge_fields
|
||||
const width = fields.length
|
||||
const edgeWidth = edgeFields.length
|
||||
const indexes = {
|
||||
type: fields.indexOf("type"),
|
||||
name: fields.indexOf("name"),
|
||||
id: fields.indexOf("id"),
|
||||
size: fields.indexOf("self_size"),
|
||||
count: fields.indexOf("edge_count"),
|
||||
edgeType: edgeFields.indexOf("type"),
|
||||
edgeName: edgeFields.indexOf("name_or_index"),
|
||||
to: edgeFields.indexOf("to_node"),
|
||||
}
|
||||
if (
|
||||
!width ||
|
||||
!edgeWidth ||
|
||||
Object.values(indexes).some((index) => index < 0) ||
|
||||
heap.nodes.length % width ||
|
||||
heap.edges.length % edgeWidth
|
||||
)
|
||||
throw new Error("Unsupported heap snapshot layout.")
|
||||
const types = heap.snapshot.meta.node_types[indexes.type]
|
||||
const edgeTypes = heap.snapshot.meta.edge_types[indexes.edgeType]
|
||||
if (!Array.isArray(types) || !Array.isArray(edgeTypes)) throw new Error("Unsupported heap snapshot types.")
|
||||
const node = (offset: number) => ({
|
||||
id: heap.nodes[offset + indexes.id],
|
||||
name: (heap.strings[heap.nodes[offset + indexes.name]] ?? "").slice(0, 100_000),
|
||||
type: types[heap.nodes[offset + indexes.type]] ?? "unknown",
|
||||
selfBytes: heap.nodes[offset + indexes.size],
|
||||
edgeCount: heap.nodes[offset + indexes.count],
|
||||
})
|
||||
const classes = new Map<string, { name: string; count: number; bytes: number }>()
|
||||
let selfBytes = 0
|
||||
for (let offset = 0; offset < heap.nodes.length; offset += width) {
|
||||
const item = node(offset)
|
||||
const name = item.name.slice(0, 2_048)
|
||||
const entry = classes.get(name) ?? { name, count: 0, bytes: 0 }
|
||||
entry.count++
|
||||
entry.bytes += item.selfBytes
|
||||
selfBytes += item.selfBytes
|
||||
classes.set(name, entry)
|
||||
}
|
||||
return {
|
||||
summary(limit = 100) {
|
||||
return {
|
||||
nodes: heap.nodes.length / width,
|
||||
edges: heap.edges.length / edgeWidth,
|
||||
selfBytes,
|
||||
classes: Array.from(classes.values())
|
||||
.sort((a, b) => b.bytes - a.bytes)
|
||||
.slice(0, limit),
|
||||
}
|
||||
},
|
||||
classes,
|
||||
query(name = "", limit = 100) {
|
||||
const found: ReturnType<typeof node>[] = []
|
||||
for (let offset = 0; offset < heap.nodes.length; offset += width) {
|
||||
const item = node(offset)
|
||||
if (item.name.toLowerCase().includes(name.toLowerCase())) found.push(item)
|
||||
}
|
||||
return { nodes: found.sort((a, b) => b.selfBytes - a.selfBytes).slice(0, limit), truncated: found.length > limit }
|
||||
},
|
||||
object(id: number, limit = 100) {
|
||||
const target = heap.nodes.findIndex((value, index) => index % width === indexes.id && value === id) - indexes.id
|
||||
if (target < 0) throw new Error("Object ID was not found in this heap snapshot.")
|
||||
const references: { name: string; node: ReturnType<typeof node> }[] = []
|
||||
const retainers: { name: string; node: ReturnType<typeof node> }[] = []
|
||||
let edgeOffset = 0
|
||||
let truncated = false
|
||||
for (let offset = 0; offset < heap.nodes.length; offset += width) {
|
||||
const count = heap.nodes[offset + indexes.count]
|
||||
for (let index = 0; index < count; index++, edgeOffset += edgeWidth) {
|
||||
const to = heap.edges[edgeOffset + indexes.to]
|
||||
if (offset !== target && to !== target) continue
|
||||
const type = edgeTypes[heap.edges[edgeOffset + indexes.edgeType]]
|
||||
const raw = heap.edges[edgeOffset + indexes.edgeName]
|
||||
const name = (type === "element" || type === "hidden" ? String(raw) : (heap.strings[raw] ?? "")).slice(
|
||||
0,
|
||||
100_000,
|
||||
)
|
||||
if (offset === target) {
|
||||
if (references.length < limit) references.push({ name, node: node(to) })
|
||||
else truncated = true
|
||||
}
|
||||
if (to === target) {
|
||||
if (retainers.length < limit) retainers.push({ name, node: node(offset) })
|
||||
else truncated = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return { node: node(target), references, retainers, truncated }
|
||||
},
|
||||
}
|
||||
}
|
||||
72
packages/desktop/src/main/browser/cdp.ts
Normal file
72
packages/desktop/src/main/browser/cdp.ts
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import type { WebContents } from "electron"
|
||||
import type { ProtocolMapping } from "devtools-protocol/types/protocol-mapping.js"
|
||||
|
||||
export type Cdp = ReturnType<typeof createCdp>
|
||||
|
||||
export function createCdp(contents: WebContents) {
|
||||
const listeners = new Map<string, Set<(params: unknown, sessionID?: string) => void>>()
|
||||
const sessions = new Set([""])
|
||||
const receive = (_event: Electron.Event, name: string, params: unknown, sessionID?: string) => {
|
||||
if (!sessions.has(sessionID ?? "")) return
|
||||
if (name === "Target.attachedToTarget") {
|
||||
const event = params as ProtocolMapping.Events["Target.attachedToTarget"][0]
|
||||
if (event.targetInfo.type === "iframe") sessions.add(event.sessionId)
|
||||
}
|
||||
if (name === "Target.detachedFromTarget")
|
||||
sessions.delete((params as ProtocolMapping.Events["Target.detachedFromTarget"][0]).sessionId)
|
||||
listeners.get(name)?.forEach((callback) => callback(params, sessionID || undefined))
|
||||
}
|
||||
contents.debugger.on("message", receive)
|
||||
return {
|
||||
async send<Method extends keyof ProtocolMapping.Commands>(
|
||||
method: Method,
|
||||
params: object = {},
|
||||
sessionID?: string,
|
||||
): Promise<ProtocolMapping.Commands[Method]["returnType"]> {
|
||||
if (contents.isDestroyed()) throw new Error("Browser tab was closed.")
|
||||
if (!contents.debugger.isAttached()) contents.debugger.attach("1.3")
|
||||
// Electron is the CDP boundary. Its native response follows the selected protocol method.
|
||||
const result = contents.debugger.sendCommand(method, params, sessionID)
|
||||
if (method !== "Page.captureScreenshot") return result
|
||||
// Keep a hidden view's compositor awake for CDP (including Lighthouse's
|
||||
// screenshot gatherer) without changing the selected Review tab.
|
||||
const [capture] = await Promise.all([
|
||||
result,
|
||||
contents.capturePage(undefined, { stayHidden: false, stayAwake: true }),
|
||||
])
|
||||
return capture
|
||||
},
|
||||
on<Method extends keyof ProtocolMapping.Events>(
|
||||
method: Method,
|
||||
callback: (params: ProtocolMapping.Events[Method][0], sessionID?: string) => void,
|
||||
) {
|
||||
const handler = (params: unknown, sessionID?: string) =>
|
||||
callback(params as ProtocolMapping.Events[Method][0], sessionID)
|
||||
const handlers = listeners.get(method) ?? new Set()
|
||||
handlers.add(handler)
|
||||
listeners.set(method, handlers)
|
||||
return () => {
|
||||
handlers.delete(handler)
|
||||
if (!handlers.size) listeners.delete(method)
|
||||
}
|
||||
},
|
||||
dispose() {
|
||||
contents.debugger.off("message", receive)
|
||||
listeners.clear()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function abortError(signal: AbortSignal) {
|
||||
if (signal.aborted) throw new Error("Browser operation was cancelled.")
|
||||
}
|
||||
|
||||
export async function waitFor(check: () => boolean | Promise<boolean>, signal: AbortSignal, timeoutMs = 10_000) {
|
||||
const deadline = Date.now() + timeoutMs
|
||||
while (!(await check())) {
|
||||
abortError(signal)
|
||||
if (Date.now() >= deadline) throw new Error(`Condition was not met within ${timeoutMs} ms.`)
|
||||
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||
}
|
||||
abortError(signal)
|
||||
}
|
||||
290
packages/desktop/src/main/browser/diagnostics.ts
Normal file
290
packages/desktop/src/main/browser/diagnostics.ts
Normal file
|
|
@ -0,0 +1,290 @@
|
|||
import { Browser } from "@opencode-ai/schema/browser"
|
||||
import type { Protocol } from "devtools-protocol"
|
||||
import type { Cdp } from "./cdp"
|
||||
|
||||
type Request = {
|
||||
info: Browser.NetworkRequest
|
||||
nativeID: string
|
||||
sessionID?: string
|
||||
started: number
|
||||
request: Pick<Protocol.Network.Request, "headers" | "postData" | "hasPostData">
|
||||
response?: Pick<Protocol.Network.Response, "headers" | "mimeType">
|
||||
headersTruncated: boolean
|
||||
postDataTruncated: boolean
|
||||
redirected?: boolean
|
||||
}
|
||||
const levels = ["debug", "info", "warning", "error"] as const
|
||||
|
||||
export function createDiagnostics(cdp: Cdp) {
|
||||
const messages: Browser.ConsoleEntry[] = []
|
||||
const requests = new Map<string, Request>()
|
||||
const current = new Map<string, Request>()
|
||||
const scope = crypto.randomUUID()
|
||||
let sequence = 0
|
||||
let droppedMessages = 0
|
||||
let droppedRequests = 0
|
||||
const add = (
|
||||
level: (typeof levels)[number],
|
||||
text: string,
|
||||
timestampMs: number,
|
||||
source?: Browser.ConsoleEntry["source"],
|
||||
) => {
|
||||
messages.push({
|
||||
id: `${scope}:${++sequence}`,
|
||||
timestampMs,
|
||||
level,
|
||||
text: text.slice(0, 2_000),
|
||||
textTruncated: text.length > 2_000,
|
||||
...(source ? { source } : {}),
|
||||
})
|
||||
if (messages.length > 500) {
|
||||
messages.shift()
|
||||
droppedMessages++
|
||||
}
|
||||
}
|
||||
cdp.on("Runtime.consoleAPICalled", (event) => {
|
||||
const source = event.stackTrace?.callFrames[0]
|
||||
add(
|
||||
event.type === "error" || event.type === "assert"
|
||||
? "error"
|
||||
: event.type === "warning"
|
||||
? "warning"
|
||||
: event.type === "debug"
|
||||
? "debug"
|
||||
: "info",
|
||||
event.args
|
||||
.map((arg) =>
|
||||
typeof arg.value === "string"
|
||||
? arg.value
|
||||
: arg.value !== undefined
|
||||
? JSON.stringify(arg.value)
|
||||
: (arg.description ?? arg.unserializableValue ?? arg.type),
|
||||
)
|
||||
.join(" "),
|
||||
event.timestamp,
|
||||
source
|
||||
? { url: source.url.slice(0, Browser.MAX_TEXT), line: source.lineNumber + 1, column: source.columnNumber + 1 }
|
||||
: undefined,
|
||||
)
|
||||
})
|
||||
cdp.on("Runtime.exceptionThrown", (event) => {
|
||||
const error = event.exceptionDetails
|
||||
add(
|
||||
"error",
|
||||
error.exception?.description ?? error.text,
|
||||
event.timestamp,
|
||||
error.url ? { url: error.url, line: error.lineNumber + 1, column: error.columnNumber + 1 } : undefined,
|
||||
)
|
||||
})
|
||||
cdp.on("Network.requestWillBeSent", (event, sessionID) => {
|
||||
const key = `${sessionID ?? ""}:${event.requestId}`
|
||||
const previous = current.get(key)
|
||||
if (previous && event.redirectResponse) {
|
||||
const response = trimHeaders(event.redirectResponse.headers)
|
||||
previous.response = { mimeType: event.redirectResponse.mimeType, headers: response.headers }
|
||||
previous.headersTruncated ||= response.truncated
|
||||
previous.redirected = true
|
||||
previous.info = {
|
||||
...previous.info,
|
||||
state: "completed",
|
||||
statusCode: event.redirectResponse.status,
|
||||
durationMs: Math.max(0, (event.timestamp - previous.started) * 1000),
|
||||
}
|
||||
}
|
||||
const type = (event.type ?? "other").toLowerCase()
|
||||
const id = `${scope}:${++sequence}`
|
||||
const headers = trimHeaders(event.request.headers)
|
||||
const request: Request = {
|
||||
nativeID: event.requestId,
|
||||
sessionID,
|
||||
started: event.timestamp,
|
||||
request: {
|
||||
headers: headers.headers,
|
||||
hasPostData: event.request.hasPostData,
|
||||
postData: event.request.postData?.slice(0, 20_000),
|
||||
},
|
||||
headersTruncated: headers.truncated,
|
||||
postDataTruncated: (event.request.postData?.length ?? 0) > 20_000,
|
||||
info: {
|
||||
id,
|
||||
url: event.request.url.slice(0, 16_384),
|
||||
method: event.request.method,
|
||||
resourceType: resourceType(type),
|
||||
timestampMs: event.wallTime * 1000,
|
||||
state: "pending",
|
||||
},
|
||||
}
|
||||
requests.set(id, request)
|
||||
current.set(key, request)
|
||||
if (requests.size > 500) {
|
||||
const first = requests.values().next().value
|
||||
if (first) {
|
||||
requests.delete(first.info.id)
|
||||
if (current.get(`${first.sessionID ?? ""}:${first.nativeID}`) === first)
|
||||
current.delete(`${first.sessionID ?? ""}:${first.nativeID}`)
|
||||
}
|
||||
droppedRequests++
|
||||
}
|
||||
})
|
||||
cdp.on("Network.responseReceived", (event, sessionID) => {
|
||||
const request = current.get(`${sessionID ?? ""}:${event.requestId}`)
|
||||
if (!request) return
|
||||
const headers = trimHeaders(event.response.headers)
|
||||
request.response = { mimeType: event.response.mimeType, headers: headers.headers }
|
||||
request.headersTruncated ||= headers.truncated
|
||||
request.info = { ...request.info, statusCode: event.response.status }
|
||||
})
|
||||
cdp.on("Network.loadingFinished", (event, sessionID) => {
|
||||
const request = current.get(`${sessionID ?? ""}:${event.requestId}`)
|
||||
if (request)
|
||||
request.info = {
|
||||
...request.info,
|
||||
state: "completed",
|
||||
durationMs: Math.max(0, (event.timestamp - request.started) * 1000),
|
||||
}
|
||||
})
|
||||
cdp.on("Network.loadingFailed", (event, sessionID) => {
|
||||
const request = current.get(`${sessionID ?? ""}:${event.requestId}`)
|
||||
if (request)
|
||||
request.info = {
|
||||
...request.info,
|
||||
state: "failed",
|
||||
failure: event.errorText.slice(0, 2_048),
|
||||
durationMs: Math.max(0, (event.timestamp - request.started) * 1000),
|
||||
}
|
||||
})
|
||||
return {
|
||||
clear() {
|
||||
messages.length = 0
|
||||
requests.clear()
|
||||
current.clear()
|
||||
droppedMessages = 0
|
||||
droppedRequests = 0
|
||||
},
|
||||
async enable(sessionID?: string) {
|
||||
await cdp.send("Runtime.enable", {}, sessionID)
|
||||
await cdp.send(
|
||||
"Network.enable",
|
||||
{ maxTotalBufferSize: 5 * 1024 * 1024, maxResourceBufferSize: 1024 * 1024, maxPostDataSize: 20_000 },
|
||||
sessionID,
|
||||
)
|
||||
},
|
||||
console(input: Extract<Browser.Action, { type: "console" }>) {
|
||||
const matching = messages.filter((entry) => levels.indexOf(entry.level) >= levels.indexOf(input.level ?? "info"))
|
||||
const result = bounded(matching, input.limit ?? 100)
|
||||
return { messages: result, truncated: matching.length > result.length, dropped: droppedMessages }
|
||||
},
|
||||
list(input: Extract<Browser.Action, { type: "network.list" }>) {
|
||||
const matching = Array.from(requests.values())
|
||||
.map((request) => request.info)
|
||||
.filter(
|
||||
(request) =>
|
||||
(!input.urlContains || request.url.includes(input.urlContains)) &&
|
||||
(!input.resourceType || input.resourceType === request.resourceType),
|
||||
)
|
||||
const result = bounded(matching, input.limit ?? 100)
|
||||
return { requests: result, truncated: matching.length > result.length, dropped: droppedRequests }
|
||||
},
|
||||
async get(input: Extract<Browser.Action, { type: "network.get" }>) {
|
||||
const request = requests.get(input.id)
|
||||
if (!request) throw new Error("Request is no longer retained in this tab. Call browser.network.list.")
|
||||
const max = input.maxBodyChars ?? 20_000
|
||||
const text = (value: string): Browser.Body => ({
|
||||
state: "text",
|
||||
text: value.slice(0, max),
|
||||
truncated: value.length > max,
|
||||
})
|
||||
const responseBody = async (): Promise<Browser.Body> => {
|
||||
if (!input.includeBody) return { state: "notRequested" }
|
||||
if (request.info.state === "pending") return { state: "pending" }
|
||||
if (request.redirected || !request.response) return { state: "unavailable", reason: "notCaptured" }
|
||||
if (
|
||||
!/^(text\/|application\/(json|.*\+json|javascript|xml|.*\+xml|x-www-form-urlencoded))/i.test(
|
||||
request.response.mimeType,
|
||||
)
|
||||
)
|
||||
return { state: "unavailable", reason: "binary" }
|
||||
const body = await cdp
|
||||
.send("Network.getResponseBody", { requestId: request.nativeID }, request.sessionID)
|
||||
.catch(() => undefined)
|
||||
if (!body) return { state: "unavailable", reason: "backendUnavailable" }
|
||||
const value = body.base64Encoded ? Buffer.from(body.body, "base64").toString("utf8") : body.body
|
||||
return value.length ? text(value) : { state: "empty" }
|
||||
}
|
||||
const requestBody: Browser.Body = !input.includeBody
|
||||
? { state: "notRequested" }
|
||||
: request.request.postData !== undefined
|
||||
? {
|
||||
state: "text",
|
||||
text: request.request.postData.slice(0, max),
|
||||
truncated: request.postDataTruncated || request.request.postData.length > max,
|
||||
}
|
||||
: request.request.hasPostData
|
||||
? { state: "unavailable", reason: "notCaptured" }
|
||||
: { state: "empty" }
|
||||
return {
|
||||
request: request.info,
|
||||
requestHeaders: headerEntries(request.request.headers),
|
||||
responseHeaders: headerEntries(request.response?.headers ?? {}),
|
||||
headersTruncated: request.headersTruncated,
|
||||
requestBody,
|
||||
responseBody: await responseBody(),
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function resourceType(type: string): Browser.ResourceType {
|
||||
switch (type) {
|
||||
case "document":
|
||||
case "stylesheet":
|
||||
case "image":
|
||||
case "media":
|
||||
case "font":
|
||||
case "script":
|
||||
case "xhr":
|
||||
case "fetch":
|
||||
case "eventsource":
|
||||
case "websocket":
|
||||
case "manifest":
|
||||
return type
|
||||
default:
|
||||
return "other"
|
||||
}
|
||||
}
|
||||
|
||||
function trimHeaders(headers: Protocol.Network.Headers) {
|
||||
const entries = Object.entries(headers)
|
||||
const kept: [string, string][] = []
|
||||
let size = 0
|
||||
let truncated = entries.length > 100
|
||||
for (const [key, value] of entries.slice(0, 100)) {
|
||||
const name = key.slice(0, 2_048)
|
||||
const text = String(value)
|
||||
const remaining = Math.max(0, 16_000 - size - name.length)
|
||||
if (!remaining) {
|
||||
truncated = true
|
||||
break
|
||||
}
|
||||
const bounded = text.slice(0, Math.min(2_000, remaining))
|
||||
truncated ||= bounded.length < text.length || name.length < key.length
|
||||
kept.push([name, bounded])
|
||||
size += name.length + bounded.length
|
||||
}
|
||||
return { headers: Object.fromEntries(kept), truncated }
|
||||
}
|
||||
|
||||
function bounded<Item>(items: readonly Item[], limit: number) {
|
||||
const selected: Item[] = []
|
||||
let size = 0
|
||||
for (const item of items.slice(-limit).reverse()) {
|
||||
size += JSON.stringify(item).length
|
||||
if (size > Browser.MAX_TEXT) break
|
||||
selected.push(item)
|
||||
}
|
||||
return selected.reverse()
|
||||
}
|
||||
|
||||
function headerEntries(headers: Protocol.Network.Headers) {
|
||||
return Object.entries(headers).map(([name, value]) => ({ name, value: String(value) }))
|
||||
}
|
||||
67
packages/desktop/src/main/browser/files.ts
Normal file
67
packages/desktop/src/main/browser/files.ts
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import { Browser } from "@opencode-ai/schema/browser"
|
||||
import { mkdir, readFile, stat, writeFile, rm } from "node:fs/promises"
|
||||
import { mkdirSync } from "node:fs"
|
||||
import { tmpdir } from "node:os"
|
||||
import path from "node:path"
|
||||
|
||||
export type BrowserFiles = ReturnType<typeof createBrowserFiles>
|
||||
|
||||
export function createBrowserFiles() {
|
||||
const directory = path.join(tmpdir(), `opencode-browser-client-${crypto.randomUUID()}`)
|
||||
const files = new Map<
|
||||
Browser.FileID,
|
||||
{
|
||||
id: Browser.FileID
|
||||
name: string
|
||||
mime: string
|
||||
bytes: number
|
||||
state: "pending" | "completed" | "failed"
|
||||
path: string
|
||||
}
|
||||
>()
|
||||
const ready = mkdir(directory, { recursive: true })
|
||||
return {
|
||||
directory,
|
||||
ready,
|
||||
list: () => Array.from(files.values()).map(({ path: _path, ...file }) => file),
|
||||
add(name: string, mime: string) {
|
||||
const id = Browser.FileID.make(`file_${crypto.randomUUID()}`)
|
||||
const target = path.join(directory, id)
|
||||
// setSavePath must run during Electron's synchronous will-download callback.
|
||||
mkdirSync(target, { recursive: true })
|
||||
const file = {
|
||||
id,
|
||||
name: name.slice(0, 2_048),
|
||||
mime,
|
||||
bytes: 0,
|
||||
state: "pending" as "pending" | "completed" | "failed",
|
||||
path: path.join(target, name.replace(/[^a-zA-Z0-9._-]/g, "_").slice(-120) || "file"),
|
||||
}
|
||||
files.set(id, file)
|
||||
return file
|
||||
},
|
||||
async save(name: string, mime: string, data: Uint8Array) {
|
||||
if (data.byteLength > Browser.MAX_FILE_BYTES)
|
||||
throw new Error("File exceeds the 5 MiB transfer limit. Capture a smaller file.")
|
||||
await ready
|
||||
const file = this.add(name, mime)
|
||||
await writeFile(file.path, data)
|
||||
file.bytes = data.byteLength
|
||||
file.state = "completed"
|
||||
return file.id
|
||||
},
|
||||
get(id: Browser.FileID) {
|
||||
const file = files.get(id)
|
||||
if (!file || file.state !== "completed")
|
||||
throw new Error("File is not available in this tab. Call browser.files.list.")
|
||||
return file
|
||||
},
|
||||
async transfer(id: Browser.FileID): Promise<Browser.File> {
|
||||
const file = this.get(id)
|
||||
if ((await stat(file.path)).size > Browser.MAX_FILE_BYTES)
|
||||
throw new Error("File exceeds the 5 MiB transfer limit.")
|
||||
return { id, name: file.name, mime: file.mime, data: new Uint8Array(await readFile(file.path)) }
|
||||
},
|
||||
dispose: () => ready.then(() => rm(directory, { recursive: true, force: true })),
|
||||
}
|
||||
}
|
||||
80
packages/desktop/src/main/browser/lighthouse.ts
Normal file
80
packages/desktop/src/main/browser/lighthouse.ts
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import type { WebContents } from "electron"
|
||||
import type { Page } from "puppeteer-core"
|
||||
import { EventEmitter } from "node:events"
|
||||
import type { BrowserFiles } from "./files"
|
||||
import type { Cdp } from "./cdp"
|
||||
|
||||
export async function audit(contents: WebContents, files: BrowserFiles, cdp: Cdp) {
|
||||
const { snapshot, generateReport } = await import("lighthouse")
|
||||
const info = (await contents.debugger.sendCommand("Target.getTargetInfo")) as { targetInfo: { targetId: string } }
|
||||
const sessions = new Map<string, ReturnType<typeof session>>()
|
||||
function session(id: string) {
|
||||
const emitter = new EventEmitter()
|
||||
const value = Object.assign(emitter, {
|
||||
id: () => id,
|
||||
send: (method: string, params?: object) => cdp.send(method as Parameters<Cdp["send"]>[0], params, id),
|
||||
detach: async () => {
|
||||
sessions.delete(id)
|
||||
await contents.debugger.sendCommand("Target.detachFromTarget", { sessionId: id })
|
||||
},
|
||||
})
|
||||
sessions.set(id, value)
|
||||
return value
|
||||
}
|
||||
const event = (_event: Electron.Event, method: string, params: unknown, sessionID?: string) => {
|
||||
const owner = sessionID ? sessions.get(sessionID) : undefined
|
||||
if (!owner) return
|
||||
if (method === "Target.attachedToTarget") {
|
||||
const child = params as { sessionId: string }
|
||||
owner.emit("sessionattached", session(child.sessionId))
|
||||
}
|
||||
owner.emit(method, params)
|
||||
owner.emit("*", method, params)
|
||||
}
|
||||
contents.debugger.on("message", event)
|
||||
try {
|
||||
const attached = (await contents.debugger.sendCommand("Target.attachToTarget", {
|
||||
targetId: info.targetInfo.targetId,
|
||||
flatten: true,
|
||||
})) as { sessionId: string }
|
||||
const root = session(attached.sessionId)
|
||||
// Lighthouse's snapshot driver uses only url() and target().createCDPSession().
|
||||
// Adapt that narrow boundary to Electron instead of exposing a browser-wide
|
||||
// Puppeteer connection or emulating unsupported Browser/Target commands.
|
||||
const page = {
|
||||
url: () => contents.getURL(),
|
||||
target: () => ({ createCDPSession: async () => root }),
|
||||
} as unknown as Page
|
||||
// Our screenshot tool handles captures separately. Lighthouse's screenshot
|
||||
// gatherer resizes the viewport and waits for frames that hidden views may not paint.
|
||||
const result = await snapshot(page, {
|
||||
flags: {
|
||||
onlyCategories: ["accessibility", "seo", "best-practices"],
|
||||
formFactor: "desktop",
|
||||
screenEmulation: { disabled: true },
|
||||
throttlingMethod: "provided",
|
||||
disableStorageReset: true,
|
||||
disableFullPageScreenshot: true,
|
||||
},
|
||||
})
|
||||
if (!result) throw new Error("Lighthouse did not produce a report.")
|
||||
return {
|
||||
scores: Object.values(result.lhr.categories).map((category) => ({
|
||||
id: category.id,
|
||||
title: category.title,
|
||||
score: category.score,
|
||||
})),
|
||||
failures: Object.values(result.lhr.audits)
|
||||
.filter((audit) => audit.score !== null && audit.score < 1)
|
||||
.map((audit) => ({ id: audit.id, title: audit.title, description: audit.description }))
|
||||
.slice(0, 100),
|
||||
files: await Promise.all([
|
||||
files.save("lighthouse.json", "application/json", Buffer.from(generateReport(result.lhr, "json"))),
|
||||
files.save("lighthouse.html", "text/html", Buffer.from(generateReport(result.lhr, "html"))),
|
||||
]),
|
||||
}
|
||||
} finally {
|
||||
contents.debugger.off("message", event)
|
||||
await Promise.all(Array.from(sessions.values(), (session) => session.detach().catch(() => undefined)))
|
||||
}
|
||||
}
|
||||
218
packages/desktop/src/main/browser/profiling.ts
Normal file
218
packages/desktop/src/main/browser/profiling.ts
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
import type { WebContents } from "electron"
|
||||
import { Browser } from "@opencode-ai/schema/browser"
|
||||
import { gzipSync, gunzipSync } from "node:zlib"
|
||||
import { readFile } from "node:fs/promises"
|
||||
import { Schema } from "effect"
|
||||
import type { Cdp } from "./cdp"
|
||||
import type { BrowserFiles } from "./files"
|
||||
import { analyzeCpu, analyzeTrace, parseHeap } from "./analysis"
|
||||
|
||||
let recording:
|
||||
| {
|
||||
owner: WebContents
|
||||
pid: number
|
||||
started: number
|
||||
timer?: ReturnType<typeof setTimeout>
|
||||
finish: () => Promise<{ id: Browser.FileID; durationMs: number; incomplete: boolean }>
|
||||
}
|
||||
| undefined
|
||||
|
||||
export function createProfiling(contents: WebContents, cdp: Cdp, files: BrowserFiles) {
|
||||
let trace: Promise<{ id: Browser.FileID; durationMs: number; incomplete: boolean }> | undefined
|
||||
let cpu:
|
||||
| {
|
||||
started: number
|
||||
timer?: ReturnType<typeof setTimeout>
|
||||
result?: Promise<{ id: Browser.FileID; durationMs: number }>
|
||||
}
|
||||
| undefined
|
||||
let takingHeap = false
|
||||
const json = async (id: Browser.FileID) => {
|
||||
const file = files.get(id)
|
||||
const data = await readFile(file.path)
|
||||
return Schema.decodeUnknownSync(Schema.fromJsonString(Schema.Unknown))(
|
||||
(file.name.endsWith(".gz") ? gunzipSync(data, { maxOutputLength: 128 * 1024 * 1024 }) : data).toString("utf8"),
|
||||
)
|
||||
}
|
||||
const stopCpu = () => {
|
||||
if (!cpu) return Promise.reject(new Error("No CPU profile has been started in this tab."))
|
||||
if (cpu.result) return cpu.result
|
||||
clearTimeout(cpu.timer)
|
||||
cpu.result = cdp.send("Profiler.stop").then(async ({ profile }) => ({
|
||||
id: await files.save("profile.cpuprofile", "application/json", Buffer.from(JSON.stringify(profile))),
|
||||
durationMs: (profile.endTime - profile.startTime) / 1000,
|
||||
}))
|
||||
return cpu.result
|
||||
}
|
||||
return {
|
||||
async startTrace(durationMs = 10_000) {
|
||||
if (recording) throw new Error("Another performance trace is active. Do not stop another tab's recording.")
|
||||
const complete = Promise.withResolvers<{ stream?: string; dataLossOccurred: boolean }>()
|
||||
const off = cdp.on("Tracing.tracingComplete", (event) => complete.resolve(event))
|
||||
const owner = {
|
||||
owner: contents,
|
||||
pid: contents.getOSProcessId(),
|
||||
started: performance.now(),
|
||||
timer: undefined as ReturnType<typeof setTimeout> | undefined,
|
||||
finish: () => {
|
||||
if (trace) return trace
|
||||
trace = (async () => {
|
||||
clearTimeout(owner.timer)
|
||||
const durationMs = performance.now() - owner.started
|
||||
const deadline = Promise.withResolvers<never>()
|
||||
const timeout = setTimeout(
|
||||
() => deadline.reject(new Error("Chromium did not finish flushing the trace within 10 seconds.")),
|
||||
10_000,
|
||||
)
|
||||
try {
|
||||
const result = await Promise.race([
|
||||
cdp.send("Tracing.end").then(() => complete.promise),
|
||||
deadline.promise,
|
||||
])
|
||||
if (!result.stream) throw new Error("Chromium did not return a trace stream.")
|
||||
const chunks: Buffer[] = []
|
||||
let bytes = 0
|
||||
try {
|
||||
while (true) {
|
||||
const part = await cdp.send("IO.read", { handle: result.stream, size: 256 * 1024 })
|
||||
const buffer = Buffer.from(part.data, part.base64Encoded ? "base64" : "utf8")
|
||||
bytes += buffer.byteLength
|
||||
if (bytes > 64 * 1024 * 1024) throw new Error("Trace exceeded its 64 MiB local capture limit.")
|
||||
chunks.push(buffer)
|
||||
if (part.eof) break
|
||||
}
|
||||
} finally {
|
||||
await cdp.send("IO.close", { handle: result.stream })
|
||||
}
|
||||
const raw = Schema.decodeUnknownSync(
|
||||
Schema.fromJsonString(
|
||||
Schema.Struct({ traceEvents: Schema.Array(Schema.Record(Schema.String, Schema.Json)) }),
|
||||
),
|
||||
)(Buffer.concat(chunks).toString("utf8"))
|
||||
// A CDP trace is browser-wide. Export only this renderer process, never
|
||||
// the application renderer or another session's process metadata.
|
||||
const traceEvents = raw.traceEvents.filter((event) => event.pid === owner.pid)
|
||||
const id = await files.save(
|
||||
"trace.json.gz",
|
||||
"application/gzip",
|
||||
gzipSync(
|
||||
JSON.stringify({
|
||||
traceEvents,
|
||||
metadata: { source: "opencode", scope: "renderer-process", processId: owner.pid },
|
||||
}),
|
||||
),
|
||||
)
|
||||
return {
|
||||
id,
|
||||
durationMs,
|
||||
incomplete:
|
||||
result.dataLossOccurred || contents.isDestroyed() || contents.getOSProcessId() !== owner.pid,
|
||||
}
|
||||
} finally {
|
||||
clearTimeout(timeout)
|
||||
off()
|
||||
if (recording === owner) recording = undefined
|
||||
}
|
||||
})()
|
||||
return trace
|
||||
},
|
||||
}
|
||||
recording = owner
|
||||
trace = undefined
|
||||
try {
|
||||
await cdp.send("Tracing.start", {
|
||||
transferMode: "ReturnAsStream",
|
||||
traceConfig: {
|
||||
recordMode: "recordUntilFull",
|
||||
traceBufferSizeInKb: 8192,
|
||||
includedCategories: [
|
||||
"devtools.timeline",
|
||||
"disabled-by-default-devtools.timeline",
|
||||
"disabled-by-default-devtools.timeline.stack",
|
||||
"v8.execute",
|
||||
"blink.user_timing",
|
||||
"disabled-by-default-v8.cpu_profiler",
|
||||
],
|
||||
excludedCategories: ["*"],
|
||||
},
|
||||
})
|
||||
owner.timer = setTimeout(() => {
|
||||
void owner.finish().catch(() => undefined)
|
||||
}, durationMs)
|
||||
} catch (error) {
|
||||
off()
|
||||
if (recording === owner) recording = undefined
|
||||
throw error
|
||||
}
|
||||
},
|
||||
stopTrace() {
|
||||
if (recording?.owner === contents) return recording.finish()
|
||||
if (trace) return trace
|
||||
return Promise.reject(new Error("This tab has no performance trace to stop."))
|
||||
},
|
||||
async startCpu() {
|
||||
if (cpu && !cpu.result) throw new Error("A CPU profile is already active in this tab.")
|
||||
await cdp.send("Profiler.enable")
|
||||
await cdp.send("Profiler.start")
|
||||
cpu = { started: Date.now() }
|
||||
cpu.timer = setTimeout(() => {
|
||||
void stopCpu().catch(() => undefined)
|
||||
}, 30_000)
|
||||
},
|
||||
stopCpu,
|
||||
async heap() {
|
||||
if (takingHeap) throw new Error("A heap snapshot is already being captured in this tab.")
|
||||
takingHeap = true
|
||||
const chunks: string[] = []
|
||||
let size = 0
|
||||
let overflow = false
|
||||
const off = cdp.on("HeapProfiler.addHeapSnapshotChunk", ({ chunk }) => {
|
||||
size += Buffer.byteLength(chunk)
|
||||
if (size > 128 * 1024 * 1024) overflow = true
|
||||
if (!overflow) chunks.push(chunk)
|
||||
})
|
||||
try {
|
||||
await cdp.send("HeapProfiler.enable")
|
||||
await cdp.send("HeapProfiler.takeHeapSnapshot", { reportProgress: false })
|
||||
} finally {
|
||||
takingHeap = false
|
||||
off()
|
||||
}
|
||||
if (overflow) throw new Error("Heap snapshot exceeded the 128 MiB local capture limit.")
|
||||
return files.save("heap.heapsnapshot.gz", "application/gzip", gzipSync(chunks.join("")))
|
||||
},
|
||||
async analyze(
|
||||
action: Extract<
|
||||
Browser.Action,
|
||||
{ type: "trace.analyze" | "cpu.analyze" | "heap.summary" | "heap.query" | "heap.object" | "heap.compare" }
|
||||
>,
|
||||
) {
|
||||
if (action.type === "heap.compare") {
|
||||
const before = parseHeap(await json(action.before)).classes
|
||||
const after = parseHeap(await json(action.after)).classes
|
||||
return {
|
||||
classes: Array.from(new Set([...before.keys(), ...after.keys()]))
|
||||
.map((name) => ({
|
||||
name,
|
||||
countDelta: (after.get(name)?.count ?? 0) - (before.get(name)?.count ?? 0),
|
||||
bytesDelta: (after.get(name)?.bytes ?? 0) - (before.get(name)?.bytes ?? 0),
|
||||
}))
|
||||
.filter((item) => item.countDelta || item.bytesDelta)
|
||||
.sort((a, b) => Math.abs(b.bytesDelta) - Math.abs(a.bytesDelta))
|
||||
.slice(0, action.limit ?? 100),
|
||||
}
|
||||
}
|
||||
const value = await json(action.fileID)
|
||||
if (action.type === "trace.analyze") return analyzeTrace(value, action.limit)
|
||||
if (action.type === "cpu.analyze") return analyzeCpu(value, action.limit)
|
||||
const heap = parseHeap(value)
|
||||
if (action.type === "heap.summary") return heap.summary(action.limit)
|
||||
if (action.type === "heap.query") return heap.query(action.name, action.limit)
|
||||
return heap.object(action.id, action.limit)
|
||||
},
|
||||
async dispose() {
|
||||
if (recording?.owner === contents) await recording.finish().catch(() => undefined)
|
||||
if (cpu && !cpu.result) await stopCpu().catch(() => undefined)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -28,7 +28,7 @@ export const api: ElectronAPI = {
|
|||
browserPane: {
|
||||
request: (request) => invoke("BrowserPane", { request }),
|
||||
send: (request) => send("BrowserPane", { request }),
|
||||
onEvent: (callback) => listen("BrowserPaneEvent", (value) => callback(mutable(value))),
|
||||
onEvent: (callback) => listen("BrowserPaneEvent", (value) => callback(value)),
|
||||
},
|
||||
wslServers: {
|
||||
getState: () => invoke("WslGetState").then(mutable),
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ const endpoint = Schema.Struct({
|
|||
})
|
||||
const target = Schema.Struct({ sessionID: text(256).check(Schema.isStartsWith("ses")), endpoint })
|
||||
const bounds = Schema.Struct({ x: Schema.Finite, y: Schema.Finite, width: Schema.Finite, height: Schema.Finite })
|
||||
const layout = Schema.Struct({ visible: Schema.Boolean, bounds: Schema.optionalKey(bounds) })
|
||||
const layout = Schema.Struct({ tabID: Browser.TabID, visible: Schema.Boolean, bounds: Schema.optionalKey(bounds) })
|
||||
export const BrowserPaneRequestSchema = Schema.Union([
|
||||
Schema.Struct({ type: Schema.Literal("register"), bindingID, target }),
|
||||
Schema.Struct({ type: Schema.Literal("layout"), bindingID, layout: Schema.optionalKey(layout) }),
|
||||
|
|
@ -21,7 +21,7 @@ export const BrowserPaneRequestSchema = Schema.Union([
|
|||
export type BrowserPaneRequest = Schema.Schema.Type<typeof BrowserPaneRequestSchema>
|
||||
|
||||
export const BrowserPaneEventSchema = Schema.Union([
|
||||
Schema.Struct({ type: Schema.Literal("open") }),
|
||||
Schema.Struct({ type: Schema.Literal("focus"), tabID: Browser.TabID }),
|
||||
Schema.Struct({
|
||||
type: Schema.Literal("state"),
|
||||
state: Schema.NullOr(Browser.State),
|
||||
|
|
|
|||
58
packages/desktop/test/browser-analysis.test.ts
Normal file
58
packages/desktop/test/browser-analysis.test.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import { expect, test } from "bun:test"
|
||||
import { analyzeCpu, analyzeTrace, parseHeap } from "../src/main/browser/analysis"
|
||||
|
||||
test("trace analysis reports observed durations without inventing Web Vitals", () => {
|
||||
const result = analyzeTrace({
|
||||
traceEvents: [
|
||||
{ name: "RunTask", ph: "X", dur: 75_000 },
|
||||
{ name: "Paint", ph: "X", dur: 2_500 },
|
||||
{ name: "Paint", ph: "X", dur: 1_000 },
|
||||
{ name: "marker", ph: "i" },
|
||||
],
|
||||
})
|
||||
expect(result.metrics).toEqual([
|
||||
{ name: "recordedEvents", value: 4, unit: "count" },
|
||||
{ name: "longTasks", value: 1, unit: "count" },
|
||||
{ name: "longTaskBlocking", value: 25, unit: "ms" },
|
||||
])
|
||||
expect(result.events).toContainEqual({ name: "Paint", count: 2, totalMs: 3.5, maxMs: 2.5 })
|
||||
})
|
||||
|
||||
test("CPU analysis attributes sample intervals to their functions", () => {
|
||||
const result = analyzeCpu({
|
||||
startTime: 0,
|
||||
endTime: 6000,
|
||||
nodes: [
|
||||
{ id: 1, callFrame: { functionName: "first", url: "a.js", lineNumber: 0 } },
|
||||
{ id: 2, callFrame: { functionName: "second", url: "b.js", lineNumber: 2 } },
|
||||
],
|
||||
samples: [1, 2, 1],
|
||||
timeDeltas: [1000, 2000, 3000],
|
||||
})
|
||||
expect(result.durationMs).toBe(6)
|
||||
expect(result.functions).toEqual([
|
||||
{ name: "first", url: "a.js", line: 1, selfMs: 4 },
|
||||
{ name: "second", url: "b.js", line: 3, selfMs: 2 },
|
||||
])
|
||||
})
|
||||
|
||||
test("heap queries and object links use snapshot IDs and shallow sizes", () => {
|
||||
const heap = parseHeap({
|
||||
snapshot: {
|
||||
meta: {
|
||||
node_fields: ["type", "name", "id", "self_size", "edge_count"],
|
||||
node_types: [["object"], "string", "number", "number", "number"],
|
||||
edge_fields: ["type", "name_or_index", "to_node"],
|
||||
edge_types: [["property"], "string", "node"],
|
||||
},
|
||||
},
|
||||
nodes: [0, 0, 1, 10, 1, 0, 1, 3, 20, 0],
|
||||
edges: [0, 2, 5],
|
||||
strings: ["root", "child", "next"],
|
||||
})
|
||||
expect(heap.summary()).toMatchObject({ nodes: 2, edges: 1, selfBytes: 30 })
|
||||
expect(heap.query("", 1)).toMatchObject({ nodes: [{ id: 3, selfBytes: 20 }], truncated: true })
|
||||
expect(heap.object(1).references).toMatchObject([{ name: "next", node: { id: 3 } }])
|
||||
expect(heap.object(3).retainers).toMatchObject([{ name: "next", node: { id: 1 } }])
|
||||
expect(() => heap.object(99)).toThrow("Object ID was not found")
|
||||
})
|
||||
105
packages/desktop/test/browser-native.test.ts
Normal file
105
packages/desktop/test/browser-native.test.ts
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
import { test, expect } from "bun:test"
|
||||
import { mkdir, mkdtemp, rm } from "node:fs/promises"
|
||||
import path from "node:path"
|
||||
import { tmpdir } from "node:os"
|
||||
|
||||
test("browser suite over authenticated HTTP RPC with separate server and desktop storage", async () => {
|
||||
const root = await mkdtemp(path.join(tmpdir(), "opencode-browser-suite-"))
|
||||
const output = await mkdtemp(path.resolve(import.meta.dir, "../node_modules/.browser-suite-"))
|
||||
const names = ["home", "config", "data", "cache", "state", "server-files", "client-files", "electron-data"]
|
||||
await Promise.all(names.map((name) => mkdir(path.join(root, name), { recursive: true })))
|
||||
const built = await Bun.build({
|
||||
entrypoints: [path.join(import.meta.dir, "browser/native.ts")],
|
||||
outdir: output,
|
||||
naming: "native.mjs",
|
||||
target: "node",
|
||||
format: "esm",
|
||||
external: ["electron", "lighthouse", "puppeteer-core"],
|
||||
})
|
||||
if (!built.success) throw new AggregateError(built.logs)
|
||||
const environment = {
|
||||
...process.env,
|
||||
OPENCODE_TEST_HOME: path.join(root, "home"),
|
||||
OPENCODE_CONFIG_DIR: path.join(root, "config"),
|
||||
XDG_CONFIG_HOME: path.join(root, "config"),
|
||||
XDG_DATA_HOME: path.join(root, "data"),
|
||||
XDG_CACHE_HOME: path.join(root, "cache"),
|
||||
XDG_STATE_HOME: path.join(root, "state"),
|
||||
SMOKE_PASSWORD: crypto.randomUUID(),
|
||||
SMOKE_SERVER_FILES: path.join(root, "server-files"),
|
||||
SMOKE_ROOT: root,
|
||||
ELECTRON_RUN_AS_NODE: undefined,
|
||||
}
|
||||
const ready = Promise.withResolvers<string>()
|
||||
const server = Bun.spawn([process.execPath, path.join(import.meta.dir, "browser/server.ts")], {
|
||||
cwd: root,
|
||||
env: { ...environment, TMP: path.join(root, "server-files"), TEMP: path.join(root, "server-files") },
|
||||
stdout: "inherit",
|
||||
stderr: "inherit",
|
||||
ipc(message: unknown) {
|
||||
if (typeof message === "string") ready.resolve(message)
|
||||
},
|
||||
})
|
||||
void server.exited.then((code) => ready.reject(new Error(`Fixture server exited: ${code}`)))
|
||||
let native: ReturnType<typeof Bun.spawn> | undefined
|
||||
let proxy: Bun.Server<undefined> | undefined
|
||||
try {
|
||||
const url = await Promise.race([
|
||||
ready.promise,
|
||||
new Promise<never>((_, reject) =>
|
||||
setTimeout(() => reject(new Error("Fixture server startup timed out")), 30_000).unref(),
|
||||
),
|
||||
])
|
||||
// Exercise the real HTTP boundary with delayed state acknowledgments, as on
|
||||
// a remote server. Neither endpoint can rely on synchronous UI/state updates.
|
||||
proxy = Bun.serve({
|
||||
hostname: "127.0.0.1",
|
||||
port: 0,
|
||||
idleTimeout: 0,
|
||||
async fetch(request) {
|
||||
const incoming = new URL(request.url)
|
||||
if (incoming.pathname.endsWith("/experimental.browser/state"))
|
||||
await new Promise((resolve) => setTimeout(resolve, 75))
|
||||
return fetch(new Request(new URL(incoming.pathname + incoming.search, url), request), { decompress: false })
|
||||
},
|
||||
})
|
||||
const electron: unknown = (await import("electron")).default
|
||||
if (typeof electron !== "string") throw new Error("Electron binary path is unavailable.")
|
||||
native = Bun.spawn([electron, path.join(output, "native.mjs")], {
|
||||
cwd: output,
|
||||
env: {
|
||||
...environment,
|
||||
SMOKE_URL: proxy.url.href,
|
||||
TMP: path.join(root, "client-files"),
|
||||
TEMP: path.join(root, "client-files"),
|
||||
},
|
||||
stdout: "inherit",
|
||||
stderr: "inherit",
|
||||
})
|
||||
expect(
|
||||
await Promise.race([
|
||||
native.exited,
|
||||
new Promise<never>((_, reject) =>
|
||||
setTimeout(() => reject(new Error("Native browser suite timed out")), 140_000).unref(),
|
||||
),
|
||||
]),
|
||||
).toBe(0)
|
||||
} finally {
|
||||
if (native?.exitCode === null) {
|
||||
native.kill()
|
||||
await native.exited
|
||||
}
|
||||
if (server.exitCode === null) server.send("stop")
|
||||
proxy?.stop(true)
|
||||
await Promise.race([
|
||||
server.exited,
|
||||
new Promise<void>((resolve) =>
|
||||
setTimeout(() => {
|
||||
if (server.exitCode === null) server.kill()
|
||||
resolve()
|
||||
}, 10_000).unref(),
|
||||
),
|
||||
])
|
||||
await Promise.all([rm(root, { recursive: true, force: true }), rm(output, { recursive: true, force: true })])
|
||||
}
|
||||
}, 180_000)
|
||||
16
packages/desktop/test/browser/contract.ts
Normal file
16
packages/desktop/test/browser/contract.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { Rpc } from "@opencode-ai/schema/rpc"
|
||||
import { Session } from "@opencode-ai/schema/session"
|
||||
import { Schema } from "effect"
|
||||
|
||||
export const Smoke = Rpc.define({
|
||||
id: "browser.smoke",
|
||||
methods: {
|
||||
execute: {
|
||||
input: Schema.Struct({ sessionID: Session.ID, code: Schema.String }),
|
||||
output: Schema.Struct({ output: Schema.String, error: Schema.optionalKey(Schema.Boolean) }),
|
||||
},
|
||||
write: { input: Schema.Struct({ text: Schema.String }), output: Schema.String },
|
||||
read: { input: Schema.Struct({ path: Schema.String }), output: Schema.String },
|
||||
},
|
||||
events: {},
|
||||
})
|
||||
313
packages/desktop/test/browser/native.ts
Normal file
313
packages/desktop/test/browser/native.ts
Normal file
|
|
@ -0,0 +1,313 @@
|
|||
import assert from "node:assert/strict"
|
||||
import { createServer } from "node:http"
|
||||
import { once } from "node:events"
|
||||
import path from "node:path"
|
||||
import { app, BrowserWindow } from "electron"
|
||||
import { Browser } from "@opencode-ai/schema/browser"
|
||||
import { OpenCode } from "@opencode-ai/client"
|
||||
import { Effect, Fiber, Schema, Stream } from "effect"
|
||||
import { createBrowserPane } from "../../src/main/browser-pane"
|
||||
import { bindIpcEvents, ipcEventStream } from "../../src/main/ipc-events"
|
||||
import { Smoke } from "./contract"
|
||||
|
||||
type Output<Name extends Browser.Method> = Schema.Schema.Type<Extract<Browser.Operation, { name: Name }>["output"]>
|
||||
|
||||
// Fail the disposable fixture in stderr instead of opening Electron's error dialog.
|
||||
process.on("uncaughtException", (error) => {
|
||||
console.error(error)
|
||||
app.exit(1)
|
||||
})
|
||||
process.on("unhandledRejection", (error) => {
|
||||
console.error(error)
|
||||
app.exit(1)
|
||||
})
|
||||
|
||||
async function main() {
|
||||
const root = process.env.SMOKE_ROOT!
|
||||
app.setPath("userData", path.join(root, "electron-data"))
|
||||
app.on("window-all-closed", () => {})
|
||||
await app.whenReady()
|
||||
const web = createServer((request, response) => {
|
||||
if (request.url === "/api/test") {
|
||||
response.setHeader("content-type", "application/json")
|
||||
response.end('{"message":"network body"}')
|
||||
return
|
||||
}
|
||||
if (request.url === "/missing") {
|
||||
response.writeHead(404, { "content-type": "text/plain" })
|
||||
response.end("not found")
|
||||
return
|
||||
}
|
||||
if (request.url === "/download") {
|
||||
response.setHeader("content-disposition", 'attachment; filename="report.txt"')
|
||||
response.setHeader("content-type", "text/plain")
|
||||
response.end("desktop download bytes")
|
||||
return
|
||||
}
|
||||
if (request.url === "/frame") {
|
||||
response.setHeader("content-type", "text/html")
|
||||
response.end("<button onclick=\"this.textContent='Frame clicked'\">Frame button</button>")
|
||||
return
|
||||
}
|
||||
response.setHeader("content-type", "text/html")
|
||||
response.end(
|
||||
`<!doctype html><html lang="en"><head><title>Browser suite</title><meta name="description" content="Native browser test"><style>body{font:16px sans-serif;padding:20px}input,button,select{margin:6px}#space{height:1400px}</style></head><body><h1>Browser suite</h1><label>Name<input aria-label="Name"></label><button onclick="document.querySelector('output').textContent=document.querySelector('input').value">Apply</button><output>Waiting</output><input type="checkbox" aria-label="Remember"><select aria-label="Color"><option value="red">Red</option><option value="blue">Blue</option></select><input type="file" aria-label="Upload"><button onclick="alert('hello dialog')">Dialog</button><a href="/download">Download</a><a href="/frame" target="_blank">Popup</a><iframe title="Child frame" src="/frame"></iframe><div id="space">Scroll content</div><script>console.log('fixture log'); console.error('fixture error'); fetch('/api/test'); fetch('/missing'); window.heapFixture={value:'heap marker'};</script></body></html>`,
|
||||
)
|
||||
})
|
||||
await once(web.listen(0, "127.0.0.1"), "listening")
|
||||
const address = web.address()
|
||||
assert(address && typeof address !== "string")
|
||||
const fixture = `http://127.0.0.1:${address.port}`
|
||||
const client = OpenCode.make({
|
||||
baseUrl: process.env.SMOKE_URL!,
|
||||
headers: { authorization: `Basic ${Buffer.from(`opencode:${process.env.SMOKE_PASSWORD}`).toString("base64")}` },
|
||||
})
|
||||
const location = { directory: process.env.SMOKE_SERVER_FILES! }
|
||||
const rpc = client.rpc(Smoke)
|
||||
const session = await client.session.create({ title: "Browser suite", location })
|
||||
const pane = createBrowserPane()
|
||||
const win = new BrowserWindow({ show: false, width: 1100, height: 800, webPreferences: { sandbox: true } })
|
||||
const readyToShow = once(win, "ready-to-show")
|
||||
await win.loadURL("about:blank")
|
||||
await readyToShow
|
||||
win.setTitle("Browser suite (isolated test)")
|
||||
win.showInactive()
|
||||
await win.webContents.executeJavaScript(
|
||||
"new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(() => resolve(null))))",
|
||||
)
|
||||
const ipcErrors: string[] = []
|
||||
const inventories = new Map<string, Browser.State | null>()
|
||||
const unbind = await Effect.runPromise(bindIpcEvents(win.webContents.id))
|
||||
const events = Effect.runFork(
|
||||
ipcEventStream(win.webContents.id).pipe(
|
||||
Stream.runForEach((event) =>
|
||||
Effect.sync(() => {
|
||||
if (event._tag !== "BrowserPaneEvent") return
|
||||
if (event.event.type === "state") {
|
||||
inventories.set(event.bindingID, event.event.state)
|
||||
return
|
||||
}
|
||||
if (!inventories.get(event.bindingID)?.tabs.some((tab) => tab.id === event.event.tabID))
|
||||
ipcErrors.push("Focus arrived before its tab inventory")
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
const visited = new Set<Browser.Method>()
|
||||
async function call<Name extends Browser.Method>(
|
||||
name: Name,
|
||||
input: Omit<Extract<Browser.Action, { type: Name }>, "type">,
|
||||
): Promise<Output<Name>> {
|
||||
console.log(`BROWSER ${name}`)
|
||||
const result = await rpc.execute(
|
||||
{ sessionID: session.id, code: `return await tools.browser.${name}(${JSON.stringify(input)})` },
|
||||
{ location },
|
||||
)
|
||||
assert(!result.error, result.output)
|
||||
const operation = Browser.Operations.find((operation) => operation.name === name)
|
||||
assert(operation)
|
||||
visited.add(name)
|
||||
return Schema.decodeUnknownSync(operation.output)(JSON.parse(result.output)) as Output<Name>
|
||||
}
|
||||
try {
|
||||
await pane.register(win, "suite", {
|
||||
sessionID: session.id,
|
||||
endpoint: { url: process.env.SMOKE_URL!, password: process.env.SMOKE_PASSWORD },
|
||||
})
|
||||
const first = await call("tabs.open", { url: fixture })
|
||||
const second = await call("tabs.open", { url: `${fixture}/other`, focus: false })
|
||||
assert.equal((await call("tabs.list", {})).tabs.length, 2)
|
||||
const tabID = first.id
|
||||
await call("evaluate", {
|
||||
tabID,
|
||||
script: `(async () => {
|
||||
const source=document.createElement('div'); source.draggable=true; source.tabIndex=0; source.setAttribute('role','button'); source.textContent='Drag source'; source.ondragstart=e=>e.dataTransfer.setData('text/plain','element dropped'); document.body.prepend(source);
|
||||
const drop=document.createElement('div'); drop.tabIndex=0; drop.setAttribute('role','button'); drop.textContent='Drop target'; drop.style.cssText='height:40px;width:300px;background:#ddd'; drop.ondragover=e=>e.preventDefault(); drop.ondrop=async e=>{e.preventDefault();document.querySelector('output').textContent=e.dataTransfer.files.length?await e.dataTransfer.files[0].text():e.dataTransfer.getData('text/plain')}; document.body.prepend(drop);
|
||||
await new Promise(resolve=>{const frame=document.querySelector('iframe'); frame.onload=()=>resolve(null); frame.src=${JSON.stringify(`http://localhost:${address.port}/frame`)};});
|
||||
})()`,
|
||||
})
|
||||
pane.layout(win, "suite", { tabID, visible: true, bounds: { x: 0, y: 0, width: 1000, height: 700 } })
|
||||
await call("tabs.focus", { tabID: second.id })
|
||||
pane.layout(win, "suite", { tabID: second.id, visible: true, bounds: { x: 0, y: 0, width: 1000, height: 700 } })
|
||||
const snap = await call("snapshot", { tabID, boxes: true })
|
||||
const ref = (text: string) => {
|
||||
const match = snap.content
|
||||
.split("\n")
|
||||
.find((line) => /@e\d+/.test(line) && line.includes(`"${text}"`))
|
||||
?.match(/@e\d+/)?.[0]
|
||||
assert(match, `Missing ${text}: ${snap.content}`)
|
||||
return Browser.Ref.make(match)
|
||||
}
|
||||
await call("fill", { tabID, ref: ref("Name"), text: "remote browser" })
|
||||
await call("hover", { tabID, ref: ref("Apply") })
|
||||
await call("click", { tabID, ref: ref("Apply") })
|
||||
assert.equal(
|
||||
(await call("evaluate", { tabID, script: "document.querySelector('output').textContent" })).value,
|
||||
"remote browser",
|
||||
)
|
||||
assert.equal(
|
||||
(await call("evaluate", { tabID: second.id, script: "document.querySelector('input').value" })).value,
|
||||
"",
|
||||
)
|
||||
assert.equal((await call("tabs.list", {})).focusedTabID, second.id)
|
||||
const wrongTab = await rpc.execute(
|
||||
{
|
||||
sessionID: session.id,
|
||||
code: `return await tools.browser.click({tabID:${JSON.stringify(second.id)},ref:${JSON.stringify(ref("Apply"))}})`,
|
||||
},
|
||||
{ location },
|
||||
)
|
||||
assert(wrongTab.error && wrongTab.output.includes("another tab"))
|
||||
await call("check", { tabID, ref: ref("Remember"), checked: true })
|
||||
await call("select", { tabID, ref: ref("Color"), values: ["blue"] })
|
||||
await call("fill_form", {
|
||||
tabID,
|
||||
fields: [
|
||||
{ type: "text", ref: ref("Name"), value: "batch" },
|
||||
{ type: "check", ref: ref("Remember"), checked: false },
|
||||
],
|
||||
})
|
||||
await call("press", { tabID, key: "Tab" })
|
||||
await call("scroll", { tabID, deltaY: 100 })
|
||||
await call("wait", { tabID, condition: "text", text: "Scroll content" })
|
||||
await call("drag", { tabID, from: ref("Drag source"), to: ref("Drop target") })
|
||||
assert.equal(
|
||||
(await call("evaluate", { tabID, script: "document.querySelector('output').textContent" })).value,
|
||||
"element dropped",
|
||||
)
|
||||
const frames = await call("frames", { tabID })
|
||||
const child = frames.frames.find((frame) => frame.parentID)
|
||||
assert(child)
|
||||
assert.equal(
|
||||
(await call("evaluate", { tabID, frameID: child.id, script: "document.querySelector('button').textContent" }))
|
||||
.value,
|
||||
"Frame button",
|
||||
)
|
||||
const childSnapshot = await call("snapshot", { tabID, frameID: child.id })
|
||||
const childRef = childSnapshot.content
|
||||
.split("\n")
|
||||
.find((line) => line.includes('[button] "Frame button"'))
|
||||
?.match(/@e\d+/)?.[0]
|
||||
assert(childRef, childSnapshot.content)
|
||||
await call("click", { tabID, ref: Browser.Ref.make(childRef) })
|
||||
assert.equal(
|
||||
(await call("evaluate", { tabID, frameID: child.id, script: "document.querySelector('button').textContent" }))
|
||||
.value,
|
||||
"Frame clicked",
|
||||
)
|
||||
const found = await call("find", { tabID, text: "Apply" })
|
||||
assert(found.content.includes("Apply"))
|
||||
const screenshot = await call("screenshot", { tabID, fullPage: true, maxWidth: 1000 })
|
||||
const screenshotBytes = await rpc.read({ path: screenshot.files[0].path }, { location })
|
||||
assert(
|
||||
Buffer.from(screenshotBytes, "base64")
|
||||
.subarray(0, 8)
|
||||
.equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10])),
|
||||
)
|
||||
assert(screenshot.files[0].path.startsWith(process.env.SMOKE_SERVER_FILES!))
|
||||
const logs = await call("console", { tabID, level: "debug" })
|
||||
assert(logs.messages.some((message) => message.text.includes("fixture error")))
|
||||
const network = await call("network.list", { tabID, urlContains: "/api/test" })
|
||||
assert(network.requests.length)
|
||||
const detail = await call("network.get", { tabID, id: network.requests[0].id, includeBody: true })
|
||||
assert.equal(detail.responseBody.state, "text")
|
||||
const upload = await rpc.write({ text: "server upload bytes" }, { location })
|
||||
const fileSnap = await call("snapshot", { tabID })
|
||||
const input = fileSnap.content
|
||||
.split("\n")
|
||||
.find((line) => line.includes('"Upload"'))
|
||||
?.match(/@e\d+/)?.[0]
|
||||
assert(input, fileSnap.content)
|
||||
await call("files.upload", { tabID, ref: Browser.Ref.make(input), paths: [upload] })
|
||||
assert.equal(
|
||||
(await call("evaluate", { tabID, script: "document.querySelector('input[type=file]').files[0].name" })).value,
|
||||
"upload.txt",
|
||||
)
|
||||
assert.equal(
|
||||
(await call("evaluate", { tabID, script: "document.querySelector('input[type=file]').files[0].text()" })).value,
|
||||
"server upload bytes",
|
||||
)
|
||||
const drop = fileSnap.content
|
||||
.split("\n")
|
||||
.find((line) => line.includes('[button] "Drop target"'))
|
||||
?.match(/@e\d+/)?.[0]
|
||||
assert(drop, fileSnap.content)
|
||||
await call("files.drop", { tabID, ref: Browser.Ref.make(drop), paths: [upload] })
|
||||
await call("wait", { tabID, condition: "text", text: "server upload bytes" })
|
||||
await call("evaluate", { tabID, script: "document.querySelector('a[href=\"/download\"]').click()" })
|
||||
const downloads = await until(async () => {
|
||||
const result = await call("files.list", { tabID })
|
||||
return result.files.find((file) => file.name === "report.txt" && file.state === "completed")
|
||||
})
|
||||
const download = await call("files.get", { tabID, fileID: downloads.id })
|
||||
assert.equal(
|
||||
Buffer.from(await rpc.read({ path: download.files[0].path }, { location }), "base64").toString(),
|
||||
"desktop download bytes",
|
||||
)
|
||||
await call("evaluate", { tabID, script: "setTimeout(()=>alert('hello dialog'),0); null" })
|
||||
await until(async () => (await call("dialog", { tabID, action: "get" })).dialog)
|
||||
await call("dialog", { tabID, action: "dismiss" })
|
||||
await call("evaluate", { tabID, script: "window.open('/frame'); null" })
|
||||
await until(async () => (await call("tabs.list", {})).tabs.length === 3)
|
||||
await call("navigate", { tabID, url: `${fixture}/next` })
|
||||
await call("back", { tabID })
|
||||
await call("forward", { tabID })
|
||||
await call("trace.start", { tabID, durationMs: 30_000 })
|
||||
await call("reload", { tabID })
|
||||
await call("evaluate", {
|
||||
tabID,
|
||||
script: "performance.mark('browser-suite-marker'); let n=0; for(let i=0;i<100000;i++) n+=i; n",
|
||||
})
|
||||
const trace = await call("trace.stop", { tabID })
|
||||
const traceAnalysis = await call("trace.analyze", { tabID, fileID: trace.files[0].id })
|
||||
assert(traceAnalysis.metrics[0].value > 0)
|
||||
await call("cpu.start", { tabID })
|
||||
await call("evaluate", { tabID, script: "Array.from({length:100000},(_,i)=>Math.sqrt(i)).reduce((a,b)=>a+b,0)" })
|
||||
const cpu = await call("cpu.stop", { tabID })
|
||||
await call("cpu.analyze", { tabID, fileID: cpu.files[0].id })
|
||||
const heap = await call("heap.snapshot", { tabID })
|
||||
const heapID = heap.files[0].id
|
||||
assert((await call("heap.summary", { tabID, fileID: heapID })).nodes > 0)
|
||||
const query = await call("heap.query", { tabID, fileID: heapID, name: "Object", limit: 3 })
|
||||
assert(query.nodes.length)
|
||||
await call("heap.object", { tabID, fileID: heapID, id: query.nodes[0].id })
|
||||
assert.equal((await call("heap.compare", { tabID, before: heapID, after: heapID })).classes.length, 0)
|
||||
const audit = await call("lighthouse", { tabID })
|
||||
assert(audit.scores.some((score) => score.id === "accessibility"))
|
||||
await call("stop", { tabID })
|
||||
await call("tabs.close", { tabID: second.id })
|
||||
assert.equal((await call("tabs.list", {})).tabs.length, 2)
|
||||
assert.deepEqual(
|
||||
Browser.Operations.map((operation) => operation.name).filter((name) => !visited.has(name)),
|
||||
[],
|
||||
)
|
||||
assert.deepEqual(ipcErrors, [])
|
||||
console.log(
|
||||
`PASS ${visited.size} browser operations over physical authenticated HTTP, including file bytes in both directions`,
|
||||
)
|
||||
} finally {
|
||||
await pane.close(win, "suite")
|
||||
await pane.dispose()
|
||||
await Effect.runPromise(Fiber.interrupt(events))
|
||||
await Effect.runPromise(unbind)
|
||||
win.destroy()
|
||||
web.closeAllConnections()
|
||||
await new Promise<void>((resolve) => web.close(() => resolve()))
|
||||
app.quit()
|
||||
}
|
||||
}
|
||||
|
||||
async function until<T>(read: () => Promise<T>) {
|
||||
const deadline = Date.now() + 10_000
|
||||
while (true) {
|
||||
const value = await read()
|
||||
if (value) return value
|
||||
if (Date.now() > deadline) throw new Error("Condition timed out")
|
||||
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error)
|
||||
app.exit(1)
|
||||
})
|
||||
55
packages/desktop/test/browser/plugin/index.ts
Normal file
55
packages/desktop/test/browser/plugin/index.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import { Plugin } from "@opencode-ai/plugin/effect"
|
||||
import { Agent } from "@opencode-ai/schema/agent"
|
||||
import { SessionMessage } from "@opencode-ai/schema/session-message"
|
||||
import { Tool } from "@opencode-ai/schema/tool"
|
||||
import { CodeModeTool } from "../../../../core/src/codemode/tool"
|
||||
import { execute } from "../../../../core/src/tool/runtime"
|
||||
import { Effect, Schema } from "effect"
|
||||
import path from "node:path"
|
||||
import { readFile, writeFile } from "node:fs/promises"
|
||||
import { Smoke } from "../contract"
|
||||
|
||||
export default Plugin.define({
|
||||
id: "browser.smoke",
|
||||
effect: (ctx) =>
|
||||
Effect.gen(function* () {
|
||||
const tools = new Map<string, Tool.Info>()
|
||||
yield* ctx.tool
|
||||
.transform((editor) => {
|
||||
editor
|
||||
.list()
|
||||
.filter((tool) => tool.options?.namespace?.startsWith("browser"))
|
||||
.forEach((tool) => tools.set(tool.id, tool))
|
||||
})
|
||||
.pipe(Effect.orDie)
|
||||
yield* ctx.rpc
|
||||
.register(Smoke, {
|
||||
execute: (input) =>
|
||||
Effect.gen(function* () {
|
||||
const result = yield* CodeModeTool.create({ tools }, (name, tool, input, context) =>
|
||||
execute(tool, input, context),
|
||||
).execute(
|
||||
{ code: input.code },
|
||||
{
|
||||
sessionID: input.sessionID,
|
||||
agent: Agent.ID.make("build"),
|
||||
messageID: SessionMessage.ID.create(),
|
||||
id: Tool.CallID.make(crypto.randomUUID()),
|
||||
progress: () => Effect.void,
|
||||
},
|
||||
)
|
||||
return Schema.decodeUnknownSync(
|
||||
Schema.Struct({ output: Schema.String, error: Schema.optionalKey(Schema.Boolean) }),
|
||||
)(result.output)
|
||||
}).pipe(Effect.orDie),
|
||||
write: ({ text }) =>
|
||||
Effect.promise(async () => {
|
||||
const file = path.join(process.env.SMOKE_SERVER_FILES!, "upload.txt")
|
||||
await writeFile(file, text)
|
||||
return file
|
||||
}),
|
||||
read: ({ path }) => Effect.promise(async () => (await readFile(path)).toString("base64")),
|
||||
})
|
||||
.pipe(Effect.orDie)
|
||||
}),
|
||||
})
|
||||
31
packages/desktop/test/browser/server.ts
Normal file
31
packages/desktop/test/browser/server.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import path from "node:path"
|
||||
import { Effect } from "effect"
|
||||
import { HttpServer } from "effect/unstable/http"
|
||||
import { ServerProcess } from "../../../server/src/process"
|
||||
|
||||
const stopped = Promise.withResolvers<void>()
|
||||
process.on("message", (message) => {
|
||||
if (message === "stop") stopped.resolve()
|
||||
})
|
||||
await Effect.gen(function* () {
|
||||
const server = yield* ServerProcess.start({
|
||||
hostname: "127.0.0.1",
|
||||
port: 0,
|
||||
password: process.env.SMOKE_PASSWORD,
|
||||
app: { name: "browser-suite-test", version: "test", channel: "test" },
|
||||
database: { path: ":memory:" },
|
||||
models: { fetch: false },
|
||||
fs: { filewatcher: false, fff: false },
|
||||
config: {
|
||||
directory: process.env.OPENCODE_CONFIG_DIR!,
|
||||
project: false,
|
||||
content: JSON.stringify({
|
||||
plugins: ["-opencode.provider.*", path.join(import.meta.dir, "plugin")],
|
||||
permissions: [{ action: "*", resource: "*", effect: "allow" }],
|
||||
}),
|
||||
},
|
||||
})
|
||||
process.send?.(HttpServer.formatAddress(server.address))
|
||||
yield* Effect.promise(() => stopped.promise)
|
||||
}).pipe(Effect.scoped, Effect.runPromise)
|
||||
process.disconnect?.()
|
||||
Loading…
Add table
Add a link
Reference in a new issue