mirror of
https://github.com/anomalyco/opencode.git
synced 2026-10-03 05:56:18 +00:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95dd0c5e0c | ||
|
|
56145fce05 | ||
|
|
4f0d6d0ab6 | ||
|
|
5fdeed271d | ||
|
|
ab5d49666a |
No files matched your search
@@ -194,7 +194,7 @@
|
||||
"zod": "catalog:",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"effect": "4.0.0-rc.112",
|
||||
"effect": "4.0.0",
|
||||
"solid-js": ">=1.9.0",
|
||||
},
|
||||
"optionalPeers": [
|
||||
@@ -406,6 +406,7 @@
|
||||
"@types/node": "catalog:",
|
||||
"@types/which": "3.0.4",
|
||||
"drizzle-kit": "catalog:",
|
||||
"fast-check": "4.9.0",
|
||||
},
|
||||
},
|
||||
"packages/desktop": {
|
||||
@@ -441,7 +442,7 @@
|
||||
"electron-builder": "26.15.7",
|
||||
"electron-vite": "6.0.0-beta.1",
|
||||
"solid-js": "catalog:",
|
||||
"typescript": "~5.6.2",
|
||||
"typescript": "catalog:",
|
||||
"vite": "8.2.2",
|
||||
},
|
||||
"optionalDependencies": {
|
||||
@@ -555,7 +556,7 @@
|
||||
"name": "@opencode/http-recorder",
|
||||
"version": "2.0.21",
|
||||
"dependencies": {
|
||||
"@effect/platform-node-shared": "4.0.0-rc.112",
|
||||
"@effect/platform-node-shared": "4.0.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@effect/platform-node": "catalog:",
|
||||
@@ -1147,10 +1148,10 @@
|
||||
"catalog": {
|
||||
"@cloudflare/workers-types": "4.20251008.0",
|
||||
"@corvu/drawer": "0.2.4",
|
||||
"@effect/opentelemetry": "4.0.0-rc.112",
|
||||
"@effect/platform-node": "4.0.0-rc.112",
|
||||
"@effect/platform-node-shared": "4.0.0-rc.112",
|
||||
"@effect/sql-sqlite-bun": "4.0.0-rc.112",
|
||||
"@effect/opentelemetry": "4.0.0",
|
||||
"@effect/platform-node": "4.0.0",
|
||||
"@effect/platform-node-shared": "4.0.0",
|
||||
"@effect/sql-sqlite-bun": "4.0.0",
|
||||
"@hono/standard-validator": "0.2.0",
|
||||
"@hono/zod-validator": "0.4.2",
|
||||
"@kobalte/core": "0.13.13",
|
||||
@@ -1190,7 +1191,7 @@
|
||||
"dompurify": "3.4.14",
|
||||
"drizzle-kit": "1.0.0-rc.5-ab785fc",
|
||||
"drizzle-orm": "1.0.0-rc.5-169397b",
|
||||
"effect": "4.0.0-rc.112",
|
||||
"effect": "4.0.0",
|
||||
"fuzzysort": "3.1.0",
|
||||
"get-east-asian-width": "1.6.0",
|
||||
"hono": "4.10.7",
|
||||
@@ -1209,7 +1210,7 @@
|
||||
"sst": "4.13.1",
|
||||
"string-width": "7.2.0",
|
||||
"tailwindcss": "4.1.11",
|
||||
"typescript": "5.8.2",
|
||||
"typescript": "5.9.3",
|
||||
"ulid": "3.0.1",
|
||||
"vite": "7.3.6",
|
||||
"vite-plugin-solid": "2.11.10",
|
||||
@@ -1722,13 +1723,13 @@
|
||||
|
||||
"@drizzle-team/brocli": ["@drizzle-team/brocli@0.12.0", "", {}, "sha512-mlUE+rZ8CatQekLhnaiN91Iemdd+e2gFKooGlnRB3oPTL3VghLfX24dx7HrzMNeC1JrIB/0kpsfyty3f5HNfxQ=="],
|
||||
|
||||
"@effect/opentelemetry": ["@effect/opentelemetry@4.0.0-rc.112", "", { "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <2.0.0", "@opentelemetry/api-logs": ">=0.203.0 <0.300.0", "@opentelemetry/resources": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-logs": ">=0.203.0 <0.300.0", "@opentelemetry/sdk-metrics": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-trace-base": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-trace-node": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-trace-web": ">=2.0.0 <3.0.0", "@opentelemetry/semantic-conventions": ">=1.33.0 <2.0.0", "effect": "^4.0.0-rc.112" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/api-logs", "@opentelemetry/resources", "@opentelemetry/sdk-logs", "@opentelemetry/sdk-metrics", "@opentelemetry/sdk-trace-base", "@opentelemetry/sdk-trace-node", "@opentelemetry/sdk-trace-web"] }, "sha512-OTRv1DxTHUmnakgJ6XVM8wVgF1KgZH4UXnOemwSLUwUjXO+RCikzF8oR/rlVmOGq81KtQzj1URM4M4nchlQOuQ=="],
|
||||
"@effect/opentelemetry": ["@effect/opentelemetry@4.0.0", "", { "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <2.0.0", "@opentelemetry/api-logs": ">=0.203.0 <0.300.0", "@opentelemetry/resources": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-logs": ">=0.203.0 <0.300.0", "@opentelemetry/sdk-metrics": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-trace-base": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-trace-node": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-trace-web": ">=2.0.0 <3.0.0", "@opentelemetry/semantic-conventions": ">=1.33.0 <2.0.0", "effect": "^4.0.0" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/api-logs", "@opentelemetry/resources", "@opentelemetry/sdk-logs", "@opentelemetry/sdk-metrics", "@opentelemetry/sdk-trace-base", "@opentelemetry/sdk-trace-node", "@opentelemetry/sdk-trace-web"] }, "sha512-vXVhqlcADHtmKlw2gIqRRlXFve4ms4JJ5ykAqzNe37QQ/5N5A5gJ9VOXkMBORHZC5O4gdHZwxhAzGZb0zvFf/Q=="],
|
||||
|
||||
"@effect/platform-node": ["@effect/platform-node@4.0.0-rc.112", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-rc.112", "mime": "^4.1.0", "undici": "^8.10.0" }, "peerDependencies": { "effect": "^4.0.0-rc.112", "redis": ">=5.0.0 <7.0.0" } }, "sha512-/BMAcdNGQQskLmI0Zoa95KfTZkr9HV9N4NSxaSrusG6GeW6Ulp9KvZ+Rlaiw8lnOt43CXjFLdfll5/k5rxL4hQ=="],
|
||||
"@effect/platform-node": ["@effect/platform-node@4.0.0", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0", "undici": "^8.11.2" }, "peerDependencies": { "effect": "^4.0.0", "redis": ">=5.0.0 <7.0.0" } }, "sha512-/L+4MUbl8FejPKLHLROYSOebvHnfixkP8KiblSD/DE5uH5xENZBHaux4nH6XMnRP+lr8ajpEJFS8sUrmmxFJeQ=="],
|
||||
|
||||
"@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0-rc.112", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.21.3" }, "peerDependencies": { "effect": "^4.0.0-rc.112" } }, "sha512-ttjz0xKamFN7vL8pNDYVwddJLjZvqKePc05djlz2VcdaKbLsnYbtMnL1rbOfHgEnIUSHGh7FkjaN4DM1Ov81sQ=="],
|
||||
"@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.22.0" }, "peerDependencies": { "effect": "^4.0.0" } }, "sha512-VBXHJU9UXVZPdQGGDRjGXpo+TpOzRwZuMrdKAtIxi1qwcRftmacZ1zTgIrzBa0Z2RA2KVkoOecQUsTlt/nJZhw=="],
|
||||
|
||||
"@effect/sql-sqlite-bun": ["@effect/sql-sqlite-bun@4.0.0-rc.112", "", { "peerDependencies": { "effect": "^4.0.0-rc.112" } }, "sha512-EqR8pWZo3VzedRvP6qo5HcfqNmFvSa+sQLWQl3oBMx2KY1FCc8NYjep79wYjNkW0qABnffrPUXCMTOncd1A+5g=="],
|
||||
"@effect/sql-sqlite-bun": ["@effect/sql-sqlite-bun@4.0.0", "", { "peerDependencies": { "effect": "^4.0.0" } }, "sha512-tUDqqNq4lZMLQkp8YDQej2uuy8BBjmqfMXcJU/KOmrK/WWbzudC8oabZ1TEs1lLSuKzhBEPhnE0cRtAOYe05ew=="],
|
||||
|
||||
"@electron-internal/extract-zip": ["@electron-internal/extract-zip@1.0.5", "", {}, "sha512-+bqFCP98pLI0Tt0XQo1TmlXtwjWchISndDOxCkEcIuUgXWpBnLyRI+2DU+mesvnMMX6L1XDqYNA0lXNDHd/yiA=="],
|
||||
|
||||
@@ -2062,18 +2063,6 @@
|
||||
|
||||
"@motionone/utils": ["@motionone/utils@10.18.0", "", { "dependencies": { "@motionone/types": "^10.17.1", "hey-listen": "^1.0.8", "tslib": "^2.3.1" } }, "sha512-3XVF7sgyTSI2KWvTf6uLlBJ5iAgRgmvp3bpuOiQJvInd4nZ19ET8lX5unn30SlmRH7hXbBbH+Gxd0m0klJ3Xtw=="],
|
||||
|
||||
"@msgpackr-extract/msgpackr-extract-darwin-arm64": ["@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ=="],
|
||||
|
||||
"@msgpackr-extract/msgpackr-extract-darwin-x64": ["@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w=="],
|
||||
|
||||
"@msgpackr-extract/msgpackr-extract-linux-arm": ["@msgpackr-extract/msgpackr-extract-linux-arm@3.0.4", "", { "os": "linux", "cpu": "arm" }, "sha512-Tg3yX65f5GbtXLkrYEHE5oibZG9epyYWas7FogTTEJeDEF9JlXJzKgXaNhT3UXlTOeA+AfZpYZYZ0uPj7Cfquw=="],
|
||||
|
||||
"@msgpackr-extract/msgpackr-extract-linux-arm64": ["@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-dgX0P/9wGPJeHFBG+ZmhgE6bmtMt7NP5CRBGyyktpopdk/mW4POnrpQsSLtKI1dwpc+pPLuXHDh6vvskyQE/sw=="],
|
||||
|
||||
"@msgpackr-extract/msgpackr-extract-linux-x64": ["@msgpackr-extract/msgpackr-extract-linux-x64@3.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-8TNXMEjJc3QEy7R/x1INhgiU+XakDAFUzBhaz7+Rbrs8NH5UQeHQxxmzsSBJGyV6I1jW79undiQm8tOI+D+8FQ=="],
|
||||
|
||||
"@msgpackr-extract/msgpackr-extract-win32-x64": ["@msgpackr-extract/msgpackr-extract-win32-x64@3.0.4", "", { "os": "win32", "cpu": "x64" }, "sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ=="],
|
||||
|
||||
"@napi-rs/lzma-linux-x64-gnu": ["@napi-rs/lzma-linux-x64-gnu@1.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ=="],
|
||||
|
||||
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.2.3", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" } }, "sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q=="],
|
||||
@@ -3912,7 +3901,7 @@
|
||||
|
||||
"editorconfig": ["editorconfig@1.0.7", "", { "dependencies": { "@one-ini/wasm": "0.1.1", "commander": "^10.0.0", "minimatch": "^9.0.1", "semver": "^7.5.3" }, "bin": { "editorconfig": "bin/editorconfig" } }, "sha512-e0GOtq/aTQhVdNyDU9e02+wz9oDDM+SIOQxWME2QRjzRX5yyLAuHDE+0aE8vHb9XRC8XD37eO2u57+F09JqFhw=="],
|
||||
|
||||
"effect": ["effect@4.0.0-rc.112", "", { "dependencies": { "fast-check": "^4.9.0", "msgpackr": "^2.0.5" } }, "sha512-wXxwuh1Ywnv4cPRM3Wfa0vDwuOHnZ1TsTgHJkG9XgzND6inhBH9n1vBxhg3iIXOia/OrpmvVmd3lrD4vq6bF3A=="],
|
||||
"effect": ["effect@4.0.0", "", {}, "sha512-ooc1TG5t+FfzgYnFz2ff6BBKyZ7EwBRVXC7c4RhQUAD6/TZ2gTXXMeb4WX7a19ozQo4J73/QW+S00YAIresoMQ=="],
|
||||
|
||||
"ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="],
|
||||
|
||||
@@ -4750,7 +4739,7 @@
|
||||
|
||||
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
||||
|
||||
"mime": ["mime@4.1.0", "", { "bin": { "mime": "bin/cli.js" } }, "sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw=="],
|
||||
"mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="],
|
||||
|
||||
"mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
||||
|
||||
@@ -4798,10 +4787,6 @@
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"msgpackr": ["msgpackr@2.0.5", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.4" } }, "sha512-cef05H/dSYpLpqp3sj/qyZh5vhUYCalnaLO7j1yOmpsR0y/XwLVtK7r5gn+U/F7CTEfMowcGhlUQJDLcLf7jcA=="],
|
||||
|
||||
"msgpackr-extract": ["msgpackr-extract@3.0.4", "", { "dependencies": { "node-gyp-build-optional-packages": "5.2.2" }, "optionalDependencies": { "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.4", "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.4", "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.4" }, "bin": { "download-msgpackr-prebuilds": "bin/download-prebuilds.js" } }, "sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw=="],
|
||||
|
||||
"muggle-string": ["muggle-string@0.4.1", "", {}, "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ=="],
|
||||
|
||||
"mustache": ["mustache@4.2.0", "", { "bin": { "mustache": "bin/mustache" } }, "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ=="],
|
||||
@@ -4844,8 +4829,6 @@
|
||||
|
||||
"node-gyp-build": ["node-gyp-build@4.8.4", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="],
|
||||
|
||||
"node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.2.2", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-optional": "optional.js", "node-gyp-build-optional-packages-test": "build-test.js" } }, "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw=="],
|
||||
|
||||
"node-html-parser": ["node-html-parser@7.1.0", "", { "dependencies": { "css-select": "^5.1.0", "he": "1.2.0" } }, "sha512-iJo8b2uYGT40Y8BTyy5ufL6IVbN8rbm/1QK2xffXU/1a/v3AAa0d1YAoqBNYqaS4R/HajkWIpIfdE6KcyFh1AQ=="],
|
||||
|
||||
"node-int64": ["node-int64@0.4.0", "", {}, "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw=="],
|
||||
@@ -5628,7 +5611,7 @@
|
||||
|
||||
"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=="],
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"typescript-auto-import-cache": ["typescript-auto-import-cache@0.3.6", "", { "dependencies": { "semver": "^7.3.8" } }, "sha512-RpuHXrknHdVdK7wv/8ug3Fr0WNsNi5l5aB8MYYuXhq2UH5lnEB1htJ1smhtD5VeCsGr2p8mUDtd83LCQDFVgjQ=="],
|
||||
|
||||
@@ -5642,7 +5625,7 @@
|
||||
|
||||
"uncrypto": ["uncrypto@0.1.3", "", {}, "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="],
|
||||
|
||||
"undici": ["undici@8.10.0", "", {}, "sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ=="],
|
||||
"undici": ["undici@8.11.2", "", {}, "sha512-u4UB2/IrKdU6lFxumHmmo1a3fCQO5tzQllRorfoRS63txhrB7xTpSn1PftwC4qEHkOaqP95fCWW4lJzwErwzhQ=="],
|
||||
|
||||
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||
|
||||
@@ -6094,15 +6077,13 @@
|
||||
|
||||
"@bufbuild/protoplugin/typescript": ["typescript@5.4.5", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ=="],
|
||||
|
||||
"@cloudflare/kv-asset-handler/mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="],
|
||||
|
||||
"@cloudflare/vite-plugin/vite": ["vite@7.3.6", "", { "dependencies": { "esbuild": "^0.27.0 || ^0.28.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg=="],
|
||||
|
||||
"@cloudflare/vite-plugin/ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="],
|
||||
|
||||
"@cspotcode/source-map-support/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="],
|
||||
|
||||
"@effect/platform-node-shared/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=="],
|
||||
"@effect/platform-node-shared/ws": ["ws@8.22.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg=="],
|
||||
|
||||
"@electron/asar/commander": ["commander@5.1.0", "", {}, "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg=="],
|
||||
|
||||
@@ -6234,8 +6215,6 @@
|
||||
|
||||
"@opencode/console-support/vite": ["vite@7.3.6", "", { "dependencies": { "esbuild": "^0.27.0 || ^0.28.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg=="],
|
||||
|
||||
"@opencode/desktop/typescript": ["typescript@5.6.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw=="],
|
||||
|
||||
"@opencode/enterprise/@tailwindcss/vite": ["@tailwindcss/vite@4.1.11", "", { "dependencies": { "@tailwindcss/node": "4.1.11", "@tailwindcss/oxide": "4.1.11", "tailwindcss": "4.1.11" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-RHYhrR3hku0MJFRV+fN2gNbDNEh3dwKvY8XJvTxCSXeMOsCRSr+uKvDWQcbizrHgjML6ZmTE5OwMrl5wKcujCw=="],
|
||||
|
||||
"@opencode/enterprise/tailwindcss": ["tailwindcss@4.1.11", "", {}, "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA=="],
|
||||
@@ -6694,6 +6673,8 @@
|
||||
|
||||
"tw-to-css/tailwindcss": ["tailwindcss@3.3.2", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.5.3", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.2.12", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", "jiti": "^1.18.2", "lilconfig": "^2.1.0", "micromatch": "^4.0.5", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.0.0", "postcss": "^8.4.23", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", "postcss-load-config": "^4.0.1", "postcss-nested": "^6.0.1", "postcss-selector-parser": "^6.0.11", "postcss-value-parser": "^4.2.0", "resolve": "^1.22.2", "sucrase": "^3.32.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" } }, "sha512-9jPkMiIBXvPc2KywkraqsUfbfj+dHDb+JPWtSJa9MLFdrPyazI7q6WX2sUrm7R9eVR7qqv3Pas7EvQFzxKnI6w=="],
|
||||
|
||||
"unifont/undici": ["undici@8.10.0", "", {}, "sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
+6
-6
@@ -42,10 +42,10 @@
|
||||
"packages/stats/*"
|
||||
],
|
||||
"catalog": {
|
||||
"@effect/opentelemetry": "4.0.0-rc.112",
|
||||
"@effect/platform-node": "4.0.0-rc.112",
|
||||
"@effect/platform-node-shared": "4.0.0-rc.112",
|
||||
"@effect/sql-sqlite-bun": "4.0.0-rc.112",
|
||||
"@effect/opentelemetry": "4.0.0",
|
||||
"@effect/platform-node": "4.0.0",
|
||||
"@effect/platform-node-shared": "4.0.0",
|
||||
"@effect/sql-sqlite-bun": "4.0.0",
|
||||
"@npmcli/arborist": "9.4.0",
|
||||
"@types/bun": "1.4.0",
|
||||
"@types/cross-spawn": "6.0.6",
|
||||
@@ -79,7 +79,7 @@
|
||||
"dompurify": "3.4.14",
|
||||
"drizzle-kit": "1.0.0-rc.5-ab785fc",
|
||||
"drizzle-orm": "1.0.0-rc.5-169397b",
|
||||
"effect": "4.0.0-rc.112",
|
||||
"effect": "4.0.0",
|
||||
"ai": "6.0.168",
|
||||
"cross-spawn": "7.0.6",
|
||||
"hono": "4.10.7",
|
||||
@@ -92,7 +92,7 @@
|
||||
"remend": "1.3.1",
|
||||
"@playwright/test": "1.59.1",
|
||||
"semver": "7.7.4",
|
||||
"typescript": "5.8.2",
|
||||
"typescript": "5.9.3",
|
||||
"@typescript/native-preview": "7.0.0-dev.20251207.1",
|
||||
"zod": "4.1.8",
|
||||
"remeda": "2.26.0",
|
||||
|
||||
@@ -15,7 +15,7 @@ import { OpenAI } from "@opencode/ai/providers"
|
||||
* hover imports and local values to see how the public API is typed.
|
||||
*/
|
||||
|
||||
const apiKey = Config.redacted("OPENAI_API_KEY")
|
||||
const apiKey = Config.Redacted("OPENAI_API_KEY")
|
||||
|
||||
// 1. Configure a provider. The configured facade records provider identity,
|
||||
// deployment options, authentication, and defaults. Per-modality selectors pick
|
||||
|
||||
@@ -5,7 +5,7 @@ import * as path from "node:path"
|
||||
import * as prompts from "@clack/prompts"
|
||||
import { AwsV4Signer } from "aws4fetch"
|
||||
import { Config, ConfigProvider, Effect, FileSystem, PlatformError, Redacted } from "effect"
|
||||
import { FetchHttpClient, HttpClient, HttpClientRequest, type HttpClientResponse } from "effect/unstable/http"
|
||||
import { FetchHttpClient, HttpClient, HttpClientRequest, type HttpClientResponse } from "effect/http"
|
||||
import * as ProviderShared from "../src/protocols/shared"
|
||||
import { CloudflareAIGateway } from "../src/providers/cloudflare-ai-gateway.js"
|
||||
import { CloudflareWorkersAI } from "../src/providers/cloudflare-workers-ai.js"
|
||||
@@ -383,7 +383,7 @@ const readEnvFile = Effect.fn("RecordingEnv.readFile")(function* () {
|
||||
})
|
||||
|
||||
const readConfigString = (provider: ConfigProvider.ConfigProvider, name: string) =>
|
||||
Config.string(name)
|
||||
Config.String(name)
|
||||
.parse(provider)
|
||||
.pipe(
|
||||
Effect.match({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Effect, Schema } from "effect"
|
||||
import { Headers, HttpClientRequest } from "effect/unstable/http"
|
||||
import { Headers, HttpClientRequest } from "effect/http"
|
||||
import {
|
||||
ChoiceQuestion,
|
||||
EvaluationInput,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Effect, Schema, Stream } from "effect"
|
||||
import { extendSchema } from "@opencode/schema/schema"
|
||||
import { Generation, ProgressEvent, QueuedEvent, type AwaitOptions } from "./generation.js"
|
||||
import { Media } from "./media.js"
|
||||
import { MediaModel, composeRoute, tryRequest } from "./media-model.js"
|
||||
@@ -142,7 +143,7 @@ const imageEventTagged = Schema.Union([
|
||||
ImageOutputEvent,
|
||||
ImageFinishEvent,
|
||||
]).pipe(Schema.toTaggedUnion("type"))
|
||||
export const ImageEvent = Object.assign(imageEventTagged, {
|
||||
export const ImageEvent = extendSchema(imageEventTagged, {
|
||||
is: {
|
||||
generationQueued: imageEventTagged.guards["generation-queued"],
|
||||
generationProgress: imageEventTagged.guards["generation-progress"],
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
export * as Media from "./media.js"
|
||||
|
||||
import { Effect, Encoding, FileSystem, Schema, SchemaGetter } from "effect"
|
||||
import { HttpClientRequest } from "effect/unstable/http"
|
||||
import { Effect, FileSystem, Schema, SchemaGetter } from "effect"
|
||||
import { Base64 } from "effect/encoding"
|
||||
import { HttpClientRequest } from "effect/http"
|
||||
import { ProviderID } from "./schema/ids.js"
|
||||
import { AIError, HttpContext, InvalidProviderOutputError, InvalidRequestError } from "./schema/errors.js"
|
||||
import { ProviderMetadata } from "./schema/options.js"
|
||||
@@ -140,7 +141,7 @@ export class Asset {
|
||||
inline(): Inline | undefined {
|
||||
const source = this.source
|
||||
if (source.type !== "bytes" && source.type !== "base64") return undefined
|
||||
const base64 = source.type === "base64" ? source.data : (this.#base64 ??= Encoding.encodeBase64(source.data))
|
||||
const base64 = source.type === "base64" ? source.data : (this.#base64 ??= Base64.encode(source.data))
|
||||
const mime = this.mediaType.toLowerCase()
|
||||
return { mime, base64, dataUrl: `data:${mime};base64,${base64}` }
|
||||
}
|
||||
@@ -155,7 +156,7 @@ export class Asset {
|
||||
return Effect.fail(invalid(`Cannot materialize provider ref ${source.provider}:${source.id}`))
|
||||
const decoded =
|
||||
source.type === "base64"
|
||||
? Effect.fromResult(Encoding.decodeBase64(source.data)).pipe(
|
||||
? Effect.fromResult(Base64.decode(source.data)).pipe(
|
||||
Effect.mapError((cause) => invalid(`Media asset contains invalid base64 data`, cause)),
|
||||
)
|
||||
: download(source, this.headers)
|
||||
@@ -168,7 +169,7 @@ export class Asset {
|
||||
const source = this.source
|
||||
if (source.type === "base64") return Effect.succeed(source.data)
|
||||
if (this.#base64 !== undefined) return Effect.succeed(this.#base64)
|
||||
return this.bytes().pipe(Effect.map((data) => (this.#base64 = Encoding.encodeBase64(data))))
|
||||
return this.bytes().pipe(Effect.map((data) => (this.#base64 = Base64.encode(data))))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -183,7 +184,7 @@ export class Asset {
|
||||
toJSON() {
|
||||
const source = this.source
|
||||
return {
|
||||
source: source.type === "bytes" ? { ...source, data: Encoding.encodeBase64(source.data) } : source,
|
||||
source: source.type === "bytes" ? { ...source, data: Base64.encode(source.data) } : source,
|
||||
info: this.info,
|
||||
providerMetadata: this.providerMetadata,
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Auth } from "../route/auth.js"
|
||||
import { Endpoint } from "../route/endpoint.js"
|
||||
import { Framing } from "../route/framing.js"
|
||||
import { Protocol } from "../route/protocol.js"
|
||||
import { Headers } from "effect/unstable/http"
|
||||
import { Headers } from "effect/http"
|
||||
import { HttpTransport } from "../route/transport/index.js"
|
||||
import {
|
||||
AIError,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Effect, Schema } from "effect"
|
||||
import type { HttpClientResponse } from "effect/unstable/http"
|
||||
import type { HttpClientResponse } from "effect/http"
|
||||
import type { Status } from "../generation.js"
|
||||
import { Media } from "../media.js"
|
||||
import { MediaProtocol } from "../route/media-protocol.js"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Effect, Encoding, Schema } from "effect"
|
||||
import { Effect, Schema } from "effect"
|
||||
import { Base64 } from "effect/encoding"
|
||||
import { Route } from "../route/client.js"
|
||||
import { Endpoint } from "../route/endpoint.js"
|
||||
import { Protocol } from "../route/protocol.js"
|
||||
@@ -610,7 +611,7 @@ interface ParserState {
|
||||
readonly reasoningRedactedContent: Readonly<Record<number, Uint8Array[]>>
|
||||
}
|
||||
|
||||
const encodeRedactedContent = (chunks: ReadonlyArray<Uint8Array>) => Encoding.encodeBase64(concatBytes(chunks))
|
||||
const encodeRedactedContent = (chunks: ReadonlyArray<Uint8Array>) => Base64.encode(concatBytes(chunks))
|
||||
|
||||
const step = (state: ParserState, event: BedrockEvent) =>
|
||||
Effect.gen(function* () {
|
||||
@@ -659,7 +660,7 @@ const step = (state: ParserState, event: BedrockEvent) =>
|
||||
const events: LLMEvent[] = []
|
||||
const redactedChunk = yield* (() => {
|
||||
if (reasoning.redactedContent === undefined) return Effect.succeed(undefined)
|
||||
return Effect.fromResult(Encoding.decodeBase64(reasoning.redactedContent)).pipe(
|
||||
return Effect.fromResult(Base64.decode(reasoning.redactedContent)).pipe(
|
||||
Effect.mapError((cause) =>
|
||||
ProviderShared.eventError(
|
||||
ADAPTER,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { EventStreamCodec } from "@smithy/eventstream-codec"
|
||||
import { fromUtf8, toUtf8 } from "@smithy/util-utf8"
|
||||
import { Effect, Encoding, Stream } from "effect"
|
||||
import { Effect, Stream } from "effect"
|
||||
import { Base64 } from "effect/encoding"
|
||||
import { AIError, AIErrorReason, InvalidProviderOutputError } from "../schema/index.js"
|
||||
import { Framing } from "../route/framing.js"
|
||||
import { ProviderShared } from "./shared.js"
|
||||
@@ -47,7 +48,7 @@ const consumeFrames = (route: string) => (state: FrameBufferState, input: FrameI
|
||||
route,
|
||||
classification: "incomplete-stream",
|
||||
message: `Incomplete Bedrock Converse event-stream frame: ${remaining.length} buffered bytes remain at end of stream`,
|
||||
body: Encoding.encodeBase64(remaining),
|
||||
body: Base64.encode(remaining),
|
||||
}),
|
||||
})
|
||||
return [state, []] as const
|
||||
@@ -68,7 +69,7 @@ const consumeFrames = (route: string) => (state: FrameBufferState, input: FrameI
|
||||
`Failed to decode Bedrock Converse event-stream frame: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
Encoding.encodeBase64(view.subarray(0, totalLength)),
|
||||
Base64.encode(view.subarray(0, totalLength)),
|
||||
error,
|
||||
),
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Effect, Schema } from "effect"
|
||||
import type { HttpClientResponse } from "effect/unstable/http"
|
||||
import type { HttpClientResponse } from "effect/http"
|
||||
import type { Status } from "../generation.js"
|
||||
import { ImageModel, ImageResponse, type ImageRequestFor } from "../image.js"
|
||||
import { Media } from "../media.js"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Effect, Schema } from "effect"
|
||||
import type { HttpClientResponse } from "effect/unstable/http"
|
||||
import type { HttpClientResponse } from "effect/http"
|
||||
import { MediaProtocol } from "../route/media-protocol.js"
|
||||
import { MediaRoute } from "../route/media.js"
|
||||
import { mergeJsonRecords, type OpenString } from "../schema/index.js"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Effect, Schema } from "effect"
|
||||
import type { HttpClientResponse } from "effect/unstable/http"
|
||||
import type { HttpClientResponse } from "effect/http"
|
||||
import { MediaProtocol } from "../route/media-protocol.js"
|
||||
import { MediaRoute } from "../route/media.js"
|
||||
import { mergeJsonRecords, type OpenString } from "../schema/index.js"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Effect, Schema } from "effect"
|
||||
import type { HttpClientResponse } from "effect/unstable/http"
|
||||
import type { HttpClientResponse } from "effect/http"
|
||||
import { ImageModel, ImageResponse, type ImageRequestFor } from "../image.js"
|
||||
import { Media } from "../media.js"
|
||||
import { MediaProtocol } from "../route/media-protocol.js"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Effect, Schema } from "effect"
|
||||
import type { HttpClientResponse } from "effect/unstable/http"
|
||||
import type { HttpClientResponse } from "effect/http"
|
||||
import { Media } from "../media.js"
|
||||
import { MediaProtocol } from "../route/media-protocol.js"
|
||||
import { MediaRoute } from "../route/media.js"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Effect, Schema } from "effect"
|
||||
import type { HttpClientResponse } from "effect/unstable/http"
|
||||
import type { HttpClientResponse } from "effect/http"
|
||||
import { ImageModel, ImageResponse, type ImageRequestFor } from "../image.js"
|
||||
import { MediaProtocol } from "../route/media-protocol.js"
|
||||
import { MediaRoute } from "../route/media.js"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Duration, Effect, Schema } from "effect"
|
||||
import type { HttpClientResponse } from "effect/unstable/http"
|
||||
import type { HttpClientResponse } from "effect/http"
|
||||
import type { Status } from "../generation.js"
|
||||
import { Media } from "../media.js"
|
||||
import { MediaProtocol } from "../route/media-protocol.js"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Effect, Schema } from "effect"
|
||||
import type { HttpClientResponse } from "effect/unstable/http"
|
||||
import type { HttpClientResponse } from "effect/http"
|
||||
import { ImageModel, ImageResponse, type ImageRequestFor } from "../image.js"
|
||||
import { Media } from "../media.js"
|
||||
import { MediaProtocol } from "../route/media-protocol.js"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Effect, Encoding, Schema } from "effect"
|
||||
import { Effect, Schema } from "effect"
|
||||
import { Base64 } from "effect/encoding"
|
||||
import { Protocol } from "../route/protocol.js"
|
||||
import { HttpTransport } from "../route/transport/index.js"
|
||||
import { LLMEvent, LLMRequest, Message, ToolResultPart } from "../schema/index.js"
|
||||
@@ -135,7 +136,7 @@ const HOSTED_TOOLS = {
|
||||
"Meta returned an image without data",
|
||||
ProviderShared.encodeJson(raw),
|
||||
)
|
||||
const data = yield* Effect.fromResult(Encoding.decodeBase64(item.result)).pipe(
|
||||
const data = yield* Effect.fromResult(Base64.decode(item.result)).pipe(
|
||||
Effect.mapError((cause) =>
|
||||
ProviderShared.eventError(
|
||||
ADAPTER,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Effect, Schema, Stream } from "effect"
|
||||
import { Headers } from "effect/unstable/http"
|
||||
import { Headers } from "effect/http"
|
||||
import { Framing } from "../route/framing.js"
|
||||
import type { HttpContext } from "../schema/index.js"
|
||||
import {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Effect, Encoding, Schema } from "effect"
|
||||
import { Headers } from "effect/unstable/http"
|
||||
import { Effect, Schema } from "effect"
|
||||
import { Base64 } from "effect/encoding"
|
||||
import { Headers } from "effect/http"
|
||||
import { Route } from "../route/client.js"
|
||||
import { Auth } from "../route/auth.js"
|
||||
import { Endpoint } from "../route/endpoint.js"
|
||||
@@ -249,7 +250,7 @@ const checkpointBody = {
|
||||
const hostedToolResult = Effect.fn("OpenAIResponses.hostedToolResult")(function* (item: ResponsesHostedTools.Item) {
|
||||
const isError = item.error !== undefined && item.error !== null
|
||||
if (item.type === "image_generation_call" && item.result) {
|
||||
yield* Effect.fromResult(Encoding.decodeBase64(item.result)).pipe(
|
||||
yield* Effect.fromResult(Base64.decode(item.result)).pipe(
|
||||
Effect.mapError((cause) =>
|
||||
ProviderShared.eventError(ADAPTER, "OpenAI Responses returned invalid image base64", undefined, cause),
|
||||
),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Duration, Effect, Schema } from "effect"
|
||||
import type { HttpClientResponse } from "effect/unstable/http"
|
||||
import type { HttpClientResponse } from "effect/http"
|
||||
import type { Status } from "../generation.js"
|
||||
import { ImageModel, ImageResponse, type ImageRequestFor } from "../image.js"
|
||||
import { Media } from "../media.js"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Duration, Effect, Schema } from "effect"
|
||||
import type { HttpClientResponse } from "effect/unstable/http"
|
||||
import type { HttpClientResponse } from "effect/http"
|
||||
import type { Status } from "../generation.js"
|
||||
import { Media } from "../media.js"
|
||||
import { MediaProtocol } from "../route/media-protocol.js"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Tool } from "@opencode/schema/tool"
|
||||
import { Effect, Option, Schema } from "effect"
|
||||
import { Headers, HttpClientRequest } from "effect/unstable/http"
|
||||
import { Headers, HttpClientRequest } from "effect/http"
|
||||
import { Media } from "../media.js"
|
||||
import {
|
||||
InvalidProviderOutputError,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Effect, Schema } from "effect"
|
||||
import type { HttpClientResponse } from "effect/unstable/http"
|
||||
import type { HttpClientResponse } from "effect/http"
|
||||
import { ImageModel, ImageResponse, type ImageRequestFor } from "../image.js"
|
||||
import { MediaProtocol } from "../route/media-protocol.js"
|
||||
import { MediaRoute } from "../route/media.js"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { AwsV4Signer } from "aws4fetch"
|
||||
import { Effect } from "effect"
|
||||
import { Headers } from "effect/unstable/http"
|
||||
import { Headers } from "effect/http"
|
||||
import { Auth, type AuthInput } from "../../route/auth.js"
|
||||
import { AIError, AuthenticationError } from "../../schema/index.js"
|
||||
import { ProviderShared } from "../shared.js"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Effect, Encoding, Schema } from "effect"
|
||||
import { Effect, Schema } from "effect"
|
||||
import { Base64 } from "effect/encoding"
|
||||
import type { MediaPart } from "../../schema/index.js"
|
||||
import { ProviderShared } from "../shared.js"
|
||||
|
||||
@@ -78,12 +79,12 @@ function documentName(filename: string | undefined, names: Set<string>) {
|
||||
|
||||
const mediaBase64 = Effect.fn("BedrockMedia.mediaBase64")(function* (part: MediaPart) {
|
||||
const media = yield* ProviderShared.requireInlineMedia("Bedrock Converse", part.media)
|
||||
const bytes = yield* Effect.fromResult(Encoding.decodeBase64(media.base64)).pipe(
|
||||
const bytes = yield* Effect.fromResult(Base64.decode(media.base64)).pipe(
|
||||
Effect.mapError((cause) =>
|
||||
ProviderShared.invalidRequest("Bedrock Converse media data must be valid base64", cause),
|
||||
),
|
||||
)
|
||||
return Encoding.encodeBase64(bytes)
|
||||
return Base64.encode(bytes)
|
||||
})
|
||||
|
||||
// Route by MIME. Known image/document formats lower into a typed block; anything
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Effect, Schema } from "effect"
|
||||
import type { HttpClientResponse } from "effect/unstable/http"
|
||||
import type { HttpClientResponse } from "effect/http"
|
||||
import type { Status } from "../../generation.js"
|
||||
import type { Media } from "../../media.js"
|
||||
import { MediaProtocol } from "../../route/media-protocol.js"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Effect, Encoding } from "effect"
|
||||
import { Effect } from "effect"
|
||||
import { Base64 } from "effect/encoding"
|
||||
import { Media } from "../../media.js"
|
||||
import type { MediaProtocol } from "../../route/media-protocol.js"
|
||||
import { mergeJsonRecords, type AIError, type ProviderID } from "../../schema/index.js"
|
||||
@@ -10,7 +11,7 @@ export const inlineBytes = (route: string, asset: Media.Asset): Effect.Effect<Ui
|
||||
if (asset.source.type === "bytes") return Effect.succeed(asset.source.data)
|
||||
const inline = asset.inline()
|
||||
if (!inline) return Effect.fail(ProviderShared.inlineRequired(route, asset))
|
||||
return Effect.fromResult(Encoding.decodeBase64(inline.base64)).pipe(
|
||||
return Effect.fromResult(Base64.decode(inline.base64)).pipe(
|
||||
Effect.mapError((cause) => ProviderShared.invalidRequest(`${route} media contains invalid base64 data`, cause)),
|
||||
)
|
||||
}
|
||||
@@ -52,7 +53,7 @@ export const decodedAsset = (
|
||||
mediaType: string | undefined,
|
||||
options?: Media.AssetOptions,
|
||||
) =>
|
||||
Effect.fromResult(Encoding.decodeBase64(data)).pipe(
|
||||
Effect.fromResult(Base64.decode(data)).pipe(
|
||||
Effect.mapError((cause) => invalid(`${label} contains invalid base64 data`, cause)),
|
||||
Effect.map((bytes) => Media.bytes(bytes, mediaType, options)),
|
||||
)
|
||||
|
||||
@@ -63,6 +63,9 @@ const objectRoot = (schema: JsonSchema): JsonSchema => {
|
||||
schema.anyOf.some((branch) => isRecord(branch) && branch.type === "object" && Object.keys(branch).length === 1)
|
||||
)
|
||||
return { type: "object", ...Object.fromEntries(Object.entries(schema).filter(([key]) => key !== "anyOf")) }
|
||||
// Effect 4 represents an empty Struct as `not: { type: "null" }`; an object root already excludes null.
|
||||
if (isRecord(schema.not) && schema.not.type === "null" && Object.keys(schema.not).length === 1)
|
||||
return { type: "object", ...Object.fromEntries(Object.entries(schema).filter(([key]) => key !== "not")) }
|
||||
return { type: "object", ...schema }
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Effect, Schema } from "effect"
|
||||
import type { HttpClientResponse } from "effect/unstable/http"
|
||||
import type { HttpClientResponse } from "effect/http"
|
||||
import { ImageModel, ImageResponse, type ImageRequestFor } from "../image.js"
|
||||
import { Media } from "../media.js"
|
||||
import { MediaProtocol } from "../route/media-protocol.js"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Effect, Schema } from "effect"
|
||||
import type { HttpClientResponse } from "effect/unstable/http"
|
||||
import type { HttpClientResponse } from "effect/http"
|
||||
import type { Status } from "../generation.js"
|
||||
import { Media } from "../media.js"
|
||||
import { MediaProtocol } from "../route/media-protocol.js"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Duration, Effect, Schema } from "effect"
|
||||
import type { HttpClientResponse } from "effect/unstable/http"
|
||||
import type { HttpClientResponse } from "effect/http"
|
||||
import { ImageModel, ImageResponse, type ImageRequestFor } from "../image.js"
|
||||
import { MediaProtocol } from "../route/media-protocol.js"
|
||||
import { MediaRoute } from "../route/media.js"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Effect, Schema } from "effect"
|
||||
import { Headers, HttpClientRequest } from "effect/unstable/http"
|
||||
import { Headers, HttpClientRequest } from "effect/http"
|
||||
import {
|
||||
EvaluationAnswer,
|
||||
EvaluationInput,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Config, Effect, Option, Redacted } from "effect"
|
||||
import { Headers } from "effect/unstable/http"
|
||||
import { Headers } from "effect/http"
|
||||
import { AuthenticationError, AIError, type HttpOptions } from "../schema/index.js"
|
||||
|
||||
export class MissingCredentialError extends Error {
|
||||
@@ -95,7 +95,7 @@ export const optional = (secret: Secret | undefined, source = "optional value")
|
||||
export const config = (name: string) =>
|
||||
credential(
|
||||
Effect.gen(function* () {
|
||||
const secret = yield* Config.option(Config.redacted(name))
|
||||
const secret = yield* Config.option(Config.Redacted(name))
|
||||
if (Option.isSome(secret) && Redacted.value(secret.value) !== "") return secret.value
|
||||
return yield* Effect.fail(new MissingCredentialError(name, `${name} is not set`))
|
||||
}),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Context, type Effect } from "effect"
|
||||
import type { HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
|
||||
import type { HttpClientRequest, HttpClientResponse } from "effect/http"
|
||||
import type { AIError } from "../schema/errors.js"
|
||||
|
||||
// The service tag lives in its own leaf module so `Media.Asset` (imported by the schema layer) can require the
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
HttpClientError,
|
||||
HttpClientRequest,
|
||||
HttpClientResponse,
|
||||
} from "effect/unstable/http"
|
||||
} from "effect/http"
|
||||
import { HttpContext, HttpRateLimitDetails, AIError, TransportError } from "../schema/index.js"
|
||||
import { classifyProviderFailure, providerErrorMessage } from "../provider-error.js"
|
||||
import { Service, type HttpMiddleware, type Interface } from "./executor-service.js"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Effect, Stream } from "effect"
|
||||
import { makeParser, type Event } from "effect/unstable/encoding/Sse"
|
||||
import { makeParser, type Event } from "effect/encoding/Sse"
|
||||
import { AIError, InvalidProviderOutputError } from "../schema/index.js"
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Clock, Duration, Effect, Schema, type Stream } from "effect"
|
||||
import { HttpClientResponse } from "effect/unstable/http"
|
||||
import { HttpClientResponse } from "effect/http"
|
||||
import type { Snapshot, Status } from "../generation.js"
|
||||
import { Media } from "../media.js"
|
||||
import type { AuthInput } from "./auth.js"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Duration, Effect, Schedule, Schema, Stream } from "effect"
|
||||
import { Headers, HttpClientRequest, type HttpClientResponse } from "effect/unstable/http"
|
||||
import { Headers, HttpClientRequest, type HttpClientResponse } from "effect/http"
|
||||
import { Auth, type AuthInput } from "./auth.js"
|
||||
import { Endpoint } from "./endpoint.js"
|
||||
import { RequestExecutorService, type Interface } from "./executor-service.js"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Effect } from "effect"
|
||||
import { Headers, HttpClientRequest } from "effect/unstable/http"
|
||||
import { Headers, HttpClientRequest } from "effect/http"
|
||||
import { Auth } from "../auth.js"
|
||||
import { render as renderEndpoint } from "../endpoint.js"
|
||||
import { Framing } from "../framing.js"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Effect, Scope, Stream } from "effect"
|
||||
import type { Headers } from "effect/unstable/http"
|
||||
import type { Headers } from "effect/http"
|
||||
import type { AIError, HttpContext } from "../../schema/index.js"
|
||||
|
||||
export interface WebSocketChannelExecutor {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Cause, Effect, Queue, Stream } from "effect"
|
||||
import { Headers } from "effect/unstable/http"
|
||||
import { Socket } from "effect/unstable/socket"
|
||||
import { Headers } from "effect/http"
|
||||
import { Socket } from "effect/socket"
|
||||
import {
|
||||
AIError,
|
||||
AIErrorReason,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Schema } from "effect"
|
||||
import { extendSchema } from "@opencode/schema/schema"
|
||||
import { LLM } from "@opencode/schema/llm"
|
||||
import { ContentBlockID, ToolCallID } from "./ids.js"
|
||||
import {
|
||||
@@ -339,7 +340,7 @@ const toolCallID = (value: ToolCallID | string) => ToolCallID.make(value)
|
||||
* Lets consumers write `events.filter(LLMEvent.is.toolCall)` instead of
|
||||
* `events.filter(LLMEvent.guards["tool-call"])`.
|
||||
*/
|
||||
export const LLMEvent = Object.assign(llmEventTagged, {
|
||||
export const LLMEvent = extendSchema(llmEventTagged, {
|
||||
compaction: CompactionPart.make,
|
||||
stepStart: StepStart.make,
|
||||
textStart: (input: WithID<TextStart, ContentBlockID>) => TextStart.make({ ...input, id: contentBlockID(input.id) }),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Schema } from "effect"
|
||||
import { extendSchema } from "@opencode/schema/schema"
|
||||
import { Tool } from "@opencode/schema/tool"
|
||||
import {
|
||||
CacheHint,
|
||||
@@ -28,7 +29,7 @@ export type SystemPart = Schema.Schema.Type<typeof systemPartSchema>
|
||||
|
||||
const makeSystemPart = (text: string): SystemPart => ({ type: "text", text })
|
||||
|
||||
export const SystemPart = Object.assign(systemPartSchema, {
|
||||
export const SystemPart = extendSchema(systemPartSchema, {
|
||||
make: makeSystemPart,
|
||||
content: (input?: string | SystemPart | ReadonlyArray<SystemPart>) => {
|
||||
if (input === undefined) return []
|
||||
@@ -76,7 +77,7 @@ const toolResultValueSchema = Schema.Union([
|
||||
export type ToolResultValue = Schema.Schema.Type<typeof toolResultValueSchema>
|
||||
const isToolResultValue = Schema.is(toolResultValueSchema)
|
||||
|
||||
export const ToolResultValue = Object.assign(toolResultValueSchema, {
|
||||
export const ToolResultValue = extendSchema(toolResultValueSchema, {
|
||||
is: isToolResultValue,
|
||||
make: (value: unknown, type: ToolResultValue["type"] = "json"): ToolResultValue => {
|
||||
if (isToolResultValue(value)) return value
|
||||
@@ -90,7 +91,7 @@ export interface ToolOutput {
|
||||
readonly content: ReadonlyArray<Tool.Content>
|
||||
}
|
||||
|
||||
export const ToolOutput = Object.assign(
|
||||
export const ToolOutput = extendSchema(
|
||||
Schema.Struct({
|
||||
structured: Schema.Unknown,
|
||||
content: Schema.Array(Tool.Content),
|
||||
@@ -127,7 +128,7 @@ const toolResultText = (value: unknown) => {
|
||||
}
|
||||
}
|
||||
|
||||
export const ToolCallPart = Object.assign(
|
||||
export const ToolCallPart = extendSchema(
|
||||
Schema.Struct({
|
||||
type: Schema.Literal("tool-call"),
|
||||
id: Schema.String,
|
||||
@@ -145,7 +146,7 @@ export const ToolCallPart = Object.assign(
|
||||
)
|
||||
export type ToolCallPart = Schema.Schema.Type<typeof ToolCallPart>
|
||||
|
||||
export const ToolResultPart = Object.assign(
|
||||
export const ToolResultPart = extendSchema(
|
||||
Schema.Struct({
|
||||
type: Schema.Literal("tool-result"),
|
||||
id: Schema.String,
|
||||
@@ -209,7 +210,7 @@ const compactionPartSchema = Schema.Struct({
|
||||
)
|
||||
.annotate({ identifier: "LLM.Content.Compaction" })
|
||||
export type CompactionPart = typeof compactionPartSchema.Type
|
||||
export const CompactionPart = Object.assign(compactionPartSchema, {
|
||||
export const CompactionPart = extendSchema(compactionPartSchema, {
|
||||
make: (input: Omit<CompactionPart, "type" | "encrypted" | "text"> & CompactionContent): CompactionPart =>
|
||||
Schema.decodeUnknownSync(compactionPartSchema)({ type: "compaction", ...input }),
|
||||
})
|
||||
@@ -326,7 +327,7 @@ export type ToolNamespaceEntryInput = ToolNamespaceInput & { readonly type: "nam
|
||||
|
||||
export const ToolNamespace: Schema.Codec<ToolNamespace> & {
|
||||
readonly make: (input: ToolNamespace | ToolNamespaceInput) => ToolNamespace
|
||||
} = Object.assign(
|
||||
} = extendSchema(
|
||||
Schema.Struct({
|
||||
type: Schema.Literal("namespace"),
|
||||
name: Schema.String,
|
||||
@@ -346,7 +347,7 @@ export type ToolEntry = ToolDefinition | ToolNamespace
|
||||
export type ToolEntryInput = ToolDefinition.Input | ToolNamespaceEntryInput
|
||||
export const ToolEntry: Schema.Codec<ToolEntry> & {
|
||||
readonly make: (input: ToolEntryInput) => ToolEntry
|
||||
} = Object.assign(
|
||||
} = extendSchema(
|
||||
Schema.Union([ToolDefinition, ToolNamespace]).pipe(
|
||||
Schema.toTaggedUnion("type"),
|
||||
Schema.annotate({ identifier: "LLM.ToolEntry" }),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Effect, Schema, Stream } from "effect"
|
||||
import { extendSchema } from "@opencode/schema/schema"
|
||||
import { ProgressEvent, QueuedEvent } from "./generation.js"
|
||||
import { Media } from "./media.js"
|
||||
import { MediaModel, composeRoute, tryRequest } from "./media-model.js"
|
||||
@@ -141,7 +142,7 @@ const speechEventTagged = Schema.Union([
|
||||
SpeechTimestampsEvent,
|
||||
SpeechFinishEvent,
|
||||
]).pipe(Schema.toTaggedUnion("type"))
|
||||
export const SpeechEvent = Object.assign(speechEventTagged, {
|
||||
export const SpeechEvent = extendSchema(speechEventTagged, {
|
||||
is: {
|
||||
generationQueued: speechEventTagged.guards["generation-queued"],
|
||||
generationProgress: speechEventTagged.guards["generation-progress"],
|
||||
|
||||
@@ -231,7 +231,8 @@ export const toDefinitions = (tools: Tools): ReadonlyArray<ToolDefinitionClass>
|
||||
)
|
||||
|
||||
const toJsonSchema = (schema: Schema.Top): JsonSchema.JsonSchema => {
|
||||
const document = Schema.toJsonSchemaDocument(schema)
|
||||
// Keep provider-facing tool schemas closed; Effect 4's default now emits additionalProperties: true.
|
||||
const document = Schema.toJsonSchemaDocument(schema, { onExcessProperty: "error" })
|
||||
if (Object.keys(document.definitions).length === 0) return document.schema
|
||||
return { ...document.schema, $defs: document.definitions }
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Effect, Schema, Stream } from "effect"
|
||||
import { extendSchema } from "@opencode/schema/schema"
|
||||
import { Generation, ProgressEvent, QueuedEvent, type AwaitOptions } from "./generation.js"
|
||||
import { Media } from "./media.js"
|
||||
import { MediaModel, composeRoute, tryRequest } from "./media-model.js"
|
||||
@@ -159,7 +160,7 @@ const transcriptionEventTagged = Schema.Union([
|
||||
TranscriptionSegmentEvent,
|
||||
TranscriptionFinishEvent,
|
||||
]).pipe(Schema.toTaggedUnion("type"))
|
||||
export const TranscriptionEvent = Object.assign(transcriptionEventTagged, {
|
||||
export const TranscriptionEvent = extendSchema(transcriptionEventTagged, {
|
||||
is: {
|
||||
generationQueued: transcriptionEventTagged.guards["generation-queued"],
|
||||
generationProgress: transcriptionEventTagged.guards["generation-progress"],
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Effect, Schema, Stream } from "effect"
|
||||
import { extendSchema } from "@opencode/schema/schema"
|
||||
import { Generation, ProgressEvent, QueuedEvent, type AwaitOptions } from "./generation.js"
|
||||
import { Media } from "./media.js"
|
||||
import { MediaModel, composeRoute, tryRequest } from "./media-model.js"
|
||||
@@ -136,7 +137,7 @@ export const VideoFinishEvent = Schema.Struct({
|
||||
const videoEventTagged = Schema.Union([QueuedEvent, ProgressEvent, VideoOutputEvent, VideoFinishEvent]).pipe(
|
||||
Schema.toTaggedUnion("type"),
|
||||
)
|
||||
export const VideoEvent = Object.assign(videoEventTagged, {
|
||||
export const VideoEvent = extendSchema(videoEventTagged, {
|
||||
is: {
|
||||
generationQueued: videoEventTagged.guards["generation-queued"],
|
||||
generationProgress: videoEventTagged.guards["generation-progress"],
|
||||
|
||||
@@ -30,7 +30,7 @@ type LanguageModel = {
|
||||
declare const auth: Auth.Definition
|
||||
declare const optionalAuthModel: LanguageModelFactory<BaseOptions, "optional", LanguageModel>
|
||||
declare const requiredAuthModel: LanguageModelFactory<BaseOptions, "required", LanguageModel>
|
||||
const configApiKey = Config.redacted("OPENAI_API_KEY")
|
||||
const configApiKey = Config.Redacted("OPENAI_API_KEY")
|
||||
|
||||
OpenAIChat.route.model({ id: "gpt-4.1-mini" })
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { ConfigProvider, Effect } from "effect"
|
||||
import { Headers } from "effect/unstable/http"
|
||||
import { Headers } from "effect/http"
|
||||
import { LLM } from "../src/index.js"
|
||||
import { Auth } from "../src/route/auth.js"
|
||||
import * as OpenAIChat from "../src/protocols/openai-chat.js"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { FetchHttpClient } from "effect/unstable/http"
|
||||
import { FetchHttpClient } from "effect/http"
|
||||
import { LLM, LLMRequest, Message } from "../src/index.js"
|
||||
import { LLMClient } from "../src/route/client.js"
|
||||
import { OpenAI } from "../src/providers.js"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { Effect, Ref, Schema } from "effect"
|
||||
import { HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
|
||||
import { HttpClientRequest, HttpClientResponse } from "effect/http"
|
||||
import {
|
||||
LLM,
|
||||
LLMRequest,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { HttpClientRequest } from "effect/unstable/http"
|
||||
import { HttpClientRequest } from "effect/http"
|
||||
import { Evaluation, EvaluationClient } from "../src/experimental.js"
|
||||
import { OpenCodeZen, OpenRouter, TypeSafeAI, VercelAIGateway } from "../src/providers.js"
|
||||
import { it } from "./lib/effect.js"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Deferred, Effect, Fiber, Layer, Ref, Stream } from "effect"
|
||||
import { Headers, HttpClientError, HttpClientRequest } from "effect/unstable/http"
|
||||
import { Headers, HttpClientError, HttpClientRequest } from "effect/http"
|
||||
import { LLM, AIError, HttpContext, InvalidProviderOutputError, TransportError } from "../src/index.js"
|
||||
import { LLMClient, RequestExecutor, WebSocketTransport, type WebSocketChannelExecutor } from "../src/route.js"
|
||||
import { route } from "../src/protocols/openai-chat.js"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect, Layer, Stream } from "effect"
|
||||
import { HttpClientRequest } from "effect/unstable/http"
|
||||
import { HttpClientRequest } from "effect/http"
|
||||
import { Image, ImageClient, Media } from "../src/index.js"
|
||||
import { BlackForestLabs, Fal, Google, OpenAI, Replicate, Stability, XAI, ZAI } from "../src/providers.js"
|
||||
import { it } from "./lib/effect.js"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Effect, Fiber, Layer, Ref } from "effect"
|
||||
import * as TestClock from "effect/testing/TestClock"
|
||||
import { HttpClient, HttpClientError, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
|
||||
import { HttpClient, HttpClientError, HttpClientRequest, HttpClientResponse } from "effect/http"
|
||||
import { LLMClient, RequestExecutor } from "../../src/route.js"
|
||||
import type { Service as LLMClientService } from "../../src/route/client.js"
|
||||
import type { Service as RequestExecutorService } from "../../src/route/executor.js"
|
||||
|
||||
@@ -2,7 +2,7 @@ import { describe, expect } from "bun:test"
|
||||
import { NodeFileSystem } from "@effect/platform-node"
|
||||
import { Effect, Ref, Schema } from "effect"
|
||||
import { FileSystem } from "effect"
|
||||
import { HttpClientRequest } from "effect/unstable/http"
|
||||
import { HttpClientRequest } from "effect/http"
|
||||
import { AIError, Media, Message } from "../src/index.js"
|
||||
import { it } from "./lib/effect.js"
|
||||
import { dynamicResponse, scriptedResponses } from "./lib/http.js"
|
||||
|
||||
@@ -3,7 +3,7 @@ import { mkdtemp, rm } from "node:fs/promises"
|
||||
import { tmpdir } from "node:os"
|
||||
import { join } from "node:path"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { HttpClientRequest } from "effect/unstable/http"
|
||||
import { HttpClientRequest } from "effect/http"
|
||||
import { AIError, LLMEvent, Media, SpeechEvent, TranscriptionEvent } from "../src/index.js"
|
||||
import { RequestExecutor } from "../src/route.js"
|
||||
import { AI } from "../src/promise.js"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect, test } from "bun:test"
|
||||
import { ConfigProvider, Effect } from "effect"
|
||||
import { Headers } from "effect/unstable/http"
|
||||
import { Headers } from "effect/http"
|
||||
import { Auth, LLM, LLMClient, LLMRequest, Message, ReasoningPart, ToolDefinition } from "../../src/index.js"
|
||||
import { Alibaba } from "../../src/providers.js"
|
||||
import { compileRequest } from "../../src/route/client.js"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { HttpClientRequest } from "effect/unstable/http"
|
||||
import { HttpClientRequest } from "effect/http"
|
||||
import {
|
||||
CacheHint,
|
||||
LLM,
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { EventStreamCodec } from "@smithy/eventstream-codec"
|
||||
import { fromUtf8, toUtf8 } from "@smithy/util-utf8"
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect, Encoding, Ref, Schema, Stream } from "effect"
|
||||
import { HttpClientRequest } from "effect/unstable/http"
|
||||
import { Effect, Ref, Schema, Stream } from "effect"
|
||||
import { Base64 } from "effect/encoding"
|
||||
import { HttpClientRequest } from "effect/http"
|
||||
import {
|
||||
Media,
|
||||
CacheHint,
|
||||
@@ -866,7 +867,7 @@ describe("Bedrock Converse route", () => {
|
||||
reason: { _tag: "InvalidProviderOutput", classification: "incomplete-stream" },
|
||||
message: `Incomplete Bedrock Converse event-stream frame: ${partial.length} buffered bytes remain at end of stream`,
|
||||
})
|
||||
expect(error.reason.body).toBe(Encoding.encodeBase64(partial))
|
||||
expect(error.reason.body).toBe(Base64.encode(partial))
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { HttpClientRequest } from "effect/unstable/http"
|
||||
import { HttpClientRequest } from "effect/http"
|
||||
import { LLM, Message } from "../../src/index.js"
|
||||
import { AmazonBedrockMantle } from "../../src/providers.js"
|
||||
import { model } from "../../src/providers/amazon-bedrock/mantle.js"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { ConfigProvider, Effect, Schema } from "effect"
|
||||
import { HttpClientRequest } from "effect/unstable/http"
|
||||
import { HttpClientRequest } from "effect/http"
|
||||
import { LLM, LLMEvent } from "../../src/index.js"
|
||||
import { CloudflareAIGateway } from "../../src/providers/cloudflare-ai-gateway.js"
|
||||
import { CloudflareWorkersAI } from "../../src/providers/cloudflare-workers-ai.js"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { HttpClientRequest } from "effect/unstable/http"
|
||||
import { HttpClientRequest } from "effect/http"
|
||||
import { LanguageModel, LLM, Message, ToolCallPart } from "../../src/index.js"
|
||||
import { GoogleVertex, GoogleVertexChat, GoogleVertexMessages, GoogleVertexResponses } from "../../src/providers.js"
|
||||
import { LLMClient } from "../../src/route.js"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect } from "bun:test"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { HttpClientRequest } from "effect/unstable/http"
|
||||
import { HttpClientRequest } from "effect/http"
|
||||
import { Image, ImageClient, LLM, LLMEvent, LLMRequest, Media, Message, ToolDefinition } from "../../src/index.js"
|
||||
import { Meta } from "../../src/providers/meta.js"
|
||||
import { MetaMessages } from "../../src/protocols/meta-messages.js"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect } from "bun:test"
|
||||
import { ConfigProvider, Effect } from "effect"
|
||||
import { Headers } from "effect/unstable/http"
|
||||
import { Headers } from "effect/http"
|
||||
import { Auth, LLM, LLMClient } from "../../src/index.js"
|
||||
import { Meta } from "../../src/providers/index.js"
|
||||
import { OpenAIChat } from "../../src/protocols/openai-chat.js"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { ConfigProvider, Effect } from "effect"
|
||||
import { Headers } from "effect/unstable/http"
|
||||
import { Headers } from "effect/http"
|
||||
import { LLM } from "../../src/index.js"
|
||||
import { MiniMax } from "../../src/providers.js"
|
||||
import { AnthropicMessages } from "../../src/protocols/anthropic-messages.js"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { ConfigProvider, Effect } from "effect"
|
||||
import { HttpClientRequest } from "effect/unstable/http"
|
||||
import { HttpClientRequest } from "effect/http"
|
||||
import { LLM, LLMEvent, Message, ToolDefinition, Media } from "../../src/index.js"
|
||||
import { Mistral } from "../../src/providers/index.js"
|
||||
import { MistralChat } from "../../src/protocols/index.js"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect } from "bun:test"
|
||||
import { ConfigProvider, Effect } from "effect"
|
||||
import { Headers } from "effect/unstable/http"
|
||||
import { Headers } from "effect/http"
|
||||
import { Auth, LLM, LLMClient, Message, ReasoningPart, ToolDefinition } from "../../src/index.js"
|
||||
import { Moonshot } from "../../src/providers.js"
|
||||
import { AnthropicMessages } from "../../src/protocols/anthropic-messages.js"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { ConfigProvider, Effect } from "effect"
|
||||
import { HttpClientRequest } from "effect/unstable/http"
|
||||
import { HttpClientRequest } from "effect/http"
|
||||
import { LLM, Message, ToolDefinition } from "../../src/index.js"
|
||||
import {
|
||||
AmazonBedrock,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect, Ref, Schema, Stream } from "effect"
|
||||
import { HttpClientRequest } from "effect/unstable/http"
|
||||
import { HttpClientRequest } from "effect/http"
|
||||
import {
|
||||
Media,
|
||||
HttpOptions,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { Effect, Schema } from "effect"
|
||||
import { HttpClientRequest } from "effect/unstable/http"
|
||||
import { HttpClientRequest } from "effect/http"
|
||||
import { LLM, LLMRequest, Message, ToolCallPart, ToolChoice, ToolDefinition } from "../../src/index.js"
|
||||
import { Auth, LLMClient } from "../../src/route.js"
|
||||
import { compileRequest } from "../../src/route/client.js"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect, Stream } from "effect"
|
||||
import { Socket } from "effect/unstable/socket"
|
||||
import { Socket } from "effect/socket"
|
||||
import { LLM, LLMRequest, Message, ToolRuntime } from "../../src/index.js"
|
||||
import {
|
||||
LLMClient,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { ConfigProvider, Effect, Layer, Logger, Ref, Schema, Stream } from "effect"
|
||||
import { Headers, HttpClientRequest } from "effect/unstable/http"
|
||||
import { Headers, HttpClientRequest } from "effect/http"
|
||||
import {
|
||||
Media,
|
||||
LLM,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { Headers, HttpClientRequest } from "effect/unstable/http"
|
||||
import { Headers, HttpClientRequest } from "effect/http"
|
||||
import { Image, ImageClient } from "../../src/index.js"
|
||||
import { XAI } from "../../src/providers.js"
|
||||
import { Auth } from "../../src/route.js"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { HttpClientRequest } from "effect/unstable/http"
|
||||
import { HttpClientRequest } from "effect/http"
|
||||
import { Image, ImageClient } from "../../src/index.js"
|
||||
import { ZAI } from "../../src/providers.js"
|
||||
import { it } from "../lib/effect.js"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect } from "bun:test"
|
||||
import { ConfigProvider, Effect } from "effect"
|
||||
import { Headers } from "effect/unstable/http"
|
||||
import { Headers } from "effect/http"
|
||||
import { Auth, LLM, LLMClient, Message, ReasoningPart, ToolDefinition } from "../../src/index.js"
|
||||
import { ZAI, ZAICodingPlan } from "../../src/providers.js"
|
||||
import { compileRequest } from "../../src/route/client.js"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { HttpRecorder } from "@opencode/http-recorder"
|
||||
import { NodeSocket } from "@effect/platform-node"
|
||||
import { Layer } from "effect"
|
||||
import { Socket } from "effect/unstable/socket"
|
||||
import { Socket } from "effect/socket"
|
||||
import * as path from "node:path"
|
||||
import { fileURLToPath } from "node:url"
|
||||
import { LLMClient, RequestExecutor } from "../src/route.js"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect, Encoding, Layer, Stream } from "effect"
|
||||
import { Effect, Layer, Stream } from "effect"
|
||||
import { Base64 } from "effect/encoding"
|
||||
import { Speech, SpeechClient, SpeechEvent } from "../src/index.js"
|
||||
import { Cartesia, Deepgram, ElevenLabs, Google, OpenAI } from "../src/providers.js"
|
||||
import { it } from "./lib/effect.js"
|
||||
@@ -47,7 +48,7 @@ describe("Speech", () => {
|
||||
JSON.stringify({
|
||||
candidates: [
|
||||
{
|
||||
content: { parts: [{ inlineData: { mimeType: "audio/wav", data: Encoding.encodeBase64(bytes) } }] },
|
||||
content: { parts: [{ inlineData: { mimeType: "audio/wav", data: Base64.encode(bytes) } }] },
|
||||
finishReason: "STOP",
|
||||
},
|
||||
],
|
||||
@@ -244,7 +245,7 @@ describe("Speech", () => {
|
||||
Effect.gen(function* () {
|
||||
const record = (bytes: ReadonlyArray<number>, character: string, start: number) =>
|
||||
JSON.stringify({
|
||||
audio_base64: Encoding.encodeBase64(Uint8Array.from(bytes)),
|
||||
audio_base64: Base64.encode(Uint8Array.from(bytes)),
|
||||
alignment: {
|
||||
characters: [character],
|
||||
character_start_times_seconds: [start],
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect, Fiber, Layer, Stream } from "effect"
|
||||
import * as TestClock from "effect/testing/TestClock"
|
||||
import { HttpClientRequest } from "effect/unstable/http"
|
||||
import { HttpClientRequest } from "effect/http"
|
||||
import { Media, Transcription, TranscriptionClient, type TranscriptionEvent } from "../src/index.js"
|
||||
import { AssemblyAI, Deepgram, ElevenLabs, Google, OpenAI } from "../src/providers.js"
|
||||
import { it } from "./lib/effect.js"
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { fileURLToPath } from "node:url"
|
||||
import { expect, story } from "../../storybook/playwright/story"
|
||||
|
||||
const source = (path: string) => `/@fs/${fileURLToPath(new URL(path, import.meta.url)).replaceAll("\\", "/")}`
|
||||
const modules = {
|
||||
fixture: source("../../gui-extensions/src/browser/panel.fixture.tsx"),
|
||||
host: source("../src/runtime/extension/host.tsx"),
|
||||
panels: source("../src/runtime/extension/panels.tsx"),
|
||||
language: source("../src/runtime/i18n/language.tsx"),
|
||||
browser: source("../../gui-extensions/src/browser/index.ts"),
|
||||
browserRenderer: source("../../gui-extensions/src/browser/renderer.tsx"),
|
||||
file: source("../../gui-extensions/src/file/index.ts"),
|
||||
fileRenderer: source("../../gui-extensions/src/file/renderer.tsx"),
|
||||
}
|
||||
|
||||
story(
|
||||
"keeps a restored browser tab selected and undrawn until the desktop's first inventory",
|
||||
async ({ mount, page }) => {
|
||||
// Any story loads the app styles; the fixture mounts the real side region and extensions beside it.
|
||||
await mount("ui-line-comment--editor")
|
||||
await page.evaluate(async (modules) => {
|
||||
const [{ mountBrowserRegion }, host, panels, language, browser, file] = await Promise.all([
|
||||
import(modules.fixture),
|
||||
import(modules.host),
|
||||
import(modules.panels),
|
||||
import(modules.language),
|
||||
import(modules.browser),
|
||||
import(modules.file),
|
||||
])
|
||||
mountBrowserRegion({
|
||||
LanguageProvider: language.LanguageProvider,
|
||||
ExtensionHostProvider: host.ExtensionHostProvider,
|
||||
useExtensionHost: host.useExtensionHost,
|
||||
createRegion: panels.createRegion,
|
||||
definitions: [
|
||||
{ ...browser.default, renderer: () => import(modules.browserRenderer) },
|
||||
{ ...file.default, renderer: () => import(modules.fileRenderer) },
|
||||
],
|
||||
})
|
||||
}, modules)
|
||||
const root = page.getByTestId("browser-region-fixture")
|
||||
const tabs = root.getByRole("tab")
|
||||
const tree = root.getByTestId("tree")
|
||||
await expect(root.getByText("Registrations: 1", { exact: true })).toBeVisible()
|
||||
await expect(tabs).toHaveText(["alpha.ts"])
|
||||
await expect(tree).toHaveText('{"tab":"changes"}')
|
||||
|
||||
// Beta was left on its browser tab, which the desktop has not reported yet.
|
||||
await root.getByRole("button", { name: "Beta", exact: true }).click()
|
||||
await expect(root.getByText("Registrations: 2", { exact: true })).toBeVisible()
|
||||
await expect(root.getByTestId("selected")).toHaveText(/^browser:tab_/)
|
||||
await expect(tabs).toHaveText(["beta.ts"])
|
||||
// No fallback tab was selected, so the file tab's selection never switched the tree to All files.
|
||||
await expect(tree).toHaveText('{"tab":"changes"}')
|
||||
|
||||
await root.getByRole("button", { name: "First inventory", exact: true }).click()
|
||||
await expect(tabs).toHaveText(["beta.ts", "Preview"])
|
||||
await expect(root.getByRole("tab", { name: "Preview", exact: true })).toHaveAttribute("aria-selected", "true")
|
||||
await expect(tree).toHaveText('{"tab":"changes"}')
|
||||
},
|
||||
)
|
||||
@@ -183,6 +183,35 @@ test("keeps a narrow session composer contained when invoking a built-in", async
|
||||
await expect(editor).toHaveText("keep me")
|
||||
})
|
||||
|
||||
test("lists slash commands in their built-in order", async ({ page }) => {
|
||||
const { editor } = await openSession(page, {
|
||||
name: "ComposerSlash",
|
||||
provider: NO_PROVIDER,
|
||||
commands: [
|
||||
{ name: "init", description: "Create AGENTS.md" },
|
||||
{ name: "review", description: "Review changes" },
|
||||
],
|
||||
// A sent message enables /undo, /compact and /fork.
|
||||
pageMessages: () => ({ items: [{ id: "msg_slash", type: "user", text: "Hello", time: { created: T0 } }] }),
|
||||
})
|
||||
await editor.fill("/")
|
||||
await expect(page.locator('[data-component="composer-suggestions"] [data-suggestion-id] bdi')).toHaveText([
|
||||
"/init",
|
||||
"/review",
|
||||
"/new",
|
||||
"/undo",
|
||||
"/compact",
|
||||
"/fork",
|
||||
"/export",
|
||||
"/open",
|
||||
"/terminal",
|
||||
"/mcp",
|
||||
"/model",
|
||||
"/connect",
|
||||
"/btw",
|
||||
])
|
||||
})
|
||||
|
||||
const followUp = "Add follow-up, / for commands, @ for context…"
|
||||
for (const row of [
|
||||
{ state: "an idle", copy: "Ask anything, / for commands, @ for context…" },
|
||||
|
||||
@@ -37,6 +37,17 @@ test("mobile files browse, comment, search, and close tabs", async ({ page }) =>
|
||||
await expect.poll(() => fullWidth(comment)).toBe(true)
|
||||
|
||||
await page.setViewportSize({ width: 390, height: 844 })
|
||||
// Its composer chip reveals the comment without leaving the conversation.
|
||||
const session = navigation.getByRole("tab", { name: "Session", exact: true })
|
||||
await session.click()
|
||||
await page
|
||||
.locator('[data-component="composer-attachments"]')
|
||||
.getByText("Full-width file comment", { exact: true })
|
||||
.click()
|
||||
await expect(session).toHaveAttribute("aria-selected", "true")
|
||||
await expect(page.getByRole("textbox", { name: "Prompt", exact: true })).toBeVisible()
|
||||
await navigation.getByRole("tab", { name: "Files", exact: true }).click()
|
||||
await expect(files.getByText("contents:first.ts", { exact: true })).toBeVisible()
|
||||
const filter = files.getByRole("combobox", { name: "Filter files", exact: true })
|
||||
await expect(filter).toBeHidden()
|
||||
await files.getByRole("button", { name: "All files", exact: true }).click()
|
||||
|
||||
@@ -221,6 +221,54 @@ test("the summary's changes row switches the view and keeps the side tabs", asyn
|
||||
|
||||
await page.setViewportSize({ width: 1280, height: 900 })
|
||||
await expect(readme).toHaveAttribute("aria-selected", "true")
|
||||
|
||||
// After a reload forgets the preview, a stored Open file tab is still the slot a narrow-screen file open replaces.
|
||||
const launcher = panel.getByRole("tab", { name: "Open file", exact: true })
|
||||
await panel.getByRole("button", { name: "Open file", exact: true }).click()
|
||||
await expect(launcher).toHaveAttribute("aria-selected", "true")
|
||||
await expect(readme).toHaveCount(0)
|
||||
await page.reload()
|
||||
await expect(launcher).toHaveAttribute("aria-selected", "true")
|
||||
await page.setViewportSize({ width: 390, height: 844 })
|
||||
await tabs.getByRole("tab", { name: "Files", exact: true }).click()
|
||||
const files = page.locator('[data-slot="session-mobile-files"]')
|
||||
await files.getByRole("button", { name: "README.md", exact: true }).click()
|
||||
await expect(files.getByText("contents:README.md", { exact: true })).toBeVisible()
|
||||
await page.setViewportSize({ width: 1280, height: 900 })
|
||||
await expect(readme).toHaveAttribute("aria-selected", "true")
|
||||
await expect(launcher).toHaveCount(0)
|
||||
})
|
||||
|
||||
test("a narrow-screen palette pick keeps the view and dock, and the side region shows the file when wide", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.setViewportSize({ width: 390, height: 844 })
|
||||
await mockStressTimeline(page, {
|
||||
fileList: (path) => (path ? [] : [fileNode(fixture.directory, "README.md")]),
|
||||
fileContent: (path) => ({ type: "text", content: `contents:${path}` }),
|
||||
findFiles: ({ query }) => ("README.md".includes(query) ? [fileNode(fixture.directory, "README.md")] : []),
|
||||
pty: {},
|
||||
})
|
||||
await openStress(page, { lastProject: { local: fixture.directory } })
|
||||
await page.goto(sessionHref(fixture.targetID))
|
||||
|
||||
const tabs = page.getByRole("tablist", { name: "Session view", exact: true })
|
||||
const terminal = page.locator("#terminal-panel")
|
||||
await tabs.getByRole("tab", { name: "Terminal", exact: true }).click()
|
||||
await expect(terminal.locator("textarea")).toBeEditable()
|
||||
await page.keyboard.press("ControlOrMeta+p")
|
||||
const dialog = page.getByRole("dialog")
|
||||
await dialog.getByRole("textbox").fill("README")
|
||||
await dialog.getByRole("option", { name: "/ README.md", exact: true }).click()
|
||||
await expect(dialog).toHaveCount(0)
|
||||
await expect(tabs.getByRole("tab", { selected: true })).toHaveText("Terminal")
|
||||
await expect(terminal).toHaveAttribute("data-opened", "true")
|
||||
|
||||
await page.setViewportSize({ width: 1280, height: 900 })
|
||||
await expect(terminal).toHaveAttribute("data-opened", "true")
|
||||
const panel = page.locator("#review-panel")
|
||||
await expect(panel.getByRole("tab", { name: "README.md", exact: true })).toHaveAttribute("aria-selected", "true")
|
||||
await expect(panel.getByText("contents:README.md", { exact: true })).toBeVisible()
|
||||
})
|
||||
|
||||
test.describe("touch", () => {
|
||||
|
||||
@@ -90,6 +90,7 @@ test("settings opened from a remote session sweep every server and keep the remo
|
||||
await expect(page).toHaveURL("/settings")
|
||||
await expect(page.locator('[data-titlebar-tab][data-active="true"]')).toHaveCount(0)
|
||||
await expect(page.getByRole("dialog")).toHaveCount(0)
|
||||
await expect(settings.getByRole("tab", { name: "Models", exact: true })).toHaveCount(0)
|
||||
await view.listed(REMOTE_SERVER, directoryB)
|
||||
await expect.poll(() => view.replies).toEqual([reply(sessionA.id, "permission-pending-a")])
|
||||
|
||||
@@ -112,6 +113,7 @@ test("settings opened from a remote session sweep every server and keep the remo
|
||||
await expect(page.locator('[data-titlebar-tab][data-active="true"]')).toContainText(sessionB.title)
|
||||
await page.keyboard.press("Control+]")
|
||||
await expect(page).toHaveURL("/settings")
|
||||
await expect(settings.getByRole("tab", { name: "Preferences", exact: true })).toHaveAttribute("aria-selected", "true")
|
||||
await page.keyboard.press("Escape")
|
||||
await expectPath(page, sessionHref(sessionB.id, REMOTE_SERVER))
|
||||
await expectSessionTitle(page, sessionB.title)
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { OpenCodeEvent, SessionMessageInfo } from "@opencode/client/promise"
|
||||
import { expect, test, type Locator, type Page } from "@playwright/test"
|
||||
import { base64Encode } from "@opencode/util/encode"
|
||||
import { workspaceKey } from "../utils/app"
|
||||
import { SERVER, project, provider, session, workspaceKey } from "../utils/app"
|
||||
import { mockOpenCodeServer } from "../utils/mock-server"
|
||||
import { fileDiff, fileNode, openSession } from "../utils/workspace"
|
||||
import { expectSessionTitle } from "../utils/waits"
|
||||
|
||||
@@ -603,6 +604,65 @@ test("keeps the review state a session stored before extensions", async ({ page
|
||||
await expect(trigger("gamma.ts")).toBeVisible()
|
||||
})
|
||||
|
||||
test("desktop review waits for the session's stored mode before it loads changes", async ({ page }) => {
|
||||
const directory = "C:/OpenCode/ReviewDesktop"
|
||||
const id = "ses_review_desktop"
|
||||
const title = "Desktop review"
|
||||
await mockOpenCodeServer(page, {
|
||||
directory,
|
||||
project: project({ id: "proj_review_desktop", directory }),
|
||||
provider: provider(),
|
||||
sessions: [session({ id, directory, title })],
|
||||
pageMessages: () => ({ items: [] }),
|
||||
vcs: { current: "feature", default: "dev" },
|
||||
vcsDiff: ({ mode }) => (mode === "branch" ? [fileDiff("beta.ts")] : [fileDiff("gamma.ts")]),
|
||||
})
|
||||
const working: string[] = []
|
||||
page.on("request", (request) => {
|
||||
const url = new URL(request.url())
|
||||
if (url.pathname.endsWith("/vcs/diff") && url.searchParams.get("mode") === "working") working.push(request.url())
|
||||
})
|
||||
const desktop = (hold?: string) =>
|
||||
`/e2e/utils/settings-wsl.html?${new URLSearchParams({
|
||||
server: SERVER,
|
||||
mode: "stopped",
|
||||
storage: "async",
|
||||
path: `/server/${base64Encode("sidecar")}/session/${id}`,
|
||||
...(hold && { hold }),
|
||||
})}`
|
||||
const mode = (name: string) => page.getByRole("button", { name, exact: true })
|
||||
const file = page.locator('[data-slot="session-review-v2-file-name"]')
|
||||
const toggle = page.getByRole("button", { name: "Toggle review", exact: true })
|
||||
|
||||
await page.goto(desktop())
|
||||
await expectSessionTitle(page, title)
|
||||
await toggle.click()
|
||||
await mode("Git changes").click()
|
||||
await page.getByRole("option", { name: "Branch changes" }).click()
|
||||
await expect(file).toHaveText("beta.ts")
|
||||
await expect
|
||||
.poll(() =>
|
||||
page.evaluate(() =>
|
||||
Object.entries(localStorage).some(
|
||||
([key, value]) => key.includes("extension.review.session") && value.includes('"branch"'),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toBe(true)
|
||||
|
||||
// The reopened window's review mounts before its session store has loaded.
|
||||
working.length = 0
|
||||
await page.goto(desktop("extension.review.session"))
|
||||
await expectSessionTitle(page, title)
|
||||
await expect(toggle).toHaveAttribute("aria-expanded", "true")
|
||||
await expect(page.locator("#review-panel").getByText("Loading changes…", { exact: true })).toBeVisible()
|
||||
await expect(mode("Git changes")).toHaveCount(0)
|
||||
await page.getByRole("button", { name: "Load held storage" }).click()
|
||||
await expect(mode("Branch changes")).toBeVisible()
|
||||
await expect(file).toHaveText("beta.ts")
|
||||
expect(working).toEqual([])
|
||||
})
|
||||
|
||||
for (const direction of ["ltr", "rtl"] as const) {
|
||||
test(`review toggle stays at the header edge in ${direction}`, async ({ page }) => {
|
||||
await page.setViewportSize({ width: 1000, height: 900 })
|
||||
|
||||
@@ -25,7 +25,7 @@ for (const custom of [false, true]) {
|
||||
await expect(tooltip.locator('[data-slot="keybind-v2-label"]')).toHaveText(
|
||||
custom ? ["F8"] : mac ? ["⇧", "⌘", "Y"] : ["Ctrl", "Shift", "Y"],
|
||||
)
|
||||
for (const id of [fixture.targetID, fixture.sourceID]) {
|
||||
for (const id of [fixture.sourceID, fixture.targetID, fixture.sourceID]) {
|
||||
await page.locator(`[data-titlebar-tab-link][href="${sessionHref(id)}"]`).click()
|
||||
const messages = id === fixture.sourceID ? fixture.expected.sourceMessageIDs : fixture.expected.targetMessageIDs
|
||||
await expect(
|
||||
|
||||
@@ -43,6 +43,27 @@ for (const mode of ["failed", "stopped", "ready"] as const) {
|
||||
})
|
||||
}
|
||||
|
||||
test("adding a WSL server while the WSL extension is down shows that WSL is unavailable", async ({ page }) => {
|
||||
await mockOpenCodeServer(page, {
|
||||
directory: "/repo",
|
||||
project: project({ id: "proj_wsl_unavailable", directory: "/repo", name: "WSL project" }),
|
||||
provider: NO_PROVIDER,
|
||||
sessions: [],
|
||||
pageMessages: () => ({ items: [] }),
|
||||
})
|
||||
await page.goto(`/e2e/utils/settings-wsl.html?${new URLSearchParams({ server: SERVER, mode: "ready" })}`)
|
||||
const settings = page.getByTestId("settings-screen")
|
||||
await expect(settings.getByRole("tab", { name: "Ubuntu", exact: true })).toHaveCount(1)
|
||||
await page.getByRole("checkbox", { name: "WSL extension" }).uncheck()
|
||||
|
||||
await settings.getByRole("button", { name: "Add server", exact: true }).press("Enter")
|
||||
await page.getByRole("menuitem", { name: "Add WSL server", exact: true }).click()
|
||||
const dialog = page.getByRole("dialog")
|
||||
await expect(dialog.getByRole("heading", { name: "WSL unavailable", exact: true })).toBeVisible()
|
||||
await expect(dialog.getByText("OpenCode could not verify WSL on this machine.", { exact: true })).toBeVisible()
|
||||
await expect(dialog.getByText("WSL is unavailable", { exact: true })).toBeVisible()
|
||||
})
|
||||
|
||||
test("an open session's terminal follows its WSL server to the endpoint it restarts on", async ({ page }) => {
|
||||
const restarted = "http://127.0.0.1:4098"
|
||||
const directory = "/home/ubuntu/project"
|
||||
@@ -131,6 +152,50 @@ test("WSL session and draft tabs outlive the extension going away until the serv
|
||||
await expect(tabs).toHaveCount(0)
|
||||
})
|
||||
|
||||
test("an SSH host that asks for sign-in again opens the dialog once per selected tab", async ({ page }) => {
|
||||
const directory = "/home/box/project"
|
||||
const box = session({ id: "ses_ssh_offer", directory, title: "SSH offer session" })
|
||||
const config = {
|
||||
directory,
|
||||
project: project({ id: "proj_ssh_offer", directory }),
|
||||
provider: provider(),
|
||||
sessions: [box],
|
||||
pageMessages: () => ({ items: [] }),
|
||||
}
|
||||
await mockServers(page, { [SERVER]: { ...config, sessions: [] }, [REMOTE_SERVER]: config })
|
||||
const path = `/server/${base64Encode("ssh:box")}/session/${box.id}`
|
||||
await page.goto(
|
||||
`/e2e/utils/settings-wsl.html?${new URLSearchParams({ server: SERVER, mode: "ready", ssh: REMOTE_SERVER, path })}`,
|
||||
)
|
||||
await expectSessionTitle(page, box.title)
|
||||
const connects = page.getByLabel("SSH connects")
|
||||
const signIn = page.getByRole("button", { name: "Require SSH sign-in" })
|
||||
const authenticate = page.getByRole("button", { name: "Authenticate", exact: true })
|
||||
|
||||
// The first request on the tab opens sign-in by itself; the fixture's host connects without a prompt.
|
||||
await signIn.click()
|
||||
await expect(connects).toHaveText("1")
|
||||
await expectSessionTitle(page, box.title)
|
||||
// A later request on the same tab only offers the button.
|
||||
await signIn.click()
|
||||
await expect(authenticate).toBeVisible()
|
||||
await expect(connects).toHaveText("1")
|
||||
await authenticate.click()
|
||||
await expect(connects).toHaveText("2")
|
||||
await expectSessionTitle(page, box.title)
|
||||
|
||||
// Another tab selected and this one selected again is a new selection, even while the host was ready.
|
||||
const tabs = page.locator("a[data-titlebar-tab-link]")
|
||||
await page.getByRole("button", { name: "New session", exact: true }).click()
|
||||
await expect(tabs).toHaveCount(2)
|
||||
await expect(tabs.nth(1)).toHaveAttribute("href", /^\/new-session\?draftId=/)
|
||||
await expect(page.getByRole("heading", { name: box.title })).toHaveCount(0)
|
||||
await tabs.nth(0).click()
|
||||
await expectSessionTitle(page, box.title)
|
||||
await signIn.click()
|
||||
await expect(connects).toHaveText("3")
|
||||
})
|
||||
|
||||
test("an open session moves to the controller a new SSH sign-in creates", async ({ page }) => {
|
||||
const directory = "/home/box/project"
|
||||
const model = { id: "box-model", name: "Box Model" }
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { expect, test, type Page } from "@playwright/test"
|
||||
import type { ConfigEntry, OpenCodeEvent, WorktreeDirectory } from "@opencode/client/promise"
|
||||
import { NO_PROVIDER, REMOTE_SERVER, SERVER, holdRoute, project, session } from "../utils/app"
|
||||
import { mockRemoteServer, openSettings, type WorkspaceInput } from "../utils/workspace"
|
||||
import { mockRemoteServer, mockWorkspace, openSettings, type WorkspaceInput } from "../utils/workspace"
|
||||
|
||||
const directory = "C:/Projects/settings-demo"
|
||||
const projectID = "proj_settings_demo"
|
||||
@@ -65,6 +65,20 @@ test("settings has its own route and returns through app history", async ({ page
|
||||
await expect(settings).toBeHidden()
|
||||
})
|
||||
|
||||
test("Ctrl+, opens settings as soon as the app loads", async ({ page }) => {
|
||||
await mockWorkspace(page, {
|
||||
name: "Settings demo",
|
||||
directory,
|
||||
provider: NO_PROVIDER,
|
||||
sessions: [],
|
||||
seed: { tabs: [] },
|
||||
})
|
||||
await page.goto("/")
|
||||
await page.keyboard.press("Control+,")
|
||||
await expect(page.getByTestId("settings-screen")).toBeVisible()
|
||||
await expect(page).toHaveURL("/settings")
|
||||
})
|
||||
|
||||
test("a settings page survives refresh", async ({ page }) => {
|
||||
const { settings } = await open(page)
|
||||
const appearance = settings.getByRole("tab", { name: "Appearance", exact: true })
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Schema, SchemaGetter } from "effect"
|
||||
import { HttpApi, HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "effect/unstable/httpapi"
|
||||
import { HttpApi, HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "effect/http-api"
|
||||
import { Pty } from "@opencode/schema/pty"
|
||||
import { Worktree } from "@opencode/schema/worktree"
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { Page } from "@playwright/test"
|
||||
import type { JsonValue, OpenCodeEvent, SessionMessageInfo } from "@opencode/client/promise"
|
||||
import { Duration, Effect, Layer } from "effect"
|
||||
import { HttpRouter, HttpServer, HttpServerResponse } from "effect/unstable/http"
|
||||
import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
|
||||
import { HttpRouter, HttpServer, HttpServerResponse } from "effect/http"
|
||||
import { HttpApiBuilder, HttpApiSchema } from "effect/http-api"
|
||||
import { SERVER } from "./app"
|
||||
import { MockApi, MockBadRequest, MockInternal, MockNotFound, MockShellNotFound, MockUnsupported } from "./mock-api"
|
||||
import { installSseTransport } from "./sse-transport"
|
||||
|
||||
@@ -10,9 +10,10 @@ import { PlatformProvider, type Platform } from "../../src/runtime/platform/plat
|
||||
import { ServerConnection } from "../../src/runtime/server/registry"
|
||||
import { useExtensionServers } from "../../src/runtime/extension/servers"
|
||||
|
||||
// `wsl` is the Ubuntu server's endpoint (default `server`); updating OpenCode restarts it on `restart` (default `wsl`).
|
||||
// `ssh` adds a saved SSH server `box`, ready on that endpoint with password `ssh-1`; each connect brings up a new
|
||||
// remote server with the next password (`ssh-2`, ...).
|
||||
// A desktop window. `wsl` is the Ubuntu server's endpoint (default `server`); updating OpenCode restarts it on `restart`
|
||||
// (default `wsl`). `ssh` adds a saved SSH server `box`, ready on that endpoint with password `ssh-1`; each connect
|
||||
// brings up a new remote server with the next password (`ssh-2`, ...). `storage=async` stores in localStorage behind
|
||||
// asynchronous reads like the desktop app's; reads of keys containing `hold` wait for "Load held storage".
|
||||
export function mount(input: {
|
||||
server: string
|
||||
mode: "failed" | "stopped" | "ready"
|
||||
@@ -20,6 +21,8 @@ export function mount(input: {
|
||||
restart?: string | null
|
||||
path?: string | null
|
||||
ssh?: string | null
|
||||
storage?: string | null
|
||||
hold?: string | null
|
||||
}) {
|
||||
const root = document.getElementById("root")
|
||||
if (!root) throw new Error("Missing fixture root")
|
||||
@@ -27,14 +30,28 @@ export function mount(input: {
|
||||
history.set({ value: input.path ?? "/settings", replace: true, scroll: false })
|
||||
const endpoint = { url: input.wsl ?? input.server }
|
||||
const ready = () => ({ kind: "ready" as const, url: endpoint.url, password: null })
|
||||
const held = Promise.withResolvers<void>()
|
||||
const storage = (name?: string) => {
|
||||
const item = (key: string) => (name ? `${name}:${key}` : key)
|
||||
return {
|
||||
getItem: async (key: string) => {
|
||||
if (input.hold && key.includes(input.hold)) await held.promise
|
||||
return localStorage.getItem(item(key))
|
||||
},
|
||||
setItem: async (key: string, value: string) => localStorage.setItem(item(key), value),
|
||||
removeItem: async (key: string) => localStorage.removeItem(item(key)),
|
||||
}
|
||||
}
|
||||
render(() => {
|
||||
const [store, setStore] = createStore<{
|
||||
calls: string[]
|
||||
connects: number
|
||||
available: boolean
|
||||
state: WslServersState
|
||||
ssh: SshState
|
||||
}>({
|
||||
calls: [],
|
||||
connects: 0,
|
||||
available: true,
|
||||
ssh: {
|
||||
revision: 0,
|
||||
@@ -95,7 +112,6 @@ export function mount(input: {
|
||||
revision: state.revision + 1,
|
||||
servers: state.servers.map((server) => ({ ...server, ...item })),
|
||||
}))
|
||||
const connects = { count: 1 }
|
||||
const methods: Record<string, Record<string, (input: { id?: string; name?: string }) => unknown>> = {
|
||||
wsl: {
|
||||
// Like main: stops the distro's server, updates OpenCode, then starts the server again on a new endpoint.
|
||||
@@ -126,8 +142,8 @@ export function mount(input: {
|
||||
ssh: {
|
||||
// Like main: a connect opens a new tunnel to a remote server with a new password.
|
||||
start() {
|
||||
connects.count += 1
|
||||
setSsh({ stage: "ready", http: { url: input.ssh ?? "", password: `ssh-${connects.count}` } })
|
||||
setStore("connects", (count) => count + 1)
|
||||
setSsh({ stage: "ready", http: { url: input.ssh ?? "", password: `ssh-${store.connects + 1}` } })
|
||||
return store.ssh.revision
|
||||
},
|
||||
},
|
||||
@@ -175,6 +191,7 @@ export function mount(input: {
|
||||
notify: async () => undefined,
|
||||
restart: unused,
|
||||
extensions: bridge,
|
||||
...(input.storage === "async" ? { storage } : {}),
|
||||
}
|
||||
function Interface() {
|
||||
const extensions = useExtensionServers()
|
||||
@@ -221,6 +238,22 @@ export function mount(input: {
|
||||
>
|
||||
Drop SSH tunnel
|
||||
</button>
|
||||
{/* A background reconnect that needs a password, as main reports it to every window. */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setSsh({ stage: "authentication" })
|
||||
publish("ssh")
|
||||
}}
|
||||
>
|
||||
Require SSH sign-in
|
||||
</button>
|
||||
<output aria-label="SSH connects">{store.connects}</output>
|
||||
</Show>
|
||||
<Show when={input.hold}>
|
||||
<button type="button" onClick={() => held.resolve()}>
|
||||
Load held storage
|
||||
</button>
|
||||
</Show>
|
||||
<Interface />
|
||||
</AppBaseProviders>
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
restart: query.get("restart"),
|
||||
path: query.get("path"),
|
||||
ssh: query.get("ssh"),
|
||||
storage: query.get("storage"),
|
||||
hold: query.get("hold"),
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@@ -448,8 +448,8 @@ function openComment(
|
||||
})
|
||||
})
|
||||
}
|
||||
// The extension that owns the comment's origin reveals it (the review diff or a file tab).
|
||||
links.open({ href: item.path, origin: item.commentOrigin, exact: true, session })
|
||||
// The extension that owns the comment's origin reveals it (the review diff or a file tab), keeping the conversation.
|
||||
links.open({ href: item.path, origin: item.commentOrigin, exact: true, background: true, session })
|
||||
if (item.commentOrigin === "review") return queueFocus()
|
||||
void Promise.resolve(files.load(item.path)).finally(() => queueFocus())
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { createMemo } from "solid-js"
|
||||
import { Command } from "@opencode/gui-extensions/sdk"
|
||||
import { useCommand } from "@/shell/commands/command"
|
||||
import { useSettings } from "@/settings/model"
|
||||
@@ -9,27 +10,34 @@ export function ExtensionCommands() {
|
||||
const host = useExtensionHost()
|
||||
const command = useCommand()
|
||||
migrateKeybinds(useSettings())
|
||||
// Extensions activate in whatever order their entries load; list them in definition order, then registration order.
|
||||
const rank = createMemo(() => new Map(host.definitions().map((definition, index) => [definition.id, index])))
|
||||
const order = (extension: string) => rank().get(extension) ?? rank().size
|
||||
command.register("extensions", () =>
|
||||
host.items(Command).map((item) => ({
|
||||
id: `${item.extension}.${item.value.id}`,
|
||||
title: item.value.title,
|
||||
description: item.value.description,
|
||||
category: item.value.group,
|
||||
section: item.value.section,
|
||||
keybind: item.value.bind,
|
||||
slash: item.value.slash?.name,
|
||||
slashArguments: item.value.slash?.arguments,
|
||||
suggested: item.value.suggested,
|
||||
featured: item.value.featured,
|
||||
disabled: item.value.enabled === false,
|
||||
hidden: item.value.hidden,
|
||||
editable: item.value.editable,
|
||||
when: item.value.scope
|
||||
? (event: KeyboardEvent) =>
|
||||
event.target instanceof Element && !!event.target.closest(item.value.scope as string)
|
||||
: undefined,
|
||||
onSelect: (_source: unknown, input?: string) => item.value.run(input),
|
||||
})),
|
||||
host
|
||||
.items(Command)
|
||||
.toSorted((a, b) => order(a.extension) - order(b.extension))
|
||||
.map((item) => ({
|
||||
id: `${item.extension}.${item.value.id}`,
|
||||
title: item.value.title,
|
||||
description: item.value.description,
|
||||
category: item.value.group,
|
||||
section: item.value.section,
|
||||
keybind: item.value.bind,
|
||||
slash: item.value.slash?.name,
|
||||
slashArguments: item.value.slash?.arguments,
|
||||
slashAfter: item.value.slash?.after,
|
||||
suggested: item.value.suggested,
|
||||
featured: item.value.featured,
|
||||
disabled: item.value.enabled === false,
|
||||
hidden: item.value.hidden,
|
||||
editable: item.value.editable,
|
||||
when: item.value.scope
|
||||
? (event: KeyboardEvent) =>
|
||||
event.target instanceof Element && !!event.target.closest(item.value.scope as string)
|
||||
: undefined,
|
||||
onSelect: (_source: unknown, input?: string) => item.value.run(input),
|
||||
})),
|
||||
)
|
||||
return null
|
||||
}
|
||||
@@ -137,9 +137,12 @@ export function createRegion(input: { region: Panel["region"]; view: SessionView
|
||||
]
|
||||
})
|
||||
|
||||
// Narrow screens never select a launcher: a stored one falls back like a missing tab.
|
||||
const desktop = createMediaQuery("(min-width: 768px)")
|
||||
const active = createMemo(() => {
|
||||
const value = input.tabs().active()
|
||||
if (value && strip().some((entry) => entry.key === value)) return value
|
||||
if (value && strip().some((entry) => entry.key === value && (desktop() || entry.tab.kind !== "launcher")))
|
||||
return value
|
||||
return strip()
|
||||
.filter((entry) => entry.tab.fallback !== undefined)
|
||||
.reduce<RegionEntry | undefined>(
|
||||
@@ -157,10 +160,13 @@ export function createRegion(input: { region: Panel["region"]; view: SessionView
|
||||
}),
|
||||
)
|
||||
|
||||
// Hidden tabs keep their place in the strip for selection, focus, and close, but draw no trigger.
|
||||
const drawn = createMemo(() => strip().filter((entry) => !entry.tab.hidden))
|
||||
|
||||
return {
|
||||
entries,
|
||||
/** Strip order by key. Renders iterate keys so a provider's fresh tab objects never remount a trigger. */
|
||||
keys: createMemo(() => strip().map((entry) => entry.key), [], { equals: same }),
|
||||
/** Drawn strip order by key. Renders iterate keys so a provider's fresh tab objects never remount a trigger. */
|
||||
keys: createMemo(() => drawn().map((entry) => entry.key), [], { equals: same }),
|
||||
entry: (key: string) => byKey().get(key),
|
||||
active,
|
||||
selected: createMemo(() => {
|
||||
@@ -171,7 +177,7 @@ export function createRegion(input: { region: Panel["region"]; view: SessionView
|
||||
/** An extension's tab ids in the stored strip. */
|
||||
openFor: (extension: string) =>
|
||||
stored().flatMap((key) => (key.startsWith(`${extension}:`) ? [key.slice(extension.length + 1)] : [])),
|
||||
lead: () => !!strip().find((entry) => entry.tab.kind !== "pinned")?.tab.first,
|
||||
lead: () => !!drawn().find((entry) => entry.tab.kind !== "pinned")?.tab.first,
|
||||
select(key: string) {
|
||||
input.tabs().setActive(key)
|
||||
},
|
||||
@@ -219,7 +225,7 @@ export function RegionContent(props: {
|
||||
<div
|
||||
id={member()?.tab.dom?.panel}
|
||||
role="tabpanel"
|
||||
aria-labelledby={active() ? member()?.tab.dom?.tab : undefined}
|
||||
aria-labelledby={active() && !member()?.tab.hidden ? member()?.tab.dom?.tab : undefined}
|
||||
data-slot="tabs-content"
|
||||
class="h-full min-h-0 overflow-hidden"
|
||||
classList={{ hidden: !active() }}
|
||||
@@ -251,7 +257,7 @@ export function RegionContent(props: {
|
||||
<div
|
||||
id={entry()?.tab.dom?.panel}
|
||||
role="tabpanel"
|
||||
aria-labelledby={entry()?.tab.dom?.tab}
|
||||
aria-labelledby={entry()?.tab.hidden ? undefined : entry()?.tab.dom?.tab}
|
||||
tabIndex={entry()?.tab.tabbable ? 0 : undefined}
|
||||
data-slot="tabs-content"
|
||||
class="flex flex-col h-full overflow-hidden contain-strict"
|
||||
|
||||
@@ -81,7 +81,9 @@ export function createExtensionServices() {
|
||||
const platform = usePlatform()
|
||||
const dialog = useDialog()
|
||||
const language = useLanguage()
|
||||
const narrow = createMediaQuery("(max-width: 767px)")
|
||||
// The session screen's breakpoint, negated, so no fractional width is both narrow and desktop.
|
||||
const desktop = createMediaQuery("(min-width: 768px)")
|
||||
const narrow = () => !desktop()
|
||||
const [attached, setAttached] = createSignal<Attached>()
|
||||
const removed = new Set<(value: { server: string; directory: string }) => void>()
|
||||
const memory = new Map<string, readonly [Store<object>, (mutation: (draft: object) => void) => void]>()
|
||||
@@ -286,12 +288,11 @@ export function createExtensionAttachment(services: ExtensionServices) {
|
||||
const host = useExtensionHost()
|
||||
const command = useCommand()
|
||||
const location = useLocation()
|
||||
const narrow = createMediaQuery("(max-width: 767px)")
|
||||
const desktop = createMediaQuery("(min-width: 768px)")
|
||||
const narrow = () => !desktop()
|
||||
const views = new Map<string, SessionView>()
|
||||
const [mounted, setMounted] = createStore({ revision: 0 })
|
||||
const refs = new Map<string, SessionRef>()
|
||||
// The panel whose toggle opened a session's side region, by session key.
|
||||
const openedFor = new Map<string, string>()
|
||||
|
||||
const connection = (id: string) => global.servers.list().find((item) => ServerConnection.key(item) === id)
|
||||
|
||||
@@ -371,11 +372,15 @@ export function createExtensionAttachment(services: ExtensionServices) {
|
||||
return Array.from(refs.values())
|
||||
})
|
||||
|
||||
const current = createMemo(() => {
|
||||
const routed = createMemo(() => {
|
||||
const value = route()
|
||||
if (value.type !== "session") return
|
||||
return value.type === "session" ? `${value.server}\n${value.sessionId}` : undefined
|
||||
})
|
||||
const current = createMemo(() => {
|
||||
const key = routed()
|
||||
if (!key) return
|
||||
void mounted.revision
|
||||
return views.get(`${value.server}\n${value.sessionId}`)
|
||||
return views.get(key)
|
||||
})
|
||||
|
||||
const scope = (id: string) => {
|
||||
@@ -398,13 +403,19 @@ export function createExtensionAttachment(services: ExtensionServices) {
|
||||
const sideOpened = (session: SessionRef) => !!tabs.pane(shellTab(session), "side")
|
||||
const dockOpened = (session: SessionRef) => !!tabs.pane(shellTab(session), "dock")
|
||||
const setDock = (session: SessionRef, opened: boolean) => tabs.setPane(shellTab(session), "dock", opened)
|
||||
// However the side region closes, it forgets its opener, so a region reopened any other way belongs to the user.
|
||||
createEffect(() => {
|
||||
const open = new Set(sessions().flatMap((session) => (sideOpened(session) ? [session.key] : [])))
|
||||
Array.from(openedFor.keys()).forEach((key) => {
|
||||
if (!open.has(key)) openedFor.delete(key)
|
||||
})
|
||||
})
|
||||
// A token per session whose side region is open, new each time the region opens.
|
||||
const sideVisits = createMemo<ReadonlyMap<string, object>>(
|
||||
(previous) =>
|
||||
new Map(
|
||||
sessions().flatMap((session) =>
|
||||
sideOpened(session) ? [[session.key, previous.get(session.key) ?? {}] as const] : [],
|
||||
),
|
||||
),
|
||||
new Map(),
|
||||
)
|
||||
// The panel whose toggle opened a side region, by the region's token. However the region closes, it reopens with
|
||||
// a new token, so a region reopened any other way belongs to the user.
|
||||
const openedFor = new WeakMap<object, string>()
|
||||
|
||||
// Keys are `${extension}:${tab id}`; the extension's panel decides the region.
|
||||
const provider = (key: string) => {
|
||||
@@ -417,19 +428,18 @@ export function createExtensionAttachment(services: ExtensionServices) {
|
||||
return view?.key === session.key ? view : undefined
|
||||
}
|
||||
|
||||
// The narrow-screen view belongs to the routed, mounted session: it resets to the conversation when that session
|
||||
// changes or unmounts (e.g. on Home). The dock's view follows the dock's own per-session state instead.
|
||||
const [mobile, setMobile] = createStore({ session: undefined as string | undefined, view: "session" })
|
||||
createEffect(
|
||||
on(
|
||||
() => current()?.key,
|
||||
(key) => {
|
||||
if (key !== mobile.session) setMobile({ session: undefined, view: "session" })
|
||||
},
|
||||
),
|
||||
// Counts routing visits: each change of the routed session, including to none (e.g. Home), starts the next one.
|
||||
const visit = createMemo(on(routed, (_key, _previous, count: number = 0) => count + 1))
|
||||
// The narrow-screen view belongs to the routed, mounted session for one visit, and reads as the conversation once
|
||||
// another visit starts. A view selected for a session that is not routed (e.g. a file link that opens Files on
|
||||
// another session) belongs to the next visit, which the navigation that follows starts. The dock's view follows
|
||||
// the dock's own per-session state instead.
|
||||
const [mobile, setMobile] = createStore({ session: undefined as string | undefined, view: "session", visit: 0 })
|
||||
const mobileView = createMemo(() =>
|
||||
mobile.session === current()?.key && mobile.visit === visit() ? mobile.view : "session",
|
||||
)
|
||||
const mobileView = createMemo(() => (mobile.session === current()?.key ? mobile.view : "session"))
|
||||
const selectMobile = (session: SessionRef, view: string) => setMobile({ session: session.key, view })
|
||||
const selectMobile = (session: SessionRef, view: string) =>
|
||||
setMobile({ session: session.key, view, visit: session.key === routed() ? visit() : visit() + 1 })
|
||||
|
||||
// The side tabs a mounted session lists right now, plus `adding` as if it were stored; unmounted sessions have none.
|
||||
const listed = (session: SessionRef, value: string, adding?: string) => {
|
||||
@@ -449,20 +459,17 @@ export function createExtensionAttachment(services: ExtensionServices) {
|
||||
)
|
||||
}
|
||||
|
||||
const open = (
|
||||
key: string,
|
||||
session: SessionRef,
|
||||
options?: { readonly preview?: boolean; readonly focus?: boolean; readonly select?: boolean },
|
||||
) => {
|
||||
const open = (key: string, session: SessionRef, options?: Parameters<Layout["open"]>[2]) => {
|
||||
const item = provider(key)
|
||||
if (item?.value.region === "dock") return setDock(session, true)
|
||||
const value = stateKey(session)
|
||||
if (!value) return
|
||||
// focus: false adds the tab quietly: no selection, no region change, no preview replacement.
|
||||
if (options?.focus === false && !options.preview) return layout.panel.append(value, key)
|
||||
// A select keeps the narrow-screen view and dock, as a background open does, and opens the side region too.
|
||||
if (options?.select)
|
||||
return batch(() => {
|
||||
if (!narrow()) tabs.setPane(shellTab(session), "side", true)
|
||||
tabs.setPane(shellTab(session), "side", true)
|
||||
layout.panel.append(value, key)
|
||||
layout.panel.focus(value, key)
|
||||
})
|
||||
@@ -471,13 +478,14 @@ export function createExtensionAttachment(services: ExtensionServices) {
|
||||
const launchers = new Set(known.flatMap((entry) => (entry.tab.kind === "launcher" ? [entry.key] : [])))
|
||||
const first = known.some((entry) => entry.key === key && entry.tab.first)
|
||||
batch(() => {
|
||||
if (narrow()) {
|
||||
if (narrow() && !options?.background) {
|
||||
setDock(session, false)
|
||||
if (item?.value.mobile) selectMobile(session, `${item.extension}:${item.value.id}`)
|
||||
// A tab its panel does not list on narrow screens stays unstored: the open only selects the panel's view.
|
||||
if (mountedView(session) && !known.some((entry) => entry.key === key)) return
|
||||
// A tab its panel does not list, or a launcher, stays unstored: the open only selects the panel's view.
|
||||
if (mountedView(session) && !known.some((entry) => entry.key === key && entry.tab.kind !== "launcher")) return
|
||||
}
|
||||
if (!narrow()) tabs.setPane(shellTab(session), "side", true)
|
||||
// A background open keeps the narrow-screen view, but its tab still shows once the window is wide.
|
||||
if (!narrow() || options?.background) tabs.setPane(shellTab(session), "side", true)
|
||||
// Pinned tabs are listed without being stored; opening one only selects it.
|
||||
if (known.some((entry) => entry.key === key && entry.tab.kind === "pinned")) return layout.panel.focus(value, key)
|
||||
if (options?.preview) return layout.panel.preview(value, key, launchers)
|
||||
@@ -569,19 +577,21 @@ export function createExtensionAttachment(services: ExtensionServices) {
|
||||
if (provider(key)?.value.region === "dock") return setDock(session, !dockOpened(session))
|
||||
const value = stateKey(session)
|
||||
if (!value) return
|
||||
const region = sideVisits().get(session.key)
|
||||
if (state(key, session) === "visible") {
|
||||
batch(() => {
|
||||
close(key, session)
|
||||
// Closing the last panel the region was opened for also closes the region.
|
||||
if (openedFor.get(session.key) === key && layout.panel.state(value).all.length === 0)
|
||||
if (region && openedFor.get(region) === key && layout.panel.state(value).all.length === 0)
|
||||
tabs.setPane(shellTab(session), "side", false)
|
||||
})
|
||||
return
|
||||
}
|
||||
const opening = !sideOpened(session)
|
||||
if (!opening) openedFor.delete(session.key)
|
||||
// A panel opened into an open region makes the region the user's.
|
||||
if (region) openedFor.delete(region)
|
||||
open(key, session)
|
||||
if (opening && sideOpened(session)) openedFor.set(session.key, key)
|
||||
const opened = sideVisits().get(session.key)
|
||||
if (!region && opened) openedFor.set(opened, key)
|
||||
},
|
||||
state,
|
||||
stored(extension, session) {
|
||||
|
||||
@@ -23,14 +23,23 @@ export function withInitial<S extends Schema.ConstraintCodec<object, unknown>>(
|
||||
initial: NoInfer<S["Type"]>,
|
||||
) {
|
||||
const schema = isMigrated(definition) ? definition.current : definition
|
||||
const read = isMigrated(definition)
|
||||
? SchemaParser.decodeUnknownResult(definition.read, { onExcessProperty: "preserve" })
|
||||
: Result.succeed<unknown>
|
||||
const read = isMigrated(definition) ? SchemaParser.decodeUnknownResult(definition.read) : Result.succeed<unknown>
|
||||
const encode = Schema.encodeUnknownSync(schema)
|
||||
return Schema.Unknown.pipe(
|
||||
Schema.decode<Schema.Unknown>({
|
||||
decode: SchemaGetter.transformOrFail((value) =>
|
||||
Effect.fromResult(Result.map(read(value), (stored) => merge(initial, recover(schema.ast, stored, initial)))),
|
||||
decode: SchemaGetter.transformEffect((value) =>
|
||||
Effect.fromResult(
|
||||
Result.map(read(value), (stored) =>
|
||||
merge(
|
||||
initial,
|
||||
recover(
|
||||
schema.ast,
|
||||
Predicate.isObject(value) && Predicate.isObject(stored) ? { ...value, ...stored } : stored,
|
||||
initial,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
encode: SchemaGetter.transform((value) => encode(value)),
|
||||
}),
|
||||
|
||||
Loaded 100 of 440 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user