Compare commits

...
440 changed files with 6393 additions and 4994 deletions

No files matched your search

+21 -40
View File
@@ -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
View File
@@ -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",
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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 -1
View File
@@ -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,
+2 -1
View File
@@ -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"],
+7 -6
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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"
+3 -2
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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,
+2 -2
View File
@@ -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 -1
View File
@@ -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
+1 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 {
+2 -2
View File
@@ -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,
+2 -1
View File
@@ -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) }),
+9 -8
View File
@@ -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" }),
+2 -1
View File
@@ -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"],
+2 -1
View File
@@ -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 }
}
+2 -1
View File
@@ -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"],
+2 -1
View File
@@ -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"],
+1 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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"
+1 -1
View File
@@ -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"
+1 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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"
+4 -3
View File
@@ -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 -1
View File
@@ -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)
+61 -1
View File
@@ -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" }
+15 -1
View File
@@ -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 -1
View File
@@ -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"
+2 -2
View File
@@ -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>
+2
View File
@@ -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>
+2 -2
View File
@@ -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())
}
+28 -20
View File
@@ -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
}
+12 -6
View File
@@ -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"
+50 -40
View File
@@ -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) {
+14 -5
View File
@@ -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