mirror of
https://github.com/anomalyco/opencode.git
synced 2026-09-22 16:47:34 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7dc227b35a |
@@ -32,7 +32,7 @@
|
||||
},
|
||||
"packages/ai": {
|
||||
"name": "@opencode/ai",
|
||||
"version": "2.0.11",
|
||||
"version": "2.0.10",
|
||||
"dependencies": {
|
||||
"@aws-sdk/credential-providers": "3.1057.0",
|
||||
"@opencode/schema": "workspace:*",
|
||||
@@ -54,7 +54,7 @@
|
||||
},
|
||||
"packages/app": {
|
||||
"name": "@opencode/app",
|
||||
"version": "2.0.11",
|
||||
"version": "2.0.10",
|
||||
"dependencies": {
|
||||
"@corvu/drawer": "catalog:",
|
||||
"@dnd-kit/abstract": "0.5.0",
|
||||
@@ -111,7 +111,7 @@
|
||||
},
|
||||
"packages/cli": {
|
||||
"name": "@opencode/cli",
|
||||
"version": "2.0.11",
|
||||
"version": "2.0.10",
|
||||
"bin": {
|
||||
"opencode2": "./bin/opencode2.cjs",
|
||||
},
|
||||
@@ -175,7 +175,7 @@
|
||||
},
|
||||
"packages/client": {
|
||||
"name": "@opencode/client",
|
||||
"version": "2.0.11",
|
||||
"version": "2.0.10",
|
||||
"dependencies": {
|
||||
"@opencode/protocol": "workspace:*",
|
||||
"@opencode/schema": "workspace:*",
|
||||
@@ -191,7 +191,7 @@
|
||||
"zod": "catalog:",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"effect": "4.0.0-rc.115",
|
||||
"effect": "4.0.0-rc.112",
|
||||
"solid-js": ">=1.9.0",
|
||||
},
|
||||
"optionalPeers": [
|
||||
@@ -201,7 +201,7 @@
|
||||
},
|
||||
"packages/codemode": {
|
||||
"name": "@opencode/codemode",
|
||||
"version": "2.0.11",
|
||||
"version": "2.0.10",
|
||||
"dependencies": {
|
||||
"acorn": "8.15.0",
|
||||
"effect": "catalog:",
|
||||
@@ -214,7 +214,7 @@
|
||||
},
|
||||
"packages/console/app": {
|
||||
"name": "@opencode/console-app",
|
||||
"version": "2.0.11",
|
||||
"version": "2.0.10",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@ibm/plex": "6.4.1",
|
||||
@@ -250,7 +250,7 @@
|
||||
},
|
||||
"packages/console/core": {
|
||||
"name": "@opencode/console-core",
|
||||
"version": "2.0.11",
|
||||
"version": "2.0.10",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sts": "3.782.0",
|
||||
"@jsx-email/render": "1.1.1",
|
||||
@@ -277,7 +277,7 @@
|
||||
},
|
||||
"packages/console/function": {
|
||||
"name": "@opencode/console-function",
|
||||
"version": "2.0.11",
|
||||
"version": "2.0.10",
|
||||
"dependencies": {
|
||||
"@openauthjs/openauth": "0.0.0-20250322224806",
|
||||
"@opencode/console-core": "workspace:*",
|
||||
@@ -294,7 +294,7 @@
|
||||
},
|
||||
"packages/console/mail": {
|
||||
"name": "@opencode/console-mail",
|
||||
"version": "2.0.11",
|
||||
"version": "2.0.10",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
@@ -318,7 +318,7 @@
|
||||
},
|
||||
"packages/console/support": {
|
||||
"name": "@opencode/console-support",
|
||||
"version": "2.0.11",
|
||||
"version": "2.0.10",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@opencode/console-core": "workspace:*",
|
||||
@@ -338,7 +338,7 @@
|
||||
},
|
||||
"packages/core": {
|
||||
"name": "@opencode/core",
|
||||
"version": "2.0.11",
|
||||
"version": "2.0.10",
|
||||
"dependencies": {
|
||||
"@ai-sdk/cohere": "3.0.27",
|
||||
"@ai-sdk/gateway": "3.0.104",
|
||||
@@ -402,12 +402,11 @@
|
||||
"@types/node": "catalog:",
|
||||
"@types/which": "3.0.4",
|
||||
"drizzle-kit": "catalog:",
|
||||
"fast-check": "catalog:",
|
||||
},
|
||||
},
|
||||
"packages/desktop": {
|
||||
"name": "@opencode/desktop",
|
||||
"version": "2.0.11",
|
||||
"version": "2.0.10",
|
||||
"dependencies": {
|
||||
"@zip.js/zip.js": "2.7.62",
|
||||
"electron-context-menu": "4.1.2",
|
||||
@@ -456,7 +455,7 @@
|
||||
},
|
||||
"packages/enterprise": {
|
||||
"name": "@opencode/enterprise",
|
||||
"version": "2.0.11",
|
||||
"version": "2.0.10",
|
||||
"dependencies": {
|
||||
"@hono/standard-validator": "catalog:",
|
||||
"@opencode-ai/sdk": "1.18.21",
|
||||
@@ -493,7 +492,7 @@
|
||||
},
|
||||
"packages/function": {
|
||||
"name": "@opencode/function",
|
||||
"version": "2.0.11",
|
||||
"version": "2.0.10",
|
||||
"dependencies": {
|
||||
"@octokit/auth-app": "8.0.1",
|
||||
"@octokit/rest": "catalog:",
|
||||
@@ -509,9 +508,9 @@
|
||||
},
|
||||
"packages/http-recorder": {
|
||||
"name": "@opencode/http-recorder",
|
||||
"version": "2.0.11",
|
||||
"version": "2.0.10",
|
||||
"dependencies": {
|
||||
"@effect/platform-node-shared": "4.0.0-rc.115",
|
||||
"@effect/platform-node-shared": "4.0.0-rc.112",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@effect/platform-node": "catalog:",
|
||||
@@ -528,7 +527,7 @@
|
||||
},
|
||||
"packages/httpapi-codegen": {
|
||||
"name": "@opencode/httpapi-codegen",
|
||||
"version": "2.0.11",
|
||||
"version": "2.0.10",
|
||||
"dependencies": {
|
||||
"effect": "catalog:",
|
||||
"prettier": "3.6.2",
|
||||
@@ -541,7 +540,7 @@
|
||||
},
|
||||
"packages/latex": {
|
||||
"name": "@opencode/latex",
|
||||
"version": "2.0.11",
|
||||
"version": "2.0.10",
|
||||
"dependencies": {
|
||||
"@opencode/plugin": "workspace:*",
|
||||
"@opentui/core": "catalog:",
|
||||
@@ -555,7 +554,7 @@
|
||||
},
|
||||
"packages/merman": {
|
||||
"name": "@opencode/merman",
|
||||
"version": "2.0.11",
|
||||
"version": "2.0.10",
|
||||
"dependencies": {
|
||||
"@opencode/plugin": "workspace:*",
|
||||
"@opentui/core": "catalog:",
|
||||
@@ -570,7 +569,7 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@opencode/plugin",
|
||||
"version": "2.0.11",
|
||||
"version": "2.0.10",
|
||||
"dependencies": {
|
||||
"@ai-sdk/provider": "3.0.8",
|
||||
"@opencode/ai": "workspace:*",
|
||||
@@ -609,7 +608,7 @@
|
||||
},
|
||||
"packages/plugin-browser": {
|
||||
"name": "@opencode/plugin-browser",
|
||||
"version": "2.0.11",
|
||||
"version": "2.0.10",
|
||||
"dependencies": {
|
||||
"@opencode/plugin": "workspace:*",
|
||||
"@opencode/schema": "workspace:*",
|
||||
@@ -639,7 +638,7 @@
|
||||
},
|
||||
"packages/protocol": {
|
||||
"name": "@opencode/protocol",
|
||||
"version": "2.0.11",
|
||||
"version": "2.0.10",
|
||||
"dependencies": {
|
||||
"@opencode/schema": "workspace:*",
|
||||
"effect": "catalog:",
|
||||
@@ -654,7 +653,7 @@
|
||||
},
|
||||
"packages/schema": {
|
||||
"name": "@opencode/schema",
|
||||
"version": "2.0.11",
|
||||
"version": "2.0.10",
|
||||
"dependencies": {
|
||||
"@standard-schema/spec": "catalog:",
|
||||
"effect": "catalog:",
|
||||
@@ -678,7 +677,7 @@
|
||||
},
|
||||
"packages/sdk": {
|
||||
"name": "@opencode/sdk",
|
||||
"version": "2.0.11",
|
||||
"version": "2.0.10",
|
||||
"dependencies": {
|
||||
"@opencode/client": "workspace:*",
|
||||
"@opencode/core": "workspace:*",
|
||||
@@ -699,7 +698,7 @@
|
||||
},
|
||||
"packages/server": {
|
||||
"name": "@opencode/server",
|
||||
"version": "2.0.11",
|
||||
"version": "2.0.10",
|
||||
"dependencies": {
|
||||
"@effect/platform-node": "catalog:",
|
||||
"@effect/platform-node-shared": "catalog:",
|
||||
@@ -721,7 +720,7 @@
|
||||
},
|
||||
"packages/session-ui": {
|
||||
"name": "@opencode/session-ui",
|
||||
"version": "2.0.11",
|
||||
"version": "2.0.10",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode/client": "workspace:*",
|
||||
@@ -756,7 +755,7 @@
|
||||
},
|
||||
"packages/simulation": {
|
||||
"name": "@opencode/simulation",
|
||||
"version": "2.0.11",
|
||||
"version": "2.0.10",
|
||||
"dependencies": {
|
||||
"@opencode/ai": "workspace:*",
|
||||
"@opencode/core": "workspace:*",
|
||||
@@ -776,7 +775,7 @@
|
||||
},
|
||||
"packages/stats/app": {
|
||||
"name": "@opencode/stats-app",
|
||||
"version": "2.0.11",
|
||||
"version": "2.0.10",
|
||||
"dependencies": {
|
||||
"@ibm/plex": "6.4.1",
|
||||
"@kobalte/core": "catalog:",
|
||||
@@ -810,7 +809,7 @@
|
||||
},
|
||||
"packages/stats/core": {
|
||||
"name": "@opencode/stats-core",
|
||||
"version": "2.0.11",
|
||||
"version": "2.0.10",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-athena": "3.933.0",
|
||||
"@planetscale/database": "1.19.0",
|
||||
@@ -829,7 +828,7 @@
|
||||
},
|
||||
"packages/stats/server": {
|
||||
"name": "@opencode/stats-server",
|
||||
"version": "2.0.11",
|
||||
"version": "2.0.10",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-firehose": "3.933.0",
|
||||
"@effect/platform-node": "catalog:",
|
||||
@@ -875,7 +874,7 @@
|
||||
},
|
||||
"packages/theme": {
|
||||
"name": "@opencode/theme",
|
||||
"version": "2.0.11",
|
||||
"version": "2.0.10",
|
||||
"dependencies": {
|
||||
"@opentui/core": "catalog:",
|
||||
"effect": "catalog:",
|
||||
@@ -889,7 +888,7 @@
|
||||
},
|
||||
"packages/tui": {
|
||||
"name": "@opencode/tui",
|
||||
"version": "2.0.11",
|
||||
"version": "2.0.10",
|
||||
"dependencies": {
|
||||
"@opencode/client": "workspace:*",
|
||||
"@opencode/core": "workspace:*",
|
||||
@@ -924,7 +923,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@opencode/ui",
|
||||
"version": "2.0.11",
|
||||
"version": "2.0.10",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@pierre/diffs": "catalog:",
|
||||
@@ -959,7 +958,7 @@
|
||||
},
|
||||
"packages/util": {
|
||||
"name": "@opencode/util",
|
||||
"version": "2.0.11",
|
||||
"version": "2.0.10",
|
||||
"dependencies": {
|
||||
"@effect/opentelemetry": "catalog:",
|
||||
"@effect/platform-node": "catalog:",
|
||||
@@ -992,7 +991,7 @@
|
||||
},
|
||||
"packages/web": {
|
||||
"name": "@opencode/web",
|
||||
"version": "2.0.11",
|
||||
"version": "2.0.10",
|
||||
"dependencies": {
|
||||
"@astrojs/cloudflare": "12.6.3",
|
||||
"@astrojs/markdown-remark": "6.3.1",
|
||||
@@ -1033,7 +1032,7 @@
|
||||
},
|
||||
"services/update": {
|
||||
"name": "@opencode/update",
|
||||
"version": "2.0.11",
|
||||
"version": "2.0.10",
|
||||
"dependencies": {
|
||||
"jose": "6.0.11",
|
||||
"semver": "catalog:",
|
||||
@@ -1098,10 +1097,10 @@
|
||||
"catalog": {
|
||||
"@cloudflare/workers-types": "4.20251008.0",
|
||||
"@corvu/drawer": "0.2.4",
|
||||
"@effect/opentelemetry": "4.0.0-rc.115",
|
||||
"@effect/platform-node": "4.0.0-rc.115",
|
||||
"@effect/platform-node-shared": "4.0.0-rc.115",
|
||||
"@effect/sql-sqlite-bun": "4.0.0-rc.115",
|
||||
"@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",
|
||||
"@hono/standard-validator": "0.2.0",
|
||||
"@hono/zod-validator": "0.4.2",
|
||||
"@kobalte/core": "0.13.13",
|
||||
@@ -1141,8 +1140,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.115",
|
||||
"fast-check": "4.9.0",
|
||||
"effect": "4.0.0-rc.112",
|
||||
"fuzzysort": "3.1.0",
|
||||
"get-east-asian-width": "1.6.0",
|
||||
"hono": "4.10.7",
|
||||
@@ -1674,13 +1672,13 @@
|
||||
|
||||
"@drizzle-team/brocli": ["@drizzle-team/brocli@0.12.0", "", {}, "sha512-mlUE+rZ8CatQekLhnaiN91Iemdd+e2gFKooGlnRB3oPTL3VghLfX24dx7HrzMNeC1JrIB/0kpsfyty3f5HNfxQ=="],
|
||||
|
||||
"@effect/opentelemetry": ["@effect/opentelemetry@4.0.0-rc.115", "", { "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.115" }, "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-ITOqqf/Z0mrs6XhuV8wb4YYFt0sdMhXZGPtytr+YG87YG6mFdYkc8IaJj8cGQFKRkGota/2CN+zjBKhQVI64OQ=="],
|
||||
"@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/platform-node": ["@effect/platform-node@4.0.0-rc.115", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-rc.115", "undici": "^8.10.2" }, "peerDependencies": { "effect": "^4.0.0-rc.115", "redis": ">=5.0.0 <7.0.0" } }, "sha512-mW03Wjq2FiTL5oi9yOkaKpCyUNBaduKgTk7qMHNELvC21Jq2qOAfdsfH+p+s2zmpB9w2RSa+ApODv4WjO9wQfw=="],
|
||||
"@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-shared": ["@effect/platform-node-shared@4.0.0-rc.115", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.21.3" }, "peerDependencies": { "effect": "^4.0.0-rc.115" } }, "sha512-rcBwhDIfb82akJoTv7Vwh8K5x9vC/XfuePS7JjOr+z+dORXzsmKqI1BUQF27ARCN9fTAbDh7I6YvBlWhLjnqNg=="],
|
||||
"@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/sql-sqlite-bun": ["@effect/sql-sqlite-bun@4.0.0-rc.115", "", { "peerDependencies": { "effect": "^4.0.0-rc.115" } }, "sha512-CypwmCPAUSOOhopcnSI9ocgpKHfpgOdERpei3GuXZm5cCGP31pk2NUETqNaYhonhr2bWd+6Rc8atctnEbIYKYQ=="],
|
||||
"@effect/sql-sqlite-bun": ["@effect/sql-sqlite-bun@4.0.0-rc.112", "", { "peerDependencies": { "effect": "^4.0.0-rc.112" } }, "sha512-EqR8pWZo3VzedRvP6qo5HcfqNmFvSa+sQLWQl3oBMx2KY1FCc8NYjep79wYjNkW0qABnffrPUXCMTOncd1A+5g=="],
|
||||
|
||||
"@electron-internal/extract-zip": ["@electron-internal/extract-zip@1.0.5", "", {}, "sha512-+bqFCP98pLI0Tt0XQo1TmlXtwjWchISndDOxCkEcIuUgXWpBnLyRI+2DU+mesvnMMX6L1XDqYNA0lXNDHd/yiA=="],
|
||||
|
||||
@@ -2014,6 +2012,18 @@
|
||||
|
||||
"@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=="],
|
||||
@@ -3866,7 +3876,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.115", "", {}, "sha512-ogYulZ5ffeOzrJqQrG0XOkDO5lKn2s9KNHhoxJy/6wKUkF0i12dlVXWA9TgsgQ+zV/JzizyG0+XMPepZEQX6vw=="],
|
||||
"effect": ["effect@4.0.0-rc.112", "", { "dependencies": { "fast-check": "^4.9.0", "msgpackr": "^2.0.5" } }, "sha512-wXxwuh1Ywnv4cPRM3Wfa0vDwuOHnZ1TsTgHJkG9XgzND6inhBH9n1vBxhg3iIXOia/OrpmvVmd3lrD4vq6bF3A=="],
|
||||
|
||||
"ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="],
|
||||
|
||||
@@ -4702,7 +4712,7 @@
|
||||
|
||||
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
||||
|
||||
"mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="],
|
||||
"mime": ["mime@4.1.0", "", { "bin": { "mime": "bin/cli.js" } }, "sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw=="],
|
||||
|
||||
"mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
||||
|
||||
@@ -4750,6 +4760,10 @@
|
||||
|
||||
"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=="],
|
||||
@@ -4792,6 +4806,8 @@
|
||||
|
||||
"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=="],
|
||||
@@ -5588,7 +5604,7 @@
|
||||
|
||||
"uncrypto": ["uncrypto@0.1.3", "", {}, "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="],
|
||||
|
||||
"undici": ["undici@8.10.2", "", {}, "sha512-/y4/bH9YNU5hi9NIrpOuvGXFcxrj3CMrV+/AYpowAYTpHn8gX/XPFjNy766FPoYY0miQhdW977JFWKGNhBdwyQ=="],
|
||||
"undici": ["undici@8.10.0", "", {}, "sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ=="],
|
||||
|
||||
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||
|
||||
@@ -6042,6 +6058,8 @@
|
||||
|
||||
"@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=="],
|
||||
@@ -6642,8 +6660,6 @@
|
||||
|
||||
"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
-7
@@ -2,7 +2,7 @@
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "opencode",
|
||||
"description": "AI-powered development tool",
|
||||
"version": "2.0.11",
|
||||
"version": "2.0.10",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"packageManager": "bun@1.4.2",
|
||||
@@ -42,10 +42,10 @@
|
||||
"packages/stats/*"
|
||||
],
|
||||
"catalog": {
|
||||
"@effect/opentelemetry": "4.0.0-rc.115",
|
||||
"@effect/platform-node": "4.0.0-rc.115",
|
||||
"@effect/platform-node-shared": "4.0.0-rc.115",
|
||||
"@effect/sql-sqlite-bun": "4.0.0-rc.115",
|
||||
"@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",
|
||||
"@npmcli/arborist": "9.4.0",
|
||||
"@types/bun": "1.4.0",
|
||||
"@types/cross-spawn": "6.0.6",
|
||||
@@ -79,8 +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.115",
|
||||
"fast-check": "4.9.0",
|
||||
"effect": "4.0.0-rc.112",
|
||||
"ai": "6.0.168",
|
||||
"cross-spawn": "7.0.6",
|
||||
"hono": "4.10.7",
|
||||
|
||||
@@ -14,7 +14,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. Pick a model. The provider helper records provider identity, protocol
|
||||
// choice, capabilities, deployment options, authentication, and defaults.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "2.0.11",
|
||||
"version": "2.0.10",
|
||||
"name": "@opencode/ai",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -278,7 +278,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({
|
||||
|
||||
@@ -73,6 +73,11 @@ const driver = (options: Options, body: string): WebSocketChannelDriver => {
|
||||
)
|
||||
if (event.type === "error") {
|
||||
terminal = true
|
||||
yield* OpenResponses.decodeKnownErrorEvent(event).pipe(
|
||||
Effect.mapError((cause) =>
|
||||
ProviderShared.eventError(options.id, `${options.name} returned a malformed error event`, frame, cause),
|
||||
),
|
||||
)
|
||||
return {
|
||||
type: "provider-failure",
|
||||
error: OpenResponses.providerFailure(event, `${options.name} stream error`, frame),
|
||||
|
||||
@@ -108,7 +108,7 @@ const incremental = (
|
||||
return input.slice(baseline.length)
|
||||
}
|
||||
|
||||
const code = (event: OpenResponses.Event) => OpenResponses.errorDetail(event).code
|
||||
const code = (event: OpenResponses.Event) => event.code || event.error?.code || event.response?.error?.code || undefined
|
||||
|
||||
const rejected = (
|
||||
observation: Extract<ChannelObservation, { readonly type: "provider-failure" }>,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Effect, Option, Schema } from "effect"
|
||||
import { Effect, Option, Schema, SchemaGetter } from "effect"
|
||||
import type { Content } from "@opencode/schema/tool"
|
||||
import { HttpTransport } from "../route/transport/index.js"
|
||||
import { Protocol } from "../route/protocol.js"
|
||||
@@ -333,13 +333,53 @@ export const StreamItem = Schema.StructWithRest(
|
||||
export type StreamItem = Schema.Schema.Type<typeof StreamItem>
|
||||
export type OutputItem = StreamItem & { readonly id: string }
|
||||
|
||||
// Responses-compatible providers put error details at the top level, under `error`, or under
|
||||
// `response.error`, and gateways reshape them freely: strings, numeric codes, extra fields. Those
|
||||
// fields decode as opaque values and `errorDetail` reads them defensively, so an error frame can
|
||||
// only fail on invalid JSON and otherwise always classifies with the raw body as the fallback.
|
||||
// Responses-compatible providers put streaming error details at the top level or
|
||||
// under `error`, and response failures under `response.error`. Accept all three shapes.
|
||||
// https://www.openresponses.org/specification
|
||||
const asText = (value: unknown) =>
|
||||
typeof value === "string" && value.length > 0 ? value : typeof value === "number" ? String(value) : undefined
|
||||
const OpenResponsesErrorObject = Schema.Struct({
|
||||
type: optionalNull(Schema.String),
|
||||
code: optionalNull(Schema.String),
|
||||
message: optionalNull(Schema.String),
|
||||
param: optionalNull(Schema.String),
|
||||
})
|
||||
const OpenResponsesErrorPayload = Schema.Union([Schema.String, OpenResponsesErrorObject]).pipe(
|
||||
Schema.decodeTo(OpenResponsesErrorObject, {
|
||||
decode: SchemaGetter.transform((error) => (typeof error === "string" ? { message: error } : error)),
|
||||
encode: SchemaGetter.passthrough(),
|
||||
}),
|
||||
)
|
||||
type OpenResponsesErrorPayload = Schema.Schema.Type<typeof OpenResponsesErrorPayload>
|
||||
|
||||
const WebSocketErrorHeader = Schema.Union([Schema.String, Schema.Number, Schema.Boolean])
|
||||
export const WebSocketErrorEvent = Schema.StructWithRest(
|
||||
Schema.Struct({
|
||||
type: Schema.tag("error"),
|
||||
status: Schema.optional(Schema.Number),
|
||||
status_code: Schema.optional(Schema.Number),
|
||||
code: optionalNull(Schema.String),
|
||||
message: Schema.optional(Schema.String),
|
||||
param: optionalNull(Schema.String),
|
||||
error: optionalNull(OpenResponsesErrorPayload),
|
||||
headers: Schema.optional(Schema.Record(Schema.String, WebSocketErrorHeader)),
|
||||
}),
|
||||
[Schema.Record(Schema.String, Schema.Unknown)],
|
||||
)
|
||||
const decodeWebSocketErrorEvent = Schema.decodeUnknownEffect(WebSocketErrorEvent)
|
||||
|
||||
export const decodeKnownErrorEvent = (event: Event) =>
|
||||
decodeWebSocketErrorEvent({
|
||||
...event,
|
||||
status: typeof event.status === "number" ? event.status : undefined,
|
||||
status_code: typeof event.status_code === "number" ? event.status_code : undefined,
|
||||
headers: ProviderShared.isRecord(event.headers)
|
||||
? Object.fromEntries(
|
||||
Object.entries(event.headers).filter(
|
||||
(entry): entry is [string, string | number | boolean] =>
|
||||
typeof entry[1] === "string" || typeof entry[1] === "number" || typeof entry[1] === "boolean",
|
||||
),
|
||||
)
|
||||
: undefined,
|
||||
})
|
||||
|
||||
export const Event = Schema.StructWithRest(
|
||||
Schema.Struct({
|
||||
@@ -360,18 +400,31 @@ export const Event = Schema.StructWithRest(
|
||||
incomplete_details: optionalNull(Schema.Struct({ reason: Schema.optional(Schema.String) })),
|
||||
output: Schema.optional(Schema.Array(StreamItem)),
|
||||
usage: optionalNull(OpenResponsesUsage),
|
||||
error: Schema.optional(Schema.Unknown),
|
||||
error: optionalNull(OpenResponsesErrorPayload),
|
||||
}),
|
||||
[Schema.Record(Schema.String, Schema.Unknown)],
|
||||
),
|
||||
),
|
||||
code: Schema.optional(Schema.Unknown),
|
||||
message: Schema.optional(Schema.Unknown),
|
||||
error: Schema.optional(Schema.Unknown),
|
||||
code: optionalNull(Schema.String),
|
||||
message: Schema.optional(Schema.String),
|
||||
param: optionalNull(Schema.String),
|
||||
error: optionalNull(OpenResponsesErrorPayload),
|
||||
status: Schema.optional(Schema.Unknown),
|
||||
status_code: Schema.optional(Schema.Unknown),
|
||||
headers: Schema.optional(Schema.Unknown),
|
||||
}),
|
||||
[Schema.Record(Schema.String, Schema.Unknown)],
|
||||
).pipe(
|
||||
Schema.decode({
|
||||
decode: SchemaGetter.transform((event) => {
|
||||
if (event.type !== "error" || event.error != null) return event
|
||||
const { code, message, param, ...rest } = event
|
||||
if (code === undefined && message === undefined && param === undefined) return event
|
||||
// Flat errors (for example, Meta's) can also arrive through generic Responses endpoints.
|
||||
return { ...rest, error: { code, message, param } }
|
||||
}),
|
||||
encode: SchemaGetter.passthrough(),
|
||||
}),
|
||||
)
|
||||
export type Event = Schema.Schema.Type<typeof Event>
|
||||
export type NormalizedEvent = Event & { readonly item?: OutputItem | null }
|
||||
@@ -380,15 +433,16 @@ const decodeEventValue = Schema.decodeUnknownEffect(Event)
|
||||
const decodeFrame = Schema.decodeUnknownEffect(ProviderShared.Json)
|
||||
|
||||
/**
|
||||
* Decodes one WebSocket frame. Some providers and gateways answer a rejected `response.create` with a bare
|
||||
* `{ "error": ... }` envelope and no event type; that reads as an error event so it classifies instead of
|
||||
* failing decoding.
|
||||
* Decodes one WebSocket frame. xAI answers a rejected `response.create` with `{ "error": { "message", "type" } }` and no
|
||||
* event type; that envelope reads as an error event so the failure classifies instead of failing decoding.
|
||||
*/
|
||||
export const decodeChannelEvent = (frame: string) =>
|
||||
decodeFrame(frame).pipe(
|
||||
Effect.flatMap((value) =>
|
||||
decodeEventValue(
|
||||
ProviderShared.isRecord(value) && value.type === undefined && value.error != null
|
||||
ProviderShared.isRecord(value) &&
|
||||
value.type === undefined &&
|
||||
(typeof value.error === "string" || ProviderShared.isRecord(value.error))
|
||||
? { ...value, type: "error" }
|
||||
: value,
|
||||
),
|
||||
@@ -1368,21 +1422,22 @@ const onResponseFinish = Effect.fn("OpenResponses.onResponseFinish")(function* (
|
||||
return [{ ...current, lifecycle }, events] satisfies StepResult
|
||||
})
|
||||
|
||||
/** Error code and message from wherever the frame put them; top-level fields win over nested ones. */
|
||||
export const errorDetail = (event: Event) => {
|
||||
const raw = event.error ?? event.response?.error
|
||||
const nested = typeof raw === "string" ? { message: raw } : ProviderShared.isRecord(raw) ? raw : undefined
|
||||
return {
|
||||
message: asText(event.message) ?? asText(nested?.message),
|
||||
code: asText(event.code) ?? asText(nested?.code),
|
||||
}
|
||||
// Build the prettiest summary available from whatever the provider supplied.
|
||||
// When both code and message are present, prefix the code so consumers see
|
||||
// the failure mode (e.g. `rate_limit_exceeded: Slow down`) instead of just
|
||||
// the bare message — production rate limits and context-length failures used
|
||||
// to be indistinguishable from generic stream drops. Returns undefined when
|
||||
// the payload carries no usable summary.
|
||||
const providerErrorMessage = (event: Event, nested: OpenResponsesErrorPayload | undefined): string | undefined => {
|
||||
const message = event.message || nested?.message || undefined
|
||||
const code = event.code || nested?.code || undefined
|
||||
if (message && code) return `${code}: ${message}`
|
||||
return message || code
|
||||
}
|
||||
|
||||
// Prefix the code when both are present (`rate_limit_exceeded: Slow down`) so the failure mode is
|
||||
// visible; fall back to the raw frame rather than a generic message when neither decodes.
|
||||
export const providerFailure = (event: Event, fallback: string, body = ProviderShared.encodeJson(event)) => {
|
||||
const detail = errorDetail(event)
|
||||
const summary = detail.message && detail.code ? `${detail.code}: ${detail.message}` : (detail.message ?? detail.code)
|
||||
const nested = event.error ?? event.response?.error ?? undefined
|
||||
const summary = providerErrorMessage(event, nested)
|
||||
const message = summary ?? (body === "{}" ? fallback : body)
|
||||
const status =
|
||||
typeof event.status === "number"
|
||||
@@ -1465,7 +1520,18 @@ export const step = (state: ParserState, event: NormalizedEvent) => {
|
||||
if (event.type === "response.output_item.done") return onOutputItemDone(state, event.item)
|
||||
if (event.type === "response.completed" || event.type === "response.incomplete") return onResponseFinish(state, event)
|
||||
if (event.type === "response.failed") return providerFailure(event, `${state.name} response failed`)
|
||||
if (event.type === "error") return providerFailure(event, `${state.name} stream error`)
|
||||
if (event.type === "error")
|
||||
return decodeKnownErrorEvent(event).pipe(
|
||||
Effect.mapError((cause) =>
|
||||
ProviderShared.eventError(
|
||||
state.id,
|
||||
`${state.name} returned a malformed error event`,
|
||||
ProviderShared.encodeJson(event),
|
||||
cause,
|
||||
),
|
||||
),
|
||||
Effect.flatMap(() => providerFailure(event, `${state.name} stream error`)),
|
||||
)
|
||||
return Effect.succeed<StepResult>([state, NO_EVENTS])
|
||||
}
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ export const optional = (secret: Secret | undefined, source = "optional value")
|
||||
? credential(Effect.fail(new MissingCredentialError(source)))
|
||||
: credentialFromSecret(secret, source)
|
||||
|
||||
export const config = (name: string) => credentialFromSecret(Config.Redacted(name), name)
|
||||
export const config = (name: string) => credentialFromSecret(Config.redacted(name), name)
|
||||
|
||||
export const effect = (load: Effect.Effect<Redacted.Redacted, CredentialError>) => credential(load)
|
||||
|
||||
|
||||
@@ -215,9 +215,7 @@ export const fromWebSocket = (
|
||||
): Effect.Effect<WebSocketConnection, AIError> =>
|
||||
Effect.gen(function* () {
|
||||
yield* waitOpen(ws, input)
|
||||
// The socket pushes frames synchronously and cannot be paused, so the hand-off to the consumer
|
||||
// fiber must absorb whole read buffers. Bun delivers over a thousand small frames in one tick.
|
||||
const messages = yield* Queue.unbounded<string | Uint8Array, AIError | Cause.Done<void>>()
|
||||
const messages = yield* Queue.bounded<string | Uint8Array, AIError | Cause.Done<void>>(128)
|
||||
|
||||
const oversized = (message: string | Uint8Array) =>
|
||||
typeof message === "string" ? new Blob([message]).size > MAX_FRAME_BYTES : message.byteLength > MAX_FRAME_BYTES
|
||||
@@ -240,7 +238,19 @@ export const fromWebSocket = (
|
||||
}
|
||||
const offer = (message: string | Uint8Array) => {
|
||||
if (rejectOversized(message)) return
|
||||
Queue.offerUnsafe(messages, message)
|
||||
if (Queue.offerUnsafe(messages, message)) return
|
||||
Queue.failCauseUnsafe(
|
||||
messages,
|
||||
Cause.fail(
|
||||
transportError("WebSocket inbound queue overflow", {
|
||||
body: typeof message === "string" ? message : new TextDecoder().decode(message),
|
||||
url: input.url,
|
||||
operation: "read",
|
||||
code: "queue-overflow",
|
||||
phase: "receive",
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
const onMessage = (event: MessageEvent) => {
|
||||
|
||||
@@ -231,7 +231,7 @@ export const toDefinitions = (tools: Tools): ReadonlyArray<ToolDefinitionClass>
|
||||
)
|
||||
|
||||
const toJsonSchema = (schema: Schema.Top): JsonSchema.JsonSchema => {
|
||||
const document = Schema.toJsonSchemaDocument(schema, { onExcessProperty: "error" })
|
||||
const document = Schema.toJsonSchemaDocument(schema)
|
||||
if (Object.keys(document.definitions).length === 0) return document.schema
|
||||
return { ...document.schema, $defs: document.definitions }
|
||||
}
|
||||
|
||||
@@ -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" })
|
||||
|
||||
|
||||
@@ -11,78 +11,66 @@ import { sseEvents } from "../lib/sse.js"
|
||||
|
||||
const decodeEvent = Schema.decodeUnknownEffect(OpenResponses.protocol.stream.event)
|
||||
|
||||
it.effect("decodes error frames verbatim in shared SSE and WebSocket decoding", () =>
|
||||
it.effect("normalizes flat errors in shared SSE and WebSocket decoding", () =>
|
||||
Effect.gen(function* () {
|
||||
const frame = {
|
||||
type: "error",
|
||||
sequence_number: 4,
|
||||
code: "server_shutting_down",
|
||||
message: "Server is shutting down. Please retry your request.",
|
||||
param: null,
|
||||
}
|
||||
for (const decode of [decodeEvent, OpenResponses.decodeChannelEvent]) {
|
||||
for (const frame of [
|
||||
{ type: "error", sequence_number: 4, code: "server_shutting_down", message: "Shutting down", param: null },
|
||||
const event = yield* decode(JSON.stringify(frame))
|
||||
expect(event).toEqual({
|
||||
type: "error",
|
||||
sequence_number: 4,
|
||||
error: { code: frame.code, message: frame.message, param: null },
|
||||
})
|
||||
|
||||
for (const unchanged of [
|
||||
event,
|
||||
{ type: "error" },
|
||||
{ type: "error", error: "Gateway failed" },
|
||||
{ type: "error", error: { code: 429, message: "slow down" } },
|
||||
{ type: "error", error: 42 },
|
||||
{ type: "error", code: 500, message: ["not", "a", "string"] },
|
||||
{ type: "response.failed", response: { id: "resp_failed", error: "Gateway failed" } },
|
||||
{ type: "response.failed", response: { id: "resp_failed", error: ["weird"] } },
|
||||
{
|
||||
type: "response.failed",
|
||||
response: { id: "resp_failed", error: { code: "server_error", message: "Internal server error" } },
|
||||
},
|
||||
{ type: "response.output_text.delta", item_id: "msg_text", delta: "Hello" },
|
||||
]) {
|
||||
expect(yield* decode(JSON.stringify(frame))).toEqual(frame)
|
||||
expect(yield* decode(JSON.stringify(unchanged))).toEqual(unchanged)
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("reads bare WebSocket error envelopes as error events", () =>
|
||||
it.effect("continues to normalize untyped xAI WebSocket errors", () =>
|
||||
Effect.gen(function* () {
|
||||
const message = "gRPC error: Response with id=resp_missing not found"
|
||||
for (const error of [{ type: "api_error", message }, message, 42]) {
|
||||
expect(yield* OpenResponses.decodeChannelEvent(JSON.stringify({ error }))).toEqual({ type: "error", error })
|
||||
}
|
||||
for (const frame of [{ error: null }, { message }]) {
|
||||
expect(yield* OpenResponses.decodeChannelEvent(JSON.stringify(frame)).pipe(Effect.flip)).toBeDefined()
|
||||
for (const error of [{ type: "api_error", message }, message]) {
|
||||
expect(yield* OpenResponses.decodeChannelEvent(JSON.stringify({ error }))).toEqual({
|
||||
type: "error",
|
||||
error: typeof error === "string" ? { message } : error,
|
||||
})
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("extracts error details from every shape and falls back to the raw frame", () =>
|
||||
it.effect("normalizes string errors in shared SSE and WebSocket decoding", () =>
|
||||
Effect.gen(function* () {
|
||||
const cases: Array<[frame: Record<string, unknown>, message: string, tag: string]> = [
|
||||
[
|
||||
{ type: "error", code: "server_shutting_down", message: "Shutting down" },
|
||||
"server_shutting_down: Shutting down",
|
||||
"UnknownProvider",
|
||||
],
|
||||
[{ type: "error", error: "Gateway failed" }, "Gateway failed", "UnknownProvider"],
|
||||
[{ type: "error", error: { code: 429, message: "slow down" } }, "429: slow down", "UnknownProvider"],
|
||||
[{ type: "error", error: { message: "slow down" }, status: 429 }, "slow down", "RateLimit"],
|
||||
[{ type: "error", code: 500, message: ["not", "a", "string"] }, "500", "UnknownProvider"],
|
||||
[
|
||||
{ type: "response.failed", response: { id: "resp_failed", error: "Gateway failed" } },
|
||||
"Gateway failed",
|
||||
"UnknownProvider",
|
||||
],
|
||||
]
|
||||
for (const [frame, message, tag] of cases) {
|
||||
const event = yield* OpenResponses.decodeChannelEvent(JSON.stringify(frame))
|
||||
const error = OpenResponses.providerFailure(event, "fallback", JSON.stringify(frame))
|
||||
expect(error.message).toBe(message)
|
||||
expect(error.reason._tag).toBe(tag)
|
||||
expect(error.reason.body).toBe(JSON.stringify(frame))
|
||||
for (const decode of [decodeEvent, OpenResponses.decodeChannelEvent]) {
|
||||
expect(yield* decode(JSON.stringify({ type: "error", error: "Gateway failed" }))).toEqual({
|
||||
type: "error",
|
||||
error: { message: "Gateway failed" },
|
||||
})
|
||||
expect(
|
||||
yield* decode(
|
||||
JSON.stringify({ type: "response.failed", response: { id: "resp_failed", error: "Gateway failed" } }),
|
||||
),
|
||||
).toEqual({
|
||||
type: "response.failed",
|
||||
response: { id: "resp_failed", error: { message: "Gateway failed" } },
|
||||
})
|
||||
}
|
||||
for (const frame of [
|
||||
{ type: "error", error: 42 },
|
||||
{ type: "response.failed", response: { id: "resp_failed", error: ["weird"] } },
|
||||
]) {
|
||||
const event = yield* OpenResponses.decodeChannelEvent(JSON.stringify(frame))
|
||||
const error = OpenResponses.providerFailure(event, "fallback", JSON.stringify(frame))
|
||||
expect(error.message).toBe(JSON.stringify(frame))
|
||||
expect(error.reason._tag).toBe("UnknownProvider")
|
||||
}
|
||||
expect(OpenResponses.providerFailure({ type: "error" }, "fallback", "{}").message).toBe("fallback")
|
||||
expect(OpenResponses.providerFailure({ type: "error" }, "fallback", "{}").reason._tag).toBe("ProviderInternal")
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@@ -19,13 +19,6 @@ export function mountBrowserPane() {
|
||||
session: "Alpha",
|
||||
mounted: true,
|
||||
visible: true,
|
||||
url: undefined as string | undefined,
|
||||
loading: false,
|
||||
generation: 0,
|
||||
delayNavigation: false,
|
||||
pendingURL: undefined as string | undefined,
|
||||
loadErrors: {} as Record<string, string | undefined>,
|
||||
error: undefined as string | undefined,
|
||||
layouts: {} as Record<string, BrowserPaneLayout | undefined>,
|
||||
})
|
||||
const tabs = ["Alpha", "Beta"].map((name) => ({
|
||||
@@ -54,33 +47,13 @@ export function mountBrowserPane() {
|
||||
opened: () => !!registrations.get(store.session),
|
||||
state: () => ({ tabs: tabs.filter((tab) => tab.title === store.session), focusedTabID: null }),
|
||||
tabs: () => tabs.filter((tab) => tab.title === store.session),
|
||||
active: () => {
|
||||
const tab = tabs.find((tab) => tab.title === store.session) ?? tabs[0]
|
||||
return {
|
||||
...tab,
|
||||
url: store.url ?? tab.url,
|
||||
loading: store.loading,
|
||||
generation: store.generation,
|
||||
loadError: store.loadErrors[store.session],
|
||||
}
|
||||
},
|
||||
active: () => tabs.find((tab) => tab.title === store.session) ?? tabs[0],
|
||||
registration: () => registrations.get(store.session),
|
||||
error: () => store.error ?? (store.loadErrors[store.session] ? "Request failed" : undefined),
|
||||
error: () => undefined,
|
||||
suspended: () => false,
|
||||
close: () => undefined,
|
||||
open: () => undefined,
|
||||
command: (command) => {
|
||||
setStore("error", undefined)
|
||||
if (command.type === "navigate" || command.type === "reload") setStore("loadErrors", store.session, undefined)
|
||||
if (command.type === "navigate") {
|
||||
if (store.delayNavigation) {
|
||||
setStore("pendingURL", command.url)
|
||||
return
|
||||
}
|
||||
setStore({ url: command.url, loading: false, generation: store.generation + 1 })
|
||||
}
|
||||
if (command.type === "stop") setStore("loading", false)
|
||||
},
|
||||
command: () => undefined,
|
||||
}
|
||||
return (
|
||||
<>
|
||||
@@ -91,32 +64,6 @@ export function mountBrowserPane() {
|
||||
{(name) => <button onClick={() => setStore({ session: name, mounted: name !== "Empty" })}>{name}</button>}
|
||||
</For>
|
||||
<button onClick={() => setStore("mounted", false)}>Unmount pane</button>
|
||||
<button onClick={() => setStore({ url: "about:blank", loading: false })}>Blank page</button>
|
||||
<button onClick={() => setStore({ url: "about:blank", loading: true })}>Loading page</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
setStore({ loading: true, generation: store.generation + 1, loadErrors: { [store.session]: undefined } })
|
||||
}
|
||||
>
|
||||
Load current page
|
||||
</button>
|
||||
<button onClick={() => setStore("loadErrors", store.session, "ERR_CONNECTION_REFUSED")}>Failed page</button>
|
||||
<button onClick={() => setStore("delayNavigation", true)}>Delay navigation</button>
|
||||
<button onClick={() => setStore({ error: "ERR_BLOCKED_BY_CLIENT", pendingURL: undefined })}>
|
||||
Block navigation
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
setStore({
|
||||
url: store.pendingURL,
|
||||
pendingURL: undefined,
|
||||
loading: false,
|
||||
generation: store.generation + 1,
|
||||
})
|
||||
}
|
||||
>
|
||||
Complete navigation
|
||||
</button>
|
||||
<button onClick={() => setStore("visible", (visible) => !visible)}>Toggle Review tab</button>
|
||||
</nav>
|
||||
<div style={{ width: "640px", height: "360px", border: "1px solid #555" }}>
|
||||
|
||||
@@ -57,153 +57,3 @@ story("hides the native view immediately while the pane stays mounted", async ({
|
||||
await toggle.click()
|
||||
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "true")
|
||||
})
|
||||
|
||||
story("shows the empty state over a blank native page and restores navigation", async ({ page }) => {
|
||||
const root = page.getByTestId("browser-pane-fixture")
|
||||
await root.getByRole("button", { name: "Blank page", exact: true }).click()
|
||||
await expect(root.getByText("Enter URL", { exact: true })).toBeVisible()
|
||||
await expect(root.getByText('Or prompt "Open in the app browser"', { exact: true })).toBeVisible()
|
||||
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "false")
|
||||
await expect(root.getByRole("button", { name: "Reload", exact: true })).toBeDisabled()
|
||||
|
||||
const address = root.getByRole("textbox", { name: "Browser address", exact: true })
|
||||
await address.fill("https://example.com/")
|
||||
await expect(root.getByText("Enter URL", { exact: true })).toBeVisible()
|
||||
await address.press("Enter")
|
||||
await expect(address).not.toBeFocused()
|
||||
await expect(root.getByText("Enter URL", { exact: true })).toBeHidden()
|
||||
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "true")
|
||||
await expect(root.getByRole("button", { name: "Reload", exact: true })).toBeEnabled()
|
||||
})
|
||||
|
||||
story("keeps Stop available and hides the empty state while a blank page loads", async ({ page }) => {
|
||||
const root = page.getByTestId("browser-pane-fixture")
|
||||
await root.getByRole("button", { name: "Loading page", exact: true }).click()
|
||||
await expect(root.getByText("Enter URL", { exact: true })).toBeHidden()
|
||||
await root.getByRole("button", { name: "Stop", exact: true }).click()
|
||||
await expect(root.getByText("Enter URL", { exact: true })).toBeVisible()
|
||||
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "false")
|
||||
})
|
||||
|
||||
story("keeps the submitted URL visible until the browser reports navigation", async ({ page }) => {
|
||||
const root = page.getByTestId("browser-pane-fixture")
|
||||
await root.getByRole("button", { name: "Blank page", exact: true }).click()
|
||||
await root.getByRole("button", { name: "Delay navigation", exact: true }).click()
|
||||
const address = root.getByRole("textbox", { name: "Browser address", exact: true })
|
||||
await address.fill("https://example.com/")
|
||||
await address.press("Enter")
|
||||
await expect(address).not.toBeFocused()
|
||||
await expect(address).toHaveValue("https://example.com/")
|
||||
await expect(root.getByText("Enter URL", { exact: true })).toBeHidden()
|
||||
|
||||
await root.getByRole("button", { name: "Complete navigation", exact: true }).click()
|
||||
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "true")
|
||||
await expect(address).toHaveValue("https://example.com/")
|
||||
|
||||
await address.fill("https://unsubmitted.example/")
|
||||
await root.getByRole("button", { name: "Delay navigation", exact: true }).click()
|
||||
await expect(address).toHaveValue("https://example.com/")
|
||||
})
|
||||
|
||||
story("restores the current URL when a submitted navigation is blocked", async ({ page }) => {
|
||||
const root = page.getByTestId("browser-pane-fixture")
|
||||
await root.getByRole("button", { name: "Delay navigation", exact: true }).click()
|
||||
const address = root.getByRole("textbox", { name: "Browser address", exact: true })
|
||||
await address.fill("https://blocked.example/")
|
||||
await address.press("Enter")
|
||||
await expect(address).toHaveValue("https://blocked.example/")
|
||||
await root.getByRole("button", { name: "Block navigation", exact: true }).click()
|
||||
await expect(root.getByText("ERR_BLOCKED_BY_CLIENT", { exact: true })).toBeVisible()
|
||||
await expect(address).toHaveValue("https://alpha.example/")
|
||||
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "true")
|
||||
})
|
||||
|
||||
story("shows a themed failure state for only the failed tab and allows retry", async ({ page }) => {
|
||||
const root = page.getByTestId("browser-pane-fixture")
|
||||
await root.getByRole("button", { name: "Failed page", exact: true }).click()
|
||||
await expect(root.getByText("URL can't be reached", { exact: true })).toBeVisible()
|
||||
await expect(root.getByText("Check the URL and your connection, then try again.", { exact: true })).toBeVisible()
|
||||
await expect(root.getByText("Request failed", { exact: true })).toBeHidden()
|
||||
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "false")
|
||||
await expect(root.getByRole("textbox", { name: "Browser address", exact: true })).toHaveValue(
|
||||
"https://alpha.example/",
|
||||
)
|
||||
|
||||
await root.getByRole("button", { name: "Beta", exact: true }).click()
|
||||
await expect(root.getByText("URL can't be reached", { exact: true })).toBeHidden()
|
||||
await expect(root.getByTestId("native-Beta")).toHaveAttribute("data-visible", "true")
|
||||
await root.getByRole("button", { name: "Alpha", exact: true }).click()
|
||||
await expect(root.getByText("URL can't be reached", { exact: true })).toBeVisible()
|
||||
|
||||
await root.getByRole("button", { name: "Reload", exact: true }).click()
|
||||
await expect(root.getByText("URL can't be reached", { exact: true })).toBeHidden()
|
||||
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "true")
|
||||
})
|
||||
|
||||
story("returns a failed tab to the empty state when an empty URL is submitted", async ({ page }) => {
|
||||
const root = page.getByTestId("browser-pane-fixture")
|
||||
await root.getByRole("button", { name: "Failed page", exact: true }).click()
|
||||
await expect(root.getByText("URL can't be reached", { exact: true })).toBeVisible()
|
||||
await root.getByRole("button", { name: "Delay navigation", exact: true }).click()
|
||||
|
||||
const address = root.getByRole("textbox", { name: "Browser address", exact: true })
|
||||
await address.fill("")
|
||||
await address.press("Enter")
|
||||
await expect(address).not.toBeFocused()
|
||||
await expect(address).toHaveValue("")
|
||||
await root.getByRole("button", { name: "Load current page", exact: true }).click()
|
||||
await expect(root.getByRole("button", { name: "Stop", exact: true })).toBeEnabled()
|
||||
await root.getByRole("button", { name: "Complete navigation", exact: true }).click()
|
||||
await expect(root.getByText("URL can't be reached", { exact: true })).toBeHidden()
|
||||
await expect(root.getByText("Enter URL", { exact: true })).toBeVisible()
|
||||
await expect(root.getByRole("button", { name: "Reload", exact: true })).toBeDisabled()
|
||||
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "false")
|
||||
})
|
||||
|
||||
story("selects the full URL when the address field gains focus", async ({ page }) => {
|
||||
const root = page.getByTestId("browser-pane-fixture")
|
||||
const address = root.getByRole("textbox", { name: "Browser address", exact: true })
|
||||
await address.click()
|
||||
await expect(address).toHaveJSProperty("selectionStart", 0)
|
||||
await expect(address).toHaveJSProperty("selectionEnd", "https://alpha.example/".length)
|
||||
await address.pressSequentially("https://example.com/")
|
||||
await expect(address).toHaveValue("https://example.com/")
|
||||
await address.press("Enter")
|
||||
await expect(address).not.toBeFocused()
|
||||
await address.focus()
|
||||
await expect(address).toHaveJSProperty("selectionStart", 0)
|
||||
await expect(address).toHaveJSProperty("selectionEnd", "https://example.com/".length)
|
||||
await address.press("ArrowRight")
|
||||
await address.click()
|
||||
await expect(address).toHaveJSProperty("selectionStart", 0)
|
||||
await expect(address).toHaveJSProperty("selectionEnd", "https://example.com/".length)
|
||||
})
|
||||
|
||||
story("keeps the current page visible while a submitted URL loads", async ({ page }) => {
|
||||
const root = page.getByTestId("browser-pane-fixture")
|
||||
await root.getByRole("button", { name: "Delay navigation", exact: true }).click()
|
||||
const address = root.getByRole("textbox", { name: "Browser address", exact: true })
|
||||
await address.fill("https://example.com/")
|
||||
await address.press("Enter")
|
||||
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "true")
|
||||
await expect(address).toHaveValue("https://example.com/")
|
||||
|
||||
await root.getByRole("button", { name: "Load current page", exact: true }).click()
|
||||
await expect(root.getByRole("button", { name: "Stop", exact: true })).toBeEnabled()
|
||||
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "true")
|
||||
await root.getByRole("button", { name: "Complete navigation", exact: true }).click()
|
||||
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "true")
|
||||
await expect(address).toHaveValue("https://example.com/")
|
||||
})
|
||||
|
||||
story("keeps the current page and restores its URL when an empty address is submitted", async ({ page }) => {
|
||||
const root = page.getByTestId("browser-pane-fixture")
|
||||
const address = root.getByRole("textbox", { name: "Browser address", exact: true })
|
||||
await address.fill("")
|
||||
await address.press("Enter")
|
||||
await expect(address).not.toBeFocused()
|
||||
await expect(address).toHaveValue("https://alpha.example/")
|
||||
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "true")
|
||||
await expect(root.getByText("Enter URL", { exact: true })).toBeHidden()
|
||||
await expect(root.getByRole("button", { name: "Reload", exact: true })).toBeEnabled()
|
||||
})
|
||||
|
||||
@@ -71,7 +71,7 @@ for (const direction of ["ltr", "rtl"] as const) {
|
||||
const shifted = await content.evaluate((element) => getComputedStyle(element).translate)
|
||||
await content.evaluate((element) => element.setAttribute("data-summary-motion", ""))
|
||||
// Keep issuing resize events before the idle timer expires, including crossing the width cutoff.
|
||||
for (const width of [1520, 1280, 1800]) {
|
||||
for (const width of [1520, 1280, 1600]) {
|
||||
await page.setViewportSize({ width, height: 900 })
|
||||
await expect(panel).toHaveAttribute("data-summary-resizing", "true")
|
||||
await page.clock.runFor(100)
|
||||
@@ -84,7 +84,6 @@ for (const direction of ["ltr", "rtl"] as const) {
|
||||
await expect(content).toHaveAttribute("data-summary-motion", "transitionrun,transitionend,")
|
||||
await expect(content).not.toHaveCSS("translate", shifted)
|
||||
await page.setViewportSize({ width: 1440, height: 900 })
|
||||
await expect(panel).toHaveAttribute("data-summary-resizing", "false")
|
||||
await expect(content).toHaveCSS("translate", shifted)
|
||||
|
||||
await content.evaluate((element) => element.setAttribute("data-summary-motion", ""))
|
||||
@@ -113,8 +112,7 @@ for (const direction of ["ltr", "rtl"] as const) {
|
||||
await expect(summary).toBeVisible()
|
||||
|
||||
await page.setViewportSize({ width: 1800, height: 900 })
|
||||
await expect(panel).toHaveAttribute("data-summary-resizing", "false")
|
||||
await expect(content).toHaveCSS("translate", "none")
|
||||
await expect(content).toHaveCSS("translate", "0px")
|
||||
await expect(summary).toBeVisible()
|
||||
|
||||
await page.emulateMedia({ reducedMotion: "reduce" })
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode/app",
|
||||
"version": "2.0.11",
|
||||
"version": "2.0.10",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
@@ -65,14 +65,14 @@
|
||||
}
|
||||
|
||||
/* Keep the 1000px conversation intact, with 320px for the summary and its gutters.
|
||||
When they overlap, center the conversation in the space that remains beside the summary. */
|
||||
@container session-chat (min-width: 1320px) and (width < 1640px) {
|
||||
Move only as far as needed; wider panels already have enough space in the margin. */
|
||||
@container session-chat (min-width: 1320px) {
|
||||
[data-slot="session-chat-panel"][data-summary-open="true"]
|
||||
:is([data-timeline-virtual-content], [data-component="session-composer-dock"] > div) {
|
||||
--session-summary-translate: -160px;
|
||||
--session-summary-translate: min(0px, calc(50cqi - 820px));
|
||||
|
||||
&:dir(rtl) {
|
||||
--session-summary-translate: 160px;
|
||||
--session-summary-translate: max(0px, calc(820px - 50cqi));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ const Current = Persistence.struct({ session: SessionsSchema })
|
||||
|
||||
export const ModelSelectionSchema = Persistence.migrate(
|
||||
Current,
|
||||
Persistence.legacy({
|
||||
Schema.Struct({
|
||||
session: Persistence.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
||||
pick: Persistence.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
||||
}).pipe(
|
||||
|
||||
@@ -906,10 +906,6 @@ export const dict = {
|
||||
"session.browser.replaced": "Browser control moved to another desktop window.",
|
||||
"session.browser.suspended": "Browser suspended. Interact with this session to reconnect.",
|
||||
"session.browser.address.placeholder": "Enter URL",
|
||||
"session.browser.empty.title": "Enter URL",
|
||||
"session.browser.empty.description": 'Or prompt "Open in the app browser"',
|
||||
"session.browser.failed.title": "URL can't be reached",
|
||||
"session.browser.failed.description": "Check the URL and your connection, then try again.",
|
||||
|
||||
"titlebar.update": "Update",
|
||||
"titlebar.tabs": "Tabs",
|
||||
|
||||
@@ -32,7 +32,7 @@ describe("persistence schemas", () => {
|
||||
|
||||
test("legacy migration observes missing fields before initial defaults are applied", () => {
|
||||
const current = Persistence.struct({ mode: Schema.Literals(["compact", "full"]), enabled: Schema.Boolean })
|
||||
const stored = Persistence.legacy({
|
||||
const stored = Schema.Struct({
|
||||
mode: Schema.optional(Schema.Unknown),
|
||||
expanded: Schema.optional(Schema.Boolean),
|
||||
}).pipe(
|
||||
@@ -53,38 +53,6 @@ describe("persistence schemas", () => {
|
||||
expect(Schema.encodeSync(schema)(decode({ expanded: true }))).toEqual({ mode: "full", enabled: true })
|
||||
})
|
||||
|
||||
test("legacy read shapes carry unknown nested fields through to the current schema", () => {
|
||||
const current = Persistence.struct({
|
||||
general: Persistence.struct({
|
||||
mode: Schema.Literals(["compact", "full"]),
|
||||
theme: Schema.String,
|
||||
nested: Persistence.struct({ keep: Schema.Boolean }),
|
||||
}),
|
||||
other: Persistence.struct({ flag: Schema.Boolean }),
|
||||
})
|
||||
const stored = Persistence.legacy({
|
||||
general: Persistence.optional(Persistence.legacy({ expanded: Persistence.optional(Schema.Boolean) })),
|
||||
}).pipe(
|
||||
Schema.decode({
|
||||
decode: SchemaGetter.transform((value) => {
|
||||
const general = value.general
|
||||
if (!general || general.expanded === undefined) return value
|
||||
return { ...value, general: { ...general, mode: general.expanded ? "full" : "compact" } }
|
||||
}),
|
||||
encode: SchemaGetter.passthrough(),
|
||||
}),
|
||||
)
|
||||
const schema = Persistence.withInitial(Persistence.migrate(current, stored), {
|
||||
general: { mode: "compact", theme: "light", nested: { keep: false } },
|
||||
other: { flag: false },
|
||||
})
|
||||
const decode = Schema.decodeUnknownSync(schema)
|
||||
expect(decode({ general: { expanded: true, theme: "dark", nested: { keep: true } }, other: { flag: true } })).toEqual({
|
||||
general: { mode: "full", theme: "dark", nested: { keep: true } },
|
||||
other: { flag: true },
|
||||
})
|
||||
})
|
||||
|
||||
test("initial merging preserves field codecs and replaces arrays rather than merging indexes", () => {
|
||||
const current = Persistence.struct({
|
||||
amount: Schema.NumberFromString.check(Schema.isFinite()),
|
||||
|
||||
@@ -23,11 +23,13 @@ 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) : Result.succeed<unknown>
|
||||
const read = isMigrated(definition)
|
||||
? SchemaParser.decodeUnknownResult(definition.read, { onExcessProperty: "preserve" })
|
||||
: Result.succeed<unknown>
|
||||
const encode = Schema.encodeUnknownSync(schema)
|
||||
return Schema.Unknown.pipe(
|
||||
Schema.decode<Schema.Unknown>({
|
||||
decode: SchemaGetter.transformEffect((value) =>
|
||||
decode: SchemaGetter.transformOrFail((value) =>
|
||||
Effect.fromResult(Result.map(read(value), (stored) => merge(initial, recover(schema.ast, stored, initial)))),
|
||||
),
|
||||
encode: SchemaGetter.transform((value) => encode(value)),
|
||||
@@ -36,12 +38,6 @@ export function withInitial<S extends Schema.ConstraintCodec<object, unknown>>(
|
||||
)
|
||||
}
|
||||
|
||||
// A legacy read shape declares only the fields it migrates. Decoding strips unknown keys, so every
|
||||
// struct level in a `migrate` read schema must stay open for the current fields it does not name.
|
||||
export function legacy<const Fields extends Schema.Struct.Fields>(fields: Fields) {
|
||||
return Schema.StructWithRest(Schema.Struct(fields), [Schema.Record(Schema.String, Schema.Unknown)])
|
||||
}
|
||||
|
||||
// Object-level codecs own their recovery. Plain structs can recover fields independently.
|
||||
function recover(ast: SchemaAST.AST, value: unknown, initial: unknown): unknown {
|
||||
if (value === undefined) return initial
|
||||
|
||||
@@ -53,7 +53,7 @@ const State = Persistence.struct({
|
||||
export function serverState(canonicalLocalServer: () => string | undefined = () => undefined) {
|
||||
return Persistence.migrate(
|
||||
State,
|
||||
Persistence.legacy({ projects: Projects, lastProject: LastProject }).pipe(
|
||||
Schema.Struct({ projects: Projects, lastProject: LastProject }).pipe(
|
||||
Schema.decode({
|
||||
decode: SchemaGetter.transform((value) => {
|
||||
const canonical = canonicalLocalServer()
|
||||
|
||||
@@ -20,18 +20,13 @@ export function SessionBrowserPane(props: { browser: ReturnType<typeof createSes
|
||||
const command = useCommand()
|
||||
const state = props.browser.active
|
||||
const address = () => (state()?.url === "about:blank" ? "" : (state()?.url ?? ""))
|
||||
const failed = () => !!state()?.loadError
|
||||
const registration = props.browser.registration
|
||||
const button = { variant: "ghost", size: "large" } as const
|
||||
const [store, setStore] = createStore({
|
||||
address: "",
|
||||
editing: false,
|
||||
submitted: false,
|
||||
// A submitted navigation the browser has not reported yet; keeps the empty state hidden meanwhile.
|
||||
navigating: false,
|
||||
visible: typeof document === "undefined" || document.visibilityState === "visible",
|
||||
})
|
||||
const empty = () => !address() && !state()?.loading && !store.navigating
|
||||
let surface: HTMLDivElement | undefined
|
||||
let addressDisplay: HTMLDivElement | undefined
|
||||
let frame: number | undefined
|
||||
@@ -48,7 +43,7 @@ export function SessionBrowserPane(props: { browser: ReturnType<typeof createSes
|
||||
title: language.t("command.browser.reload"),
|
||||
category: language.t("command.category.view"),
|
||||
keybind: "f5",
|
||||
disabled: !props.visible || !address(),
|
||||
disabled: !props.visible || !state(),
|
||||
onSelect: () => {
|
||||
const tab = state()
|
||||
if (tab) props.browser.command({ type: "reload", tabID: tab.id })
|
||||
@@ -76,9 +71,7 @@ export function SessionBrowserPane(props: { browser: ReturnType<typeof createSes
|
||||
const top = Math.round(rect.top * zoom)
|
||||
const right = Math.round(rect.right * zoom)
|
||||
const bottom = Math.round(rect.bottom * zoom)
|
||||
// The desktop page hides blank and loading documents itself; only hide here
|
||||
// while the pane shows its own empty or failed state over the surface.
|
||||
const visible = props.visible && store.visible && !empty() && !failed() && !dialog.active && !covered(rect)
|
||||
const visible = props.visible && store.visible && !dialog.active && !covered(rect)
|
||||
// The cutout exposes the app backdrop outside the rounded Review card,
|
||||
// not the browser surface inside it.
|
||||
const color = getComputedStyle(
|
||||
@@ -114,25 +107,7 @@ export function SessionBrowserPane(props: { browser: ReturnType<typeof createSes
|
||||
if (frame === undefined) frame = requestAnimationFrame(tick)
|
||||
}
|
||||
|
||||
createEffect(on([() => state()?.id, address], () => !store.editing && setStore("address", address())))
|
||||
// Any reported movement, including a rejected or blocked request, ends the submitted navigation.
|
||||
createEffect(
|
||||
on(
|
||||
[() => state()?.id, () => state()?.generation, () => state()?.loading, () => props.browser.error()],
|
||||
() => setStore("navigating", false),
|
||||
{ defer: true },
|
||||
),
|
||||
)
|
||||
// A blocked or rejected submission leaves the page where it was; show that page's URL again.
|
||||
createEffect(
|
||||
on(
|
||||
() => props.browser.error(),
|
||||
(error) => {
|
||||
if (error && !store.editing) setStore("address", address())
|
||||
},
|
||||
{ defer: true },
|
||||
),
|
||||
)
|
||||
createEffect(() => !store.editing && setStore("address", address()))
|
||||
createEffect(
|
||||
on(registration, (current) => {
|
||||
// Session routes can change before this pane unmounts. Hide the registration
|
||||
@@ -148,8 +123,6 @@ export function SessionBrowserPane(props: { browser: ReturnType<typeof createSes
|
||||
() => store.visible,
|
||||
() => props.visible,
|
||||
() => state()?.id,
|
||||
empty,
|
||||
failed,
|
||||
registration,
|
||||
],
|
||||
() => {
|
||||
@@ -184,7 +157,7 @@ export function SessionBrowserPane(props: { browser: ReturnType<typeof createSes
|
||||
|
||||
return (
|
||||
<aside id="browser-panel" class="relative size-full min-w-0 overflow-hidden bg-v2-background-bg-base flex flex-col">
|
||||
<div class="h-10 shrink-0 flex items-center gap-1 px-3 border-b border-v2-border-border-muted">
|
||||
<div class="h-10 shrink-0 flex items-center gap-1 px-2 border-b border-v2-border-border-muted">
|
||||
<For each={["back", "forward"] as const}>
|
||||
{(direction) => (
|
||||
<Tooltip placement="top" value={language.t(direction === "back" ? "common.goBack" : "common.goForward")}>
|
||||
@@ -220,7 +193,7 @@ export function SessionBrowserPane(props: { browser: ReturnType<typeof createSes
|
||||
>
|
||||
<IconButton
|
||||
{...button}
|
||||
disabled={!state()?.loading && !address()}
|
||||
disabled={!state()}
|
||||
aria-label={language.t(state()?.loading ? "prompt.action.stop" : "error.page.action.reload")}
|
||||
onClick={() => {
|
||||
const tab = state()
|
||||
@@ -239,13 +212,8 @@ export function SessionBrowserPane(props: { browser: ReturnType<typeof createSes
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault()
|
||||
const tab = state()
|
||||
const url = store.address.trim()
|
||||
if (!tab) return
|
||||
if (url || failed()) {
|
||||
setStore({ submitted: true, address: url, navigating: true })
|
||||
props.browser.command({ type: "navigate", tabID: tab.id, url: url || "about:blank" })
|
||||
}
|
||||
event.currentTarget.querySelector("input")?.blur()
|
||||
if (tab && store.address.trim())
|
||||
props.browser.command({ type: "navigate", tabID: tab.id, url: store.address })
|
||||
}}
|
||||
>
|
||||
<input
|
||||
@@ -256,14 +224,8 @@ export function SessionBrowserPane(props: { browser: ReturnType<typeof createSes
|
||||
disabled={!state()}
|
||||
placeholder={language.t("session.browser.address.placeholder")}
|
||||
aria-label={language.t("session.browser.address")}
|
||||
onFocus={(event) => {
|
||||
setStore("editing", true)
|
||||
event.currentTarget.select()
|
||||
}}
|
||||
onClick={(event) => event.currentTarget.select()}
|
||||
onBlur={() =>
|
||||
setStore({ editing: false, address: store.submitted ? store.address : address(), submitted: false })
|
||||
}
|
||||
onFocus={() => setStore("editing", true)}
|
||||
onBlur={() => setStore({ editing: false, address: address() })}
|
||||
onInput={(event) => setStore("address", event.currentTarget.value)}
|
||||
onScroll={(event) => {
|
||||
if (addressDisplay) addressDisplay.scrollLeft = event.currentTarget.scrollLeft
|
||||
@@ -281,27 +243,12 @@ export function SessionBrowserPane(props: { browser: ReturnType<typeof createSes
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<Show when={props.browser.error() && !failed()}>
|
||||
<Show when={props.browser.error()}>
|
||||
<div class="shrink-0 px-3 py-1.5 text-12-regular text-text-danger-base border-b border-v2-border-border-muted">
|
||||
{props.browser.error()}
|
||||
</div>
|
||||
</Show>
|
||||
<div ref={surface} class="min-h-0 flex-1 bg-v2-background-bg-base flex items-center justify-center">
|
||||
<Show when={(empty() || failed()) && !props.browser.suspended()}>
|
||||
{/* Add the 40px toolbar to the file empty state's 160px bottom padding to align their centers. */}
|
||||
<div
|
||||
dir="auto"
|
||||
class="flex size-full flex-col items-center justify-center gap-2 p-6 pb-[200px] text-center text-text-weak"
|
||||
>
|
||||
<Icon name="globe" size="large" class="mb-2 shrink-0" />
|
||||
<div class="text-[13px] font-medium leading-[var(--line-height-compact)] text-text-strong">
|
||||
{language.t(failed() ? "session.browser.failed.title" : "session.browser.empty.title")}
|
||||
</div>
|
||||
<div class="text-13-regular leading-[var(--line-height-base)]">
|
||||
{language.t(failed() ? "session.browser.failed.description" : "session.browser.empty.description")}
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={props.browser.suspended()}>
|
||||
<p class="px-6 text-center text-13-regular text-v2-text-text-subtle" role="status">
|
||||
{language.t("session.browser.suspended")}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { children, createMemo, Show } from "solid-js"
|
||||
import type { JSX } from "solid-js"
|
||||
import { useSortable } from "@dnd-kit/solid/sortable"
|
||||
import { Icon } from "@opencode/ui/icon"
|
||||
import { IconButton } from "@opencode/ui/icon-button"
|
||||
import { Keybind } from "@opencode/ui/keybind"
|
||||
import { Tooltip } from "@opencode/ui/tooltip"
|
||||
import { Tabs } from "@opencode/ui/tabs"
|
||||
@@ -71,19 +69,9 @@ export function SortableTab(props: {
|
||||
placement="bottom"
|
||||
gutter={10}
|
||||
>
|
||||
<IconButton
|
||||
size="small"
|
||||
variant="ghost-muted"
|
||||
onPointerDown={(event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}}
|
||||
onClick={(event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
props.onTabClose(props.tab)
|
||||
}}
|
||||
icon={<Icon name="xmark-small" />}
|
||||
<Tabs.CloseButton
|
||||
class="h-5 w-5"
|
||||
onClick={() => props.onTabClose(props.tab)}
|
||||
aria-label={language.t("common.closeTab")}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
@@ -188,9 +188,9 @@ function legacyTimelineActivity(value: boolean | "hidden" | "compact" | "full" |
|
||||
|
||||
export const settingsPersistence = Persistence.migrate(
|
||||
settingsSchema,
|
||||
Persistence.legacy({
|
||||
Schema.Struct({
|
||||
general: Persistence.optional(
|
||||
Persistence.legacy({
|
||||
Schema.Struct({
|
||||
// Keep invalid explicit values distinct from absent values so legacy preferences cannot replace them.
|
||||
timelineDetail: Schema.optional(
|
||||
Schema.NullOr(
|
||||
|
||||
@@ -173,16 +173,16 @@ export const layoutSchema = Persistence.struct({
|
||||
|
||||
export const layoutPersistence = Persistence.migrate(
|
||||
layoutSchema,
|
||||
Persistence.legacy({
|
||||
Schema.Struct({
|
||||
sidebar: Persistence.optional(
|
||||
Persistence.legacy({
|
||||
Schema.Struct({
|
||||
workspaces: Persistence.optional(Schema.Union([Schema.Boolean, Schema.Record(Schema.String, Schema.Boolean)])),
|
||||
workspacesDefault: Persistence.optional(Schema.Boolean),
|
||||
}),
|
||||
),
|
||||
review: Persistence.optional(Persistence.legacy({ panelOpened: Persistence.optional(Schema.Boolean) })),
|
||||
review: Persistence.optional(Schema.Struct({ panelOpened: Persistence.optional(Schema.Boolean) })),
|
||||
fileTree: Persistence.optional(
|
||||
Persistence.legacy({
|
||||
Schema.Struct({
|
||||
opened: Persistence.optional(Schema.Boolean),
|
||||
width: Persistence.optional(Schema.Finite),
|
||||
tab: Persistence.optional(Schema.Literals(["changes", "all"])),
|
||||
|
||||
@@ -9,8 +9,7 @@
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
[data-titlebar-tab],
|
||||
[data-titlebar-tab-action] {
|
||||
[data-titlebar-tab] {
|
||||
--tab-base: var(--v2-background-bg-deep);
|
||||
--tab-overlay: transparent;
|
||||
background: linear-gradient(var(--tab-overlay), var(--tab-overlay)), var(--tab-base);
|
||||
@@ -69,14 +68,6 @@
|
||||
--tab-overlay: var(--v2-overlay-simple-overlay-pressed);
|
||||
}
|
||||
|
||||
[data-titlebar-tab-action]:is(:hover, :focus-visible) {
|
||||
--tab-overlay: var(--v2-overlay-simple-overlay-hover);
|
||||
}
|
||||
|
||||
[data-titlebar-tab-action]:is(:active, [data-state="pressed"]) {
|
||||
--tab-overlay: var(--v2-overlay-simple-overlay-pressed);
|
||||
}
|
||||
|
||||
[data-titlebar-tab]:is(:hover, [data-state="pressed"]) [data-slot="tab-close"] {
|
||||
background: linear-gradient(var(--tab-overlay), var(--tab-overlay)), var(--tab-base);
|
||||
}
|
||||
|
||||
@@ -351,10 +351,9 @@ export function Titlebar(props: {
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
data-titlebar-tab-action
|
||||
data-action="vertical-tabs-home"
|
||||
data-state={layout.route().type === "home" ? "pressed" : undefined}
|
||||
class="group mb-1 flex h-7 w-full shrink-0 items-center gap-1.5 rounded-[6px] ps-1.5 pe-2 text-[13px] leading-4 text-v2-text-text-faint hover:text-v2-text-text-base data-[state=pressed]:text-v2-text-text-base"
|
||||
class="group mb-1 flex h-7 w-full shrink-0 items-center gap-1.5 rounded-[6px] ps-1.5 pe-2 text-[13px] leading-4 text-v2-text-text-faint hover:bg-v2-background-bg-layer-02 hover:text-v2-text-text-base data-[state=pressed]:bg-v2-background-bg-layer-02 data-[state=pressed]:text-v2-text-text-base"
|
||||
onClick={toggleHome}
|
||||
aria-label={language.t("home.title")}
|
||||
aria-pressed={layout.route().type === "home"}
|
||||
@@ -651,9 +650,8 @@ export function Titlebar(props: {
|
||||
{homeButton(true)}
|
||||
<button
|
||||
type="button"
|
||||
data-titlebar-tab-action
|
||||
data-action="vertical-tabs-new-session"
|
||||
class="group flex h-7 w-full shrink-0 items-center gap-1.5 rounded-[6px] ps-1.5 pe-2 text-[13px] leading-4 text-v2-text-text-faint hover:text-v2-text-text-base"
|
||||
class="group flex h-7 w-full shrink-0 items-center gap-1.5 rounded-[6px] ps-1.5 pe-2 text-[13px] leading-4 text-v2-text-text-faint hover:bg-v2-background-bg-layer-02 hover:text-v2-text-text-base"
|
||||
onClick={openNewTab}
|
||||
aria-label={language.t("command.session.new")}
|
||||
>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/cli",
|
||||
"version": "2.0.11",
|
||||
"version": "2.0.10",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
|
||||
@@ -3,8 +3,8 @@ import { Schema } from "effect"
|
||||
import { Spec } from "../framework/spec"
|
||||
import { Updater } from "../services/updater"
|
||||
|
||||
export const PrintLogs = GlobalFlag.Setting("print-logs")({
|
||||
flag: Flag.Boolean("print-logs").pipe(
|
||||
export const PrintLogs = GlobalFlag.setting("print-logs")({
|
||||
flag: Flag.boolean("print-logs").pipe(
|
||||
Flag.withDescription("Print logs to stderr (server logs require --standalone)"),
|
||||
Flag.withDefault(false),
|
||||
),
|
||||
@@ -13,23 +13,23 @@ export const PrintLogs = GlobalFlag.Setting("print-logs")({
|
||||
declare const OPENCODE_CLI_NAME: string | undefined
|
||||
|
||||
const ServerParams = {
|
||||
standalone: Flag.Boolean("standalone").pipe(
|
||||
standalone: Flag.boolean("standalone").pipe(
|
||||
Flag.withDescription("Run with a private server instead of the background service"),
|
||||
Flag.withDefault(false),
|
||||
),
|
||||
server: Flag.String("server").pipe(
|
||||
server: Flag.string("server").pipe(
|
||||
Flag.withDescription("Connect to a server URL instead of the background service"),
|
||||
Flag.optional,
|
||||
),
|
||||
}
|
||||
|
||||
const PermissionParams = {
|
||||
auto: Flag.Boolean("auto").pipe(
|
||||
auto: Flag.boolean("auto").pipe(
|
||||
Flag.withDescription("Auto-approve permissions that are not explicitly denied"),
|
||||
Flag.withDefault(false),
|
||||
),
|
||||
yolo: Flag.Boolean("yolo").pipe(Flag.withDefault(false), Flag.withHidden),
|
||||
dangerouslySkipPermissions: Flag.Boolean("dangerously-skip-permissions").pipe(
|
||||
yolo: Flag.boolean("yolo").pipe(Flag.withDefault(false), Flag.withHidden),
|
||||
dangerouslySkipPermissions: Flag.boolean("dangerously-skip-permissions").pipe(
|
||||
Flag.withDefault(false),
|
||||
Flag.withHidden,
|
||||
),
|
||||
@@ -40,32 +40,32 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME
|
||||
params: {
|
||||
...ServerParams,
|
||||
...PermissionParams,
|
||||
directory: Argument.String("directory").pipe(
|
||||
directory: Argument.string("directory").pipe(
|
||||
Argument.withDescription("Directory to start OpenCode in"),
|
||||
Argument.optional,
|
||||
),
|
||||
continue: Flag.Boolean("continue").pipe(
|
||||
continue: Flag.boolean("continue").pipe(
|
||||
Flag.withAlias("c"),
|
||||
Flag.withDescription("Continue the last session"),
|
||||
Flag.withDefault(false),
|
||||
),
|
||||
session: Flag.String("session").pipe(
|
||||
session: Flag.string("session").pipe(
|
||||
Flag.withAlias("s"),
|
||||
Flag.withDescription("Session ID to continue"),
|
||||
Flag.optional,
|
||||
),
|
||||
prompt: Flag.String("prompt").pipe(Flag.withDescription("Prompt to use"), Flag.optional),
|
||||
prompt: Flag.string("prompt").pipe(Flag.withDescription("Prompt to use"), Flag.optional),
|
||||
},
|
||||
commands: [
|
||||
Spec.make("upgrade", {
|
||||
description: "Upgrade OpenCode to the latest or a specific version",
|
||||
aliases: ["update"],
|
||||
params: {
|
||||
target: Argument.String("target").pipe(
|
||||
target: Argument.string("target").pipe(
|
||||
Argument.withDescription("Version to upgrade to (with or without a leading v)"),
|
||||
Argument.optional,
|
||||
),
|
||||
method: Flag.Literals("method", Updater.methods).pipe(
|
||||
method: Flag.choice("method", Updater.methods).pipe(
|
||||
Flag.withAlias("m"),
|
||||
Flag.withDescription("Installation method to use"),
|
||||
Flag.optional,
|
||||
@@ -75,21 +75,21 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME
|
||||
Spec.make("uninstall", {
|
||||
description: "Uninstall OpenCode and remove all related files",
|
||||
params: {
|
||||
keepConfig: Flag.Boolean("keep-config").pipe(
|
||||
keepConfig: Flag.boolean("keep-config").pipe(
|
||||
Flag.withAlias("c"),
|
||||
Flag.withDescription("Keep configuration files"),
|
||||
Flag.withDefault(false),
|
||||
),
|
||||
keepData: Flag.Boolean("keep-data").pipe(
|
||||
keepData: Flag.boolean("keep-data").pipe(
|
||||
Flag.withAlias("d"),
|
||||
Flag.withDescription("Keep session data and snapshots"),
|
||||
Flag.withDefault(false),
|
||||
),
|
||||
dryRun: Flag.Boolean("dry-run").pipe(
|
||||
dryRun: Flag.boolean("dry-run").pipe(
|
||||
Flag.withDescription("Show what would be removed without removing"),
|
||||
Flag.withDefault(false),
|
||||
),
|
||||
force: Flag.Boolean("force").pipe(
|
||||
force: Flag.boolean("force").pipe(
|
||||
Flag.withAlias("f"),
|
||||
Flag.withDescription("Skip confirmation prompts"),
|
||||
Flag.withDefault(false),
|
||||
@@ -101,17 +101,17 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME
|
||||
description: "Make a request to the running server",
|
||||
params: {
|
||||
...ServerParams,
|
||||
request: Argument.String("operation | method path").pipe(
|
||||
request: Argument.string("operation | method path").pipe(
|
||||
Argument.withDescription("OpenAPI operation ID, or an HTTP method followed by a path"),
|
||||
Argument.variadic({ min: 1, max: 2 }),
|
||||
),
|
||||
data: Flag.String("data").pipe(Flag.withAlias("d"), Flag.withDescription("Request body"), Flag.optional),
|
||||
header: Flag.String("header").pipe(
|
||||
data: Flag.string("data").pipe(Flag.withAlias("d"), Flag.withDescription("Request body"), Flag.optional),
|
||||
header: Flag.string("header").pipe(
|
||||
Flag.withAlias("H"),
|
||||
Flag.withDescription("Request header in name:value form"),
|
||||
Flag.atMost(100),
|
||||
),
|
||||
param: Flag.KeyValuePair("param").pipe(Flag.withDescription("OpenAPI path or query parameter"), Flag.optional),
|
||||
param: Flag.keyValuePair("param").pipe(Flag.withDescription("OpenAPI path or query parameter"), Flag.optional),
|
||||
},
|
||||
}),
|
||||
Spec.make("debug", {
|
||||
@@ -122,7 +122,7 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME
|
||||
Spec.make("paths", {
|
||||
description: "Show global paths (data, config, cache, state)",
|
||||
params: {
|
||||
name: Argument.Literals("name", [
|
||||
name: Argument.choice("name", [
|
||||
"db",
|
||||
"home",
|
||||
"data",
|
||||
@@ -134,9 +134,7 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME
|
||||
"log",
|
||||
"repos",
|
||||
]).pipe(
|
||||
Argument.withDescription(
|
||||
"Print only one path: db, home, data, config, cache, state, tmp, bin, log, repos",
|
||||
),
|
||||
Argument.withDescription("Print only one path: db, home, data, config, cache, state, tmp, bin, log, repos"),
|
||||
Argument.optional,
|
||||
),
|
||||
},
|
||||
@@ -150,7 +148,7 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME
|
||||
description: "list providers and credentials",
|
||||
params: {
|
||||
...ServerParams,
|
||||
format: Flag.Literals("format", ["default", "json"]).pipe(
|
||||
format: Flag.choice("format", ["default", "json"]).pipe(
|
||||
Flag.withDescription("Output format"),
|
||||
Flag.withDefault("default"),
|
||||
),
|
||||
@@ -160,12 +158,12 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME
|
||||
description: "log in to a provider",
|
||||
params: {
|
||||
...ServerParams,
|
||||
target: Argument.String("target").pipe(
|
||||
target: Argument.string("target").pipe(
|
||||
Argument.withDescription("Integration ID, name, or well-known provider URL"),
|
||||
Argument.optional,
|
||||
),
|
||||
method: Flag.String("method").pipe(Flag.withDescription("Authentication method ID"), Flag.optional),
|
||||
answer: Flag.String("answer").pipe(
|
||||
method: Flag.string("method").pipe(Flag.withDescription("Authentication method ID"), Flag.optional),
|
||||
answer: Flag.string("answer").pipe(
|
||||
Flag.withDescription("Provider form answer (key=value; repeat for multiple fields)"),
|
||||
Flag.atMost(100),
|
||||
),
|
||||
@@ -175,11 +173,11 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME
|
||||
description: "log out of a saved account",
|
||||
params: {
|
||||
...ServerParams,
|
||||
target: Argument.String("target").pipe(
|
||||
target: Argument.string("target").pipe(
|
||||
Argument.withDescription("Integration ID or name"),
|
||||
Argument.optional,
|
||||
),
|
||||
credential: Argument.String("credential").pipe(
|
||||
credential: Argument.string("credential").pipe(
|
||||
Argument.withDescription("Credential ID or label (opens an account picker when omitted)"),
|
||||
Argument.optional,
|
||||
),
|
||||
@@ -189,11 +187,11 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME
|
||||
description: "switch the active account for an integration",
|
||||
params: {
|
||||
...ServerParams,
|
||||
target: Argument.String("target").pipe(
|
||||
target: Argument.string("target").pipe(
|
||||
Argument.withDescription("Integration ID or name"),
|
||||
Argument.optional,
|
||||
),
|
||||
credential: Argument.String("credential").pipe(
|
||||
credential: Argument.string("credential").pipe(
|
||||
Argument.withDescription("Credential ID or label (opens an account picker when omitted)"),
|
||||
Argument.optional,
|
||||
),
|
||||
@@ -208,21 +206,21 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME
|
||||
Spec.make("add", {
|
||||
description: "Add an MCP server to your configuration",
|
||||
params: {
|
||||
name: Argument.String("name").pipe(Argument.withDescription("Name of the MCP server")),
|
||||
command: Argument.String("command").pipe(
|
||||
name: Argument.string("name").pipe(Argument.withDescription("Name of the MCP server")),
|
||||
command: Argument.string("command").pipe(
|
||||
Argument.withDescription("Command and arguments for a local server, passed after --"),
|
||||
Argument.variadic({ min: 0 }),
|
||||
),
|
||||
url: Flag.String("url").pipe(Flag.withDescription("URL for a remote MCP server"), Flag.optional),
|
||||
header: Flag.KeyValuePair("header").pipe(
|
||||
url: Flag.string("url").pipe(Flag.withDescription("URL for a remote MCP server"), Flag.optional),
|
||||
header: Flag.keyValuePair("header").pipe(
|
||||
Flag.withDescription("HTTP header for a remote server, as name=value"),
|
||||
Flag.optional,
|
||||
),
|
||||
env: Flag.KeyValuePair("env").pipe(
|
||||
env: Flag.keyValuePair("env").pipe(
|
||||
Flag.withDescription("Environment variable for a local server, as name=value"),
|
||||
Flag.optional,
|
||||
),
|
||||
global: Flag.Boolean("global").pipe(
|
||||
global: Flag.boolean("global").pipe(
|
||||
Flag.withDescription("Write to the global config instead of the project config"),
|
||||
Flag.withDefault(false),
|
||||
),
|
||||
@@ -230,11 +228,11 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME
|
||||
}),
|
||||
Spec.make("auth", {
|
||||
description: "Authenticate with an OAuth-capable remote MCP server",
|
||||
params: { name: Argument.String("name").pipe(Argument.withDescription("Name of the MCP server")) },
|
||||
params: { name: Argument.string("name").pipe(Argument.withDescription("Name of the MCP server")) },
|
||||
}),
|
||||
Spec.make("logout", {
|
||||
description: "Remove stored OAuth credentials for an MCP server",
|
||||
params: { name: Argument.String("name").pipe(Argument.withDescription("Name of the MCP server")) },
|
||||
params: { name: Argument.string("name").pipe(Argument.withDescription("Name of the MCP server")) },
|
||||
}),
|
||||
],
|
||||
}),
|
||||
@@ -244,7 +242,7 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME
|
||||
Spec.make("list", {
|
||||
description: "List plugins",
|
||||
params: {
|
||||
builtin: Flag.Boolean("builtin").pipe(
|
||||
builtin: Flag.boolean("builtin").pipe(
|
||||
Flag.withDescription("Include built-in server plugins"),
|
||||
Flag.withDefault(false),
|
||||
),
|
||||
@@ -253,13 +251,13 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME
|
||||
Spec.make("add", {
|
||||
description: "Install a plugin and add it to the global configuration",
|
||||
params: {
|
||||
package: Argument.String("package").pipe(Argument.withDescription("npm registry or Git package specifier")),
|
||||
package: Argument.string("package").pipe(Argument.withDescription("npm registry or Git package specifier")),
|
||||
},
|
||||
}),
|
||||
Spec.make("check", {
|
||||
description: "Check package plugins for updates",
|
||||
params: {
|
||||
target: Argument.String("target").pipe(
|
||||
target: Argument.string("target").pipe(
|
||||
Argument.withDescription("Configured package target"),
|
||||
Argument.optional,
|
||||
),
|
||||
@@ -268,7 +266,7 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME
|
||||
Spec.make("update", {
|
||||
description: "Update package plugins",
|
||||
params: {
|
||||
target: Argument.String("target").pipe(
|
||||
target: Argument.string("target").pipe(
|
||||
Argument.withDescription("Configured package target; omit to update all outdated plugins"),
|
||||
Argument.optional,
|
||||
),
|
||||
@@ -277,7 +275,7 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME
|
||||
Spec.make("remove", {
|
||||
description: "Remove a plugin from global configuration",
|
||||
params: {
|
||||
package: Argument.String("package").pipe(Argument.withDescription("configured package specifier")),
|
||||
package: Argument.string("package").pipe(Argument.withDescription("configured package specifier")),
|
||||
},
|
||||
}),
|
||||
],
|
||||
@@ -290,108 +288,108 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME
|
||||
description: "Show shareable usage statistics",
|
||||
params: {
|
||||
...ServerParams,
|
||||
days: Flag.Int("days").pipe(
|
||||
days: Flag.integer("days").pipe(
|
||||
Flag.withSchema(Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))),
|
||||
Flag.withDescription("Show the last N days; 0 means today"),
|
||||
Flag.optional,
|
||||
),
|
||||
year: Flag.Int("year").pipe(
|
||||
year: Flag.integer("year").pipe(
|
||||
Flag.withSchema(Schema.Int.check(Schema.isBetween({ minimum: 1970, maximum: 9_999 }))),
|
||||
Flag.withDescription("Show a calendar year"),
|
||||
Flag.optional,
|
||||
),
|
||||
all: Flag.Boolean("all").pipe(Flag.withDescription("Show lifetime statistics"), Flag.withDefault(false)),
|
||||
project: Flag.String("project").pipe(
|
||||
all: Flag.boolean("all").pipe(Flag.withDescription("Show lifetime statistics"), Flag.withDefault(false)),
|
||||
project: Flag.string("project").pipe(
|
||||
Flag.withDescription('Filter by project ID, or use "." for the current project'),
|
||||
Flag.optional,
|
||||
),
|
||||
models: Flag.Boolean("models").pipe(Flag.withDescription("Show model usage"), Flag.withDefault(false)),
|
||||
tools: Flag.Boolean("tools").pipe(Flag.withDescription("Show tool reliability"), Flag.withDefault(false)),
|
||||
cost: Flag.Boolean("cost").pipe(Flag.withDescription("Show cost and token details"), Flag.withDefault(false)),
|
||||
full: Flag.Boolean("full").pipe(Flag.withDescription("Show every detailed section"), Flag.withDefault(false)),
|
||||
limit: Flag.Int("limit").pipe(
|
||||
models: Flag.boolean("models").pipe(Flag.withDescription("Show model usage"), Flag.withDefault(false)),
|
||||
tools: Flag.boolean("tools").pipe(Flag.withDescription("Show tool reliability"), Flag.withDefault(false)),
|
||||
cost: Flag.boolean("cost").pipe(Flag.withDescription("Show cost and token details"), Flag.withDefault(false)),
|
||||
full: Flag.boolean("full").pipe(Flag.withDescription("Show every detailed section"), Flag.withDefault(false)),
|
||||
limit: Flag.integer("limit").pipe(
|
||||
Flag.withSchema(Schema.Int.check(Schema.isGreaterThanOrEqualTo(1))),
|
||||
Flag.withDescription("Number of rows in detailed sections"),
|
||||
Flag.withDefault(5),
|
||||
),
|
||||
json: Flag.Boolean("json").pipe(Flag.withDescription("Output statistics as JSON"), Flag.withDefault(false)),
|
||||
json: Flag.boolean("json").pipe(Flag.withDescription("Output statistics as JSON"), Flag.withDefault(false)),
|
||||
},
|
||||
}),
|
||||
Spec.make("mini", {
|
||||
description: "Start the minimal interactive interface",
|
||||
params: {
|
||||
...ServerParams,
|
||||
continue: Flag.Boolean("continue").pipe(
|
||||
continue: Flag.boolean("continue").pipe(
|
||||
Flag.withAlias("c"),
|
||||
Flag.withDescription("Continue the last session"),
|
||||
Flag.withDefault(false),
|
||||
),
|
||||
session: Flag.String("session").pipe(
|
||||
session: Flag.string("session").pipe(
|
||||
Flag.withAlias("s"),
|
||||
Flag.withDescription("Session ID to continue"),
|
||||
Flag.optional,
|
||||
),
|
||||
fork: Flag.Boolean("fork").pipe(
|
||||
fork: Flag.boolean("fork").pipe(
|
||||
Flag.withDescription("Fork the session when continuing"),
|
||||
Flag.withDefault(false),
|
||||
),
|
||||
replay: Flag.Boolean("replay").pipe(
|
||||
replay: Flag.boolean("replay").pipe(
|
||||
Flag.withDescription("Restore session history on resume and resize (disable with --no-replay)"),
|
||||
Flag.optional,
|
||||
),
|
||||
replayLimit: Flag.Int("replay-limit").pipe(
|
||||
replayLimit: Flag.integer("replay-limit").pipe(
|
||||
Flag.withDescription("Limit replay to the newest N messages (default: 200)"),
|
||||
Flag.optional,
|
||||
),
|
||||
model: Flag.String("model").pipe(
|
||||
model: Flag.string("model").pipe(
|
||||
Flag.withAlias("m"),
|
||||
Flag.withDescription("Model to use in the format provider/model"),
|
||||
Flag.optional,
|
||||
),
|
||||
agent: Flag.String("agent").pipe(Flag.withDescription("Agent to use"), Flag.optional),
|
||||
prompt: Flag.String("prompt").pipe(Flag.withDescription("Prompt to use"), Flag.optional),
|
||||
demo: Flag.Boolean("demo").pipe(Flag.withDefault(false), Flag.withHidden),
|
||||
agent: Flag.string("agent").pipe(Flag.withDescription("Agent to use"), Flag.optional),
|
||||
prompt: Flag.string("prompt").pipe(Flag.withDescription("Prompt to use"), Flag.optional),
|
||||
demo: Flag.boolean("demo").pipe(Flag.withDefault(false), Flag.withHidden),
|
||||
},
|
||||
}),
|
||||
Spec.make("run", {
|
||||
description: "Run OpenCode with a message",
|
||||
params: {
|
||||
...ServerParams,
|
||||
message: Argument.String("message").pipe(
|
||||
message: Argument.string("message").pipe(
|
||||
Argument.withDescription("Message to send"),
|
||||
Argument.variadic({ min: 0 }),
|
||||
),
|
||||
continue: Flag.Boolean("continue").pipe(
|
||||
continue: Flag.boolean("continue").pipe(
|
||||
Flag.withAlias("c"),
|
||||
Flag.withDescription("Continue the last session"),
|
||||
Flag.withDefault(false),
|
||||
),
|
||||
session: Flag.String("session").pipe(
|
||||
session: Flag.string("session").pipe(
|
||||
Flag.withAlias("s"),
|
||||
Flag.withDescription("Session ID to continue"),
|
||||
Flag.optional,
|
||||
),
|
||||
fork: Flag.Boolean("fork").pipe(
|
||||
fork: Flag.boolean("fork").pipe(
|
||||
Flag.withDescription("Fork the session before continuing"),
|
||||
Flag.withDefault(false),
|
||||
),
|
||||
model: Flag.String("model").pipe(
|
||||
model: Flag.string("model").pipe(
|
||||
Flag.withAlias("m"),
|
||||
Flag.withDescription("Model to use in the format provider/model#variant"),
|
||||
Flag.optional,
|
||||
),
|
||||
agent: Flag.String("agent").pipe(Flag.withDescription("Agent to use"), Flag.optional),
|
||||
format: Flag.Literals("format", ["default", "json"]).pipe(
|
||||
agent: Flag.string("agent").pipe(Flag.withDescription("Agent to use"), Flag.optional),
|
||||
format: Flag.choice("format", ["default", "json"]).pipe(
|
||||
Flag.withDescription("Output format"),
|
||||
Flag.withDefault("default"),
|
||||
),
|
||||
file: Flag.String("file").pipe(
|
||||
file: Flag.string("file").pipe(
|
||||
Flag.withAlias("f"),
|
||||
Flag.withDescription("File to attach to the message"),
|
||||
Flag.atMost(100),
|
||||
),
|
||||
title: Flag.String("title").pipe(Flag.withDescription("Session title"), Flag.optional),
|
||||
thinking: Flag.Boolean("thinking").pipe(Flag.withDescription("Show thinking blocks"), Flag.withDefault(false)),
|
||||
title: Flag.string("title").pipe(Flag.withDescription("Session title"), Flag.optional),
|
||||
thinking: Flag.boolean("thinking").pipe(Flag.withDescription("Show thinking blocks"), Flag.withDefault(false)),
|
||||
...PermissionParams,
|
||||
},
|
||||
}),
|
||||
@@ -402,13 +400,13 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME
|
||||
description: "List top-level sessions in the current project, newest first",
|
||||
params: {
|
||||
...ServerParams,
|
||||
maxCount: Flag.Int("max-count").pipe(
|
||||
maxCount: Flag.integer("max-count").pipe(
|
||||
Flag.withAlias("n"),
|
||||
Flag.withSchema(Schema.Int.check(Schema.isGreaterThanOrEqualTo(1))),
|
||||
Flag.withDescription("Limit to N most recent sessions (default: 100)"),
|
||||
Flag.optional,
|
||||
),
|
||||
format: Flag.Literals("format", ["table", "json"]).pipe(
|
||||
format: Flag.choice("format", ["table", "json"]).pipe(
|
||||
Flag.withDescription("Output format"),
|
||||
Flag.withDefault("table"),
|
||||
),
|
||||
@@ -418,18 +416,18 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME
|
||||
description: "Delete a session and its child sessions",
|
||||
params: {
|
||||
...ServerParams,
|
||||
sessionID: Argument.String("sessionID").pipe(Argument.withDescription("Session ID to delete")),
|
||||
sessionID: Argument.string("sessionID").pipe(Argument.withDescription("Session ID to delete")),
|
||||
},
|
||||
}),
|
||||
Spec.make("export", {
|
||||
description: "Export session data as JSON",
|
||||
params: {
|
||||
...ServerParams,
|
||||
session: Argument.String("session").pipe(
|
||||
session: Argument.string("session").pipe(
|
||||
Argument.withDescription("Session ID to export"),
|
||||
Argument.optional,
|
||||
),
|
||||
sanitize: Flag.Boolean("sanitize").pipe(
|
||||
sanitize: Flag.boolean("sanitize").pipe(
|
||||
Flag.withDescription("Redact sensitive transcript and file data"),
|
||||
Flag.withDefault(false),
|
||||
),
|
||||
@@ -439,8 +437,8 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME
|
||||
description: "Import session data from a JSON file or URL",
|
||||
params: {
|
||||
...ServerParams,
|
||||
file: Argument.String("file").pipe(Argument.withDescription("JSON file or URL to import")),
|
||||
directory: Flag.String("directory").pipe(
|
||||
file: Argument.string("file").pipe(Argument.withDescription("JSON file or URL to import")),
|
||||
directory: Flag.string("directory").pipe(
|
||||
Flag.withDescription("Directory in which to import the session"),
|
||||
Flag.optional,
|
||||
),
|
||||
@@ -458,8 +456,8 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME
|
||||
Spec.make("get", {
|
||||
description: "Get service configuration",
|
||||
params: {
|
||||
key: Argument.String("key").pipe(Argument.withDescription("Service setting or env"), Argument.optional),
|
||||
name: Argument.String("name").pipe(
|
||||
key: Argument.string("key").pipe(Argument.withDescription("Service setting or env"), Argument.optional),
|
||||
name: Argument.string("name").pipe(
|
||||
Argument.withDescription("Environment variable name"),
|
||||
Argument.optional,
|
||||
),
|
||||
@@ -468,11 +466,11 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME
|
||||
Spec.make("set", {
|
||||
description: "Set service configuration",
|
||||
params: {
|
||||
key: Argument.String("key").pipe(Argument.withDescription("Service setting or env")),
|
||||
value: Argument.String("value").pipe(
|
||||
key: Argument.string("key").pipe(Argument.withDescription("Service setting or env")),
|
||||
value: Argument.string("value").pipe(
|
||||
Argument.withDescription("Setting value or environment variable name"),
|
||||
),
|
||||
nestedValue: Argument.String("env-value").pipe(
|
||||
nestedValue: Argument.string("env-value").pipe(
|
||||
Argument.withDescription("Environment variable value"),
|
||||
Argument.optional,
|
||||
),
|
||||
@@ -481,8 +479,8 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME
|
||||
Spec.make("unset", {
|
||||
description: "Unset service configuration",
|
||||
params: {
|
||||
key: Argument.String("key").pipe(Argument.withDescription("Service setting or env")),
|
||||
name: Argument.String("name").pipe(
|
||||
key: Argument.string("key").pipe(Argument.withDescription("Service setting or env")),
|
||||
name: Argument.string("name").pipe(
|
||||
Argument.withDescription("Environment variable name"),
|
||||
Argument.optional,
|
||||
),
|
||||
@@ -499,7 +497,7 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME
|
||||
Spec.make("pair", {
|
||||
description: "Show server pairing information",
|
||||
params: {
|
||||
url: Flag.String("url").pipe(
|
||||
url: Flag.string("url").pipe(
|
||||
Flag.withDescription("Advertise an external HTTP(S) server URL in the pairing QR code"),
|
||||
Flag.mapTryCatch(
|
||||
(value) => {
|
||||
@@ -517,15 +515,15 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME
|
||||
Spec.make("serve", {
|
||||
description: "Start the v2 API and web server",
|
||||
params: {
|
||||
hostname: Flag.String("hostname").pipe(Flag.optional),
|
||||
port: Flag.Int("port").pipe(Flag.optional),
|
||||
cors: Flag.String("cors").pipe(
|
||||
hostname: Flag.string("hostname").pipe(Flag.optional),
|
||||
port: Flag.integer("port").pipe(Flag.optional),
|
||||
cors: Flag.string("cors").pipe(
|
||||
Flag.withSchema(Schema.NonEmptyString),
|
||||
Flag.withDescription("Additional allowed CORS origin (repeat for multiple origins)"),
|
||||
Flag.atLeast(0),
|
||||
),
|
||||
service: Flag.Boolean("service").pipe(Flag.withDefault(false)),
|
||||
stdio: Flag.Boolean("stdio").pipe(Flag.withDefault(false)),
|
||||
service: Flag.boolean("service").pipe(Flag.withDefault(false)),
|
||||
stdio: Flag.boolean("stdio").pipe(Flag.withDefault(false)),
|
||||
},
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -7,8 +7,8 @@ import { Config } from "effect"
|
||||
// The opencode server password: sent by clients connecting to an explicit
|
||||
// --server, and adopted by a manually run or standalone server. The legacy
|
||||
// name is still honored.
|
||||
export const password = Config.Redacted("OPENCODE_PASSWORD").pipe(
|
||||
Config.orElse(() => Config.Redacted("OPENCODE_SERVER_PASSWORD")),
|
||||
export const password = Config.redacted("OPENCODE_PASSWORD").pipe(
|
||||
Config.orElse(() => Config.redacted("OPENCODE_SERVER_PASSWORD")),
|
||||
Config.withDefault(undefined),
|
||||
)
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { NodeRuntime, NodeServices } from "@effect/platform-node"
|
||||
import { Cause, Effect } from "effect"
|
||||
import { getErrorReported } from "effect/Runtime"
|
||||
import { Effect } from "effect"
|
||||
import { Commands } from "./commands/commands"
|
||||
import { Runtime } from "./framework/runtime"
|
||||
import { Observability } from "@opencode/util/observability"
|
||||
@@ -131,15 +130,5 @@ Effect.gen(function* () {
|
||||
Effect.provide(NodeServices.layer),
|
||||
Effect.scoped,
|
||||
Effect.tap(() => Effect.sync(() => process.exit(process.exitCode ?? 0))),
|
||||
// runMain's default reporter logs the fatal cause to stdout. Write it to stderr instead: the
|
||||
// desktop and `Service.ensure` only capture stderr from `serve --service`, so this is the only
|
||||
// channel through which a startup failure's reason reaches the user.
|
||||
Effect.tapCause((cause) =>
|
||||
Effect.sync(() => {
|
||||
if (Cause.hasInterruptsOnly(cause)) return
|
||||
if (!getErrorReported(Cause.squash(cause))) return
|
||||
process.stderr.write(Cause.pretty(cause) + "\n")
|
||||
}),
|
||||
),
|
||||
NodeRuntime.runMain({ disableErrorReporting: true }),
|
||||
NodeRuntime.runMain,
|
||||
)
|
||||
|
||||
@@ -4,7 +4,6 @@ import { Service, type Info } from "@opencode/client/effect/service"
|
||||
import path from "node:path"
|
||||
import { Effect, FileSystem, Schedule, Schema } from "effect"
|
||||
import { HttpServer } from "effect/unstable/http"
|
||||
import { NetAddress } from "effect/unstable/net"
|
||||
import { OPENCODE_VERSION } from "../version"
|
||||
|
||||
const infoJson = Schema.fromJsonString(Service.Info)
|
||||
@@ -12,7 +11,7 @@ const encodeInfo = Schema.encodeEffect(infoJson)
|
||||
const decodeInfo = Schema.decodeUnknownEffect(infoJson)
|
||||
|
||||
export const register = Effect.fnUntraced(function* (options: {
|
||||
readonly address: NetAddress.SocketAddress
|
||||
readonly address: HttpServer.Address
|
||||
readonly password: string
|
||||
readonly id: string
|
||||
readonly file: string
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { NodeSocket } from "@effect/platform-node"
|
||||
import { Effect, Schema, Stdio, Stream } from "effect"
|
||||
import { Socket } from "effect/unstable/socket"
|
||||
|
||||
const Response = Schema.fromJsonString(Schema.Struct({ value: Schema.NullOr(Schema.String) }))
|
||||
const Port = Schema.NumberFromString.check(Schema.isInt(), Schema.isGreaterThan(0), Schema.isLessThanOrEqualTo(65535))
|
||||
@@ -11,15 +10,16 @@ export const askpass = Effect.gen(function* () {
|
||||
const port = yield* Schema.decodeUnknownEffect(Port)(process.env.OPENCODE_SSH_ASKPASS_PORT)
|
||||
const stdio = yield* Stdio.Stdio
|
||||
const socket = yield* NodeSocket.makeNet({ host: "127.0.0.1", port })
|
||||
const writer = yield* socket.writer
|
||||
const write = yield* socket.writer
|
||||
const response = { text: "" }
|
||||
yield* Effect.all(
|
||||
[
|
||||
Effect.gen(function* () {
|
||||
const pull = yield* Socket.readerString(socket)
|
||||
while (true) response.text += (yield* pull).join("")
|
||||
}).pipe(Effect.catchReason("SocketError", "SocketCloseError", () => Effect.void)),
|
||||
writer.write(
|
||||
socket.runString((text) =>
|
||||
Effect.sync(() => {
|
||||
response.text += text
|
||||
}),
|
||||
),
|
||||
write(
|
||||
JSON.stringify({
|
||||
token: process.env.OPENCODE_SSH_ASKPASS_TOKEN,
|
||||
text: process.argv.slice(2).join(" "),
|
||||
|
||||
@@ -4,7 +4,6 @@ import { Global } from "@opencode/util/global"
|
||||
import { OPENCODE_VERSION } from "../src/version"
|
||||
import { expect, test } from "bun:test"
|
||||
import { Effect, Schema } from "effect"
|
||||
import { NetAddress } from "effect/unstable/net"
|
||||
import fs from "node:fs/promises"
|
||||
import os from "node:os"
|
||||
import path from "node:path"
|
||||
@@ -502,7 +501,7 @@ test("service registration replaces a stale owner with the bound address", async
|
||||
try {
|
||||
const cleanup = await Effect.runPromise(
|
||||
ServiceRegistration.register({
|
||||
address: NetAddress.inetAddressFromIpStringUnsafe("127.0.0.1", 4321),
|
||||
address: { _tag: "TcpAddress", hostname: "127.0.0.1", port: 4321 },
|
||||
password: "secret",
|
||||
id: "owner",
|
||||
file: registration,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/client",
|
||||
"version": "2.0.11",
|
||||
"version": "2.0.10",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
@@ -37,7 +37,7 @@
|
||||
"@opencode/protocol": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"effect": "4.0.0-rc.115",
|
||||
"effect": "4.0.0-rc.112",
|
||||
"solid-js": ">=1.9.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
|
||||
@@ -241,6 +241,7 @@ export type SessionSwitchModelOperation<E = never> = (
|
||||
export type SessionUpdateInput = {
|
||||
readonly sessionID: Session.ID
|
||||
readonly title?: string | undefined
|
||||
readonly metadata?: Session.Metadata | undefined
|
||||
readonly permissions?: Permission.Ruleset | undefined
|
||||
}
|
||||
export type SessionUpdateOutput = void
|
||||
@@ -516,6 +517,20 @@ export type SessionLogOutput =
|
||||
| undefined
|
||||
readonly data: { readonly sessionID: Session.ID; readonly title: string }
|
||||
}
|
||||
| {
|
||||
readonly id: Event.ID
|
||||
readonly created: number
|
||||
readonly metadata?: { readonly [x: string]: unknown } | undefined
|
||||
readonly type: "session.metadata.updated"
|
||||
readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version }
|
||||
readonly location?:
|
||||
| {
|
||||
readonly directory: AbsolutePath
|
||||
readonly workspaceID?: (string & Brand.Brand<"Workspace.ID">) | undefined
|
||||
}
|
||||
| undefined
|
||||
readonly data: { readonly sessionID: Session.ID; readonly metadata: Session.Metadata }
|
||||
}
|
||||
| {
|
||||
readonly id: Event.ID
|
||||
readonly created: number
|
||||
|
||||
@@ -453,7 +453,7 @@ const EndpointSessionUpdate = (raw: RawClient["server.session"]) => (input: Sess
|
||||
preserveEffect<SessionUpdateOutput>()(
|
||||
raw["session.update"]({
|
||||
params: { sessionID: input["sessionID"] },
|
||||
payload: { title: input["title"], permissions: input["permissions"] },
|
||||
payload: { title: input["title"], metadata: input["metadata"], permissions: input["permissions"] },
|
||||
}).pipe(Effect.mapError(mapClientError)),
|
||||
)
|
||||
|
||||
|
||||
@@ -662,7 +662,7 @@ export function make(options: ClientOptions) {
|
||||
{
|
||||
method: "PATCH",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}`,
|
||||
body: { title: input["title"], permissions: input["permissions"] },
|
||||
body: { title: input["title"], metadata: input["metadata"], permissions: input["permissions"] },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [400, 401, 404],
|
||||
empty: true,
|
||||
|
||||
@@ -1221,6 +1221,16 @@ export type SessionMoved = {
|
||||
|
||||
export type SessionInboxMovePayload1 = { location: LocationRef; projectID: string; subpath?: string }
|
||||
|
||||
export type SessionMetadataUpdated = {
|
||||
id: string
|
||||
created: number
|
||||
metadata?: { [x: string]: any }
|
||||
type: "session.metadata.updated"
|
||||
durable: { aggregateID: string; seq: number; version: 1 }
|
||||
location?: LocationRef
|
||||
data: { sessionID: string; metadata: SessionMetadata }
|
||||
}
|
||||
|
||||
export type SessionShellStarted = {
|
||||
id: string
|
||||
created: number
|
||||
@@ -2308,6 +2318,7 @@ export type SessionEventDurable =
|
||||
| SessionModelSelected
|
||||
| SessionMoved
|
||||
| SessionRenamed
|
||||
| SessionMetadataUpdated
|
||||
| SessionPermissions
|
||||
| SessionViewed
|
||||
| SessionDeleted
|
||||
@@ -2370,6 +2381,7 @@ export type V2Event =
|
||||
| SessionModelSelected
|
||||
| SessionMoved
|
||||
| SessionRenamed
|
||||
| SessionMetadataUpdated
|
||||
| SessionPermissions
|
||||
| SessionViewed
|
||||
| SessionUsageUpdated
|
||||
@@ -3946,12 +3958,21 @@ export type SessionUpdateInput = {
|
||||
readonly sessionID: { readonly sessionID: string }["sessionID"]
|
||||
readonly title?: {
|
||||
readonly title?: string | undefined
|
||||
readonly metadata?: { readonly [x: string]: JsonValue } | undefined
|
||||
readonly permissions?:
|
||||
| ReadonlyArray<{ readonly action: string; readonly resource: string; readonly effect: "allow" | "deny" | "ask" }>
|
||||
| undefined
|
||||
}["title"]
|
||||
readonly metadata?: {
|
||||
readonly title?: string | undefined
|
||||
readonly metadata?: { readonly [x: string]: JsonValue } | undefined
|
||||
readonly permissions?:
|
||||
| ReadonlyArray<{ readonly action: string; readonly resource: string; readonly effect: "allow" | "deny" | "ask" }>
|
||||
| undefined
|
||||
}["metadata"]
|
||||
readonly permissions?: {
|
||||
readonly title?: string | undefined
|
||||
readonly metadata?: { readonly [x: string]: JsonValue } | undefined
|
||||
readonly permissions?:
|
||||
| ReadonlyArray<{ readonly action: string; readonly resource: string; readonly effect: "allow" | "deny" | "ask" }>
|
||||
| undefined
|
||||
|
||||
@@ -24,7 +24,7 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
|
||||
and Map, RegExp, and generators serialize as `{}`. A bare `undefined` result is `null`.
|
||||
Tool results come back the way `JSON.parse(JSON.stringify(result))` would. The table, where a value cannot
|
||||
be JSON but what the program meant is clear: a promise is awaited (a rejection fails the program), a Set
|
||||
crosses as an array, a URLSearchParams as its query string, an Error as `{ name, message, ...own enumerable }`, a
|
||||
crosses as an array, a URLSearchParams as its query string, an Error as `{ name, message, ...own }`, a
|
||||
Uint8Array is rejected with a hint to encode as text, and own `__proto__` keys are dropped so merging tool
|
||||
inputs or results cannot replace a prototype. In-program `JSON.stringify` keeps JS behavior except for the
|
||||
Error form and a promise, which is a `TypeError` with an await hint rather than a silent `{}`.
|
||||
@@ -38,11 +38,8 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
|
||||
10,000,000 elements (`Array(n)`, `length =`, `Array.from`, `split`, `matchAll`, `concat`, `flat`; below the JS
|
||||
maximum of 2^32 - 1), and 10,000 pending promises at once. Exceeding one throws a `RangeError`. A single regular
|
||||
expression match can still run long on a pathological pattern; the host regex engine has no interrupt hook.
|
||||
- [x] A trailing comma after a rest parameter is a syntax error, with or without `"use strict"`.
|
||||
- [x] A program that begins with `"use strict"` rejects `yield` as an identifier and duplicate parameter names at
|
||||
parse time. Without it, `yield` is an ordinary binding.
|
||||
- [ ] Duplicate parameter names in non-strict code throw when the function is called, instead of binding the last
|
||||
parameter as JavaScript does.
|
||||
- [ ] Strict-mode early errors: duplicate parameter names, `yield` as an identifier, and a trailing comma after a
|
||||
rest parameter are accepted unless the program itself begins with `"use strict"`.
|
||||
|
||||
## Values and literals
|
||||
|
||||
@@ -56,7 +53,7 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
|
||||
- [x] `NaN` and `Infinity` globals.
|
||||
- [ ] BigInt literals and in-interpreter BigInt arithmetic; BigInt remains invalid at JSON-like host boundaries.
|
||||
- [ ] Arbitrary Symbol primitive values and symbol-keyed properties. The confined `Symbol.iterator` and
|
||||
`Symbol.asyncIterator` keys are available only for the iterator protocols.
|
||||
`Symbol.asyncIterator` keys are available only for custom iterator protocols.
|
||||
- [ ] Tagged-template calls.
|
||||
- [ ] Getter and setter definitions in object literals.
|
||||
|
||||
@@ -95,10 +92,9 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
|
||||
- [x] `if`/`else` and conditional expressions.
|
||||
- [x] `switch`, including default clauses and fallthrough.
|
||||
- [x] `for`, `while`, and `do...while`.
|
||||
- [x] `for...of` over arrays, strings, Maps, Sets, URLSearchParams, Headers, Uint8Arrays, built-in iterators, custom
|
||||
synchronous iterators, and confined synchronous generators. Abrupt completion invokes the iterator's optional `return()`.
|
||||
- [x] `for...in` over own keys of plain objects, arrays, strings, and tool references. `null`, `undefined`, and other
|
||||
non-objects iterate nothing. An un-awaited promise throws rather than iterating.
|
||||
- [x] `for...of` over arrays, strings, Maps, Sets, URLSearchParams, Headers, custom synchronous iterators, and
|
||||
confined synchronous generators. Abrupt completion invokes the iterator's optional `return()`.
|
||||
- [x] `for...in` over own keys of plain objects, arrays, strings, and tool references; other values iterate nothing.
|
||||
- [x] Unlabeled `break` and `continue`.
|
||||
- [x] `try`, `catch`, optional catch bindings, and `finally`.
|
||||
- [x] `throw` with arbitrary values.
|
||||
@@ -112,8 +108,7 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
|
||||
## Functions and callbacks
|
||||
|
||||
- [x] Function declarations, function expressions, and arrow functions.
|
||||
- [x] Synchronous and `async` functions. A line break between `function` and the name is allowed, as in JavaScript;
|
||||
a line break between `async` and `function` is not an async function.
|
||||
- [x] Synchronous and `async` functions.
|
||||
- [x] Closures, recursion, default parameters, rest parameters, and destructured parameters.
|
||||
- [x] A call depth limit of 10000: deeper nesting throws a catchable `RangeError: Maximum call stack size exceeded`
|
||||
at the overflowing call instead of running until the timeout. Callbacks invoked by built-ins count below the
|
||||
@@ -148,6 +143,7 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
|
||||
`Array.prototype.push.name === "push"`).
|
||||
- [ ] A named function expression's name is not bound inside its own body.
|
||||
- [ ] Redeclaring a function in the same scope is rejected; in JavaScript the last declaration wins.
|
||||
- [ ] A line terminator between `async function` and the function name.
|
||||
- [ ] Generator and async generator functions evaluate parameter defaults and destructuring at the first `next()`
|
||||
rather than at the call, so their errors are not thrown synchronously.
|
||||
- [x] Synchronous and async generator declarations/expressions, `yield`, and `yield*`, including lazy bodies,
|
||||
@@ -184,8 +180,7 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
|
||||
on any other value throws a catchable `TypeError` naming the callee: other built-in functions such as `Number`
|
||||
say `new` is unsupported and point at the plain call, user-defined functions report the constructor gap below,
|
||||
and non-callable values are not constructors. Error constructors take the ES2022 options object, so
|
||||
`new Error(message, { cause })` installs a non-enumerable `cause` when the option is present. `Error.isError`
|
||||
is true for every Error value.
|
||||
`new Error(message, { cause })` installs a non-enumerable `cause` when the option is present.
|
||||
- [x] Arithmetic operators: `+`, `-`, `*`, `/`, `%`, and `**`.
|
||||
- [x] Equality and ordering: `==`, `!=`, `===`, `!==`, `<`, `<=`, `>`, and `>=`.
|
||||
- [x] Bitwise operators: `&`, `|`, `^`, `~`, `<<`, `>>`, and `>>>`.
|
||||
@@ -275,8 +270,8 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
|
||||
- [x] `Object.is` for supported data values.
|
||||
- [x] `Object.groupBy` over finite collections and custom synchronous iterators/generators, with string-key coercion
|
||||
and plain-object results.
|
||||
- [x] `Object.prototype` methods on values: `toString` (`"[object Array]"`), `toLocaleString` (calls the value's
|
||||
`toString`, as in JS), `valueOf`, `hasOwnProperty`, `isPrototypeOf`, and `propertyIsEnumerable`.
|
||||
- [x] `Object.prototype` methods on values: `toString` (`"[object Array]"`), `toLocaleString`, `valueOf`,
|
||||
`hasOwnProperty`, `isPrototypeOf`, and `propertyIsEnumerable`.
|
||||
|
||||
## Arrays
|
||||
|
||||
@@ -290,14 +285,9 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
|
||||
`lastIndexOf`.
|
||||
- [x] Aggregation: `reduce` and `reduceRight`.
|
||||
- [x] Ordering: `sort`, `toSorted`, `reverse`, and `toReversed`.
|
||||
- [x] Access/copying: `at`, `slice`, `concat`, `flat`, `with`, `join`, and `toLocaleString` (each element's
|
||||
`toLocaleString`, holes and nullish elements as empty strings).
|
||||
- [x] Access/copying: `at`, `slice`, `concat`, `flat`, `with`, and `join`.
|
||||
- [x] Mutation: `push`, `pop`, `shift`, `unshift`, `splice`, `fill`, and `copyWithin`.
|
||||
- [x] `keys`, `values`, `entries`, and `[Symbol.iterator]` (the same function as `values`) return live iterator objects
|
||||
with `next()` and `[Symbol.iterator]`, as in JS. Iterator objects are opaque references: they print as
|
||||
`[opaque reference]`, serialize to `{}`, and cannot be passed to extensions. Every built-in collection iterator
|
||||
shares one prototype. JavaScript gives each collection its own; the difference is not observable here because
|
||||
`Object.getPrototypeOf` is not exposed.
|
||||
- [x] Materialized iteration helpers: `keys`, `values`, and `entries` return arrays rather than iterators.
|
||||
- [x] `length`, numeric indexing, index assignment, spread, and `for...of`.
|
||||
- [x] The `thisArg` argument of `Array.from` is accepted and ignored, like JS arrows.
|
||||
- [x] `Array.prototype.toSpliced`.
|
||||
@@ -310,13 +300,13 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
|
||||
form, like `JSON.stringify`.
|
||||
- [ ] Argument coercion for `indexOf`, `lastIndexOf`, `includes`, `fill`, `flat`, `copyWithin`, and the `join`
|
||||
separator: JavaScript applies ToIntegerOrInfinity/ToString (including `valueOf`, strings, and `undefined`), the
|
||||
interpreter requires numbers and strings. `indexOf()` and `lastIndexOf()` with no argument already search for
|
||||
`undefined`; `includes()` still requires a value.
|
||||
interpreter requires numbers and strings; `includes()`/`indexOf()` with no argument should search for
|
||||
`undefined`.
|
||||
- [ ] Iterator objects from `keys`, `values`, and `entries` with a live `next()`.
|
||||
|
||||
## Strings
|
||||
|
||||
- [x] Case/normalization: `toLowerCase`, `toUpperCase`, `normalize`, and the `toLocaleLowerCase`/`toLocaleUpperCase`
|
||||
aliases, which ignore their locale argument and apply the default Unicode casing.
|
||||
- [x] Case/normalization: `toLowerCase`, `toUpperCase`, `normalize`.
|
||||
- [x] Trimming: `trim`, `trimStart`, and `trimEnd`, plus the Annex B `trimLeft` and `trimRight` aliases.
|
||||
- [x] Searching/tests: `includes`, `startsWith`, `endsWith`, `indexOf`, `lastIndexOf`, and `search`.
|
||||
- [x] Slicing/access: `slice`, `substring`, Annex B `substr`, `at`, `charAt`, `charCodeAt`, and `codePointAt`.
|
||||
@@ -324,7 +314,7 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
|
||||
- [x] Regular-expression integration: `match`, materialized `matchAll`, `replace`, `replaceAll`, `split`, and `search`.
|
||||
- [x] `localeCompare`; locale and options arguments are currently ignored.
|
||||
- [x] `isWellFormed` and `toWellFormed`.
|
||||
- [x] `toString`, `length`, numeric indexing, spread, `for...of`, and `[Symbol.iterator]` by Unicode code point.
|
||||
- [x] `toString`, `length`, numeric indexing, spread, and `for...of` by Unicode code point.
|
||||
- [x] Static `String.fromCharCode` and `String.fromCodePoint`.
|
||||
- [x] Native argument coercion for supported String methods; for example, `includes(1)` and `slice("1")` coerce like
|
||||
native JS, `split(undefined)` returns the whole string, and `includes`/`startsWith`/`endsWith` reject regular
|
||||
@@ -340,8 +330,7 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
|
||||
- [x] Coercion functions: `Number`, `parseInt`, and `parseFloat`.
|
||||
- [x] Number predicates/parsers: `Number.isInteger`, `Number.isFinite`, `Number.isNaN`, `Number.isSafeInteger`,
|
||||
`Number.parseInt`, and `Number.parseFloat`.
|
||||
- [x] Number formatting: `toFixed`, `toPrecision`, `toExponential`, `toString`, `valueOf`, and `toLocaleString`, which
|
||||
always formats as `en-US` (`"1,234.5"`) so output does not depend on the host.
|
||||
- [x] Number formatting: `toFixed`, `toPrecision`, `toExponential`, `toString`, and `valueOf`.
|
||||
- [x] Number constants: `MAX_SAFE_INTEGER`, `MIN_SAFE_INTEGER`, `MAX_VALUE`, `MIN_VALUE`, `EPSILON`, `NaN`,
|
||||
`POSITIVE_INFINITY`, and `NEGATIVE_INFINITY`.
|
||||
- [x] Math constants: `PI`, `E`, `LN2`, `LN10`, `LOG2E`, `LOG10E`, `SQRT2`, and `SQRT1_2`.
|
||||
@@ -354,8 +343,7 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
|
||||
use their epoch time) and reject opaque runtime references as data errors.
|
||||
- [x] Unknown static members on global namespaces and on `Number`/`String`/the coercion functions read as `undefined`
|
||||
for feature detection. Calling any undefined value reports a native-style `TypeError` naming the callee, for
|
||||
example `Math.sum is not a function.` or `search(...).catch is not a function.` Unknown `Promise` statics keep
|
||||
their descriptive error.
|
||||
example `Math.sum is not a function.` Unknown `Promise` statics keep their descriptive error.
|
||||
- [x] `Math.sumPrecise` over finite collections and custom synchronous iterators/generators, rejecting non-number
|
||||
elements without coercion.
|
||||
- [x] Global coercing `isFinite` and `isNaN`; opaque runtime references reject as data errors, like `Number(...)`.
|
||||
@@ -389,8 +377,6 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
|
||||
`TimeClip` behavior.
|
||||
- [x] `Date.prototype.toUTCString` and its `toGMTString` alias.
|
||||
- [x] `toDateString` and `toTimeString` in the host's local timezone.
|
||||
- [x] `toLocaleString`, `toLocaleDateString`, and `toLocaleTimeString` always format as `en-US` in UTC
|
||||
(`"1/1/1970, 12:00:00 AM"`) so output does not depend on the host.
|
||||
- [x] Native one-argument Date coercion for supported values, including booleans, null, arrays, and plain objects.
|
||||
- [ ] Date setters and multi-argument construction coerce object arguments through `valueOf`/`toString` and surface
|
||||
their throws.
|
||||
@@ -416,11 +402,10 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
|
||||
|
||||
- [x] Static `Map.groupBy` over finite collections and custom synchronous iterators/generators, preserving key identity.
|
||||
- [x] `new Map()` from synchronous iterables of entries.
|
||||
- [x] Map `get`, `set`, `has`, `delete`, `clear`, `size`, `forEach`, `getOrInsert`, and `getOrInsertComputed`.
|
||||
- [x] Map `get`, `set`, `has`, `delete`, `clear`, `size`, and `forEach`.
|
||||
- [x] `new Set()` from synchronous iterables.
|
||||
- [x] Set `add`, `has`, `delete`, `clear`, `size`, and `forEach`.
|
||||
- [x] Live `keys`, `values`, `entries`, and `[Symbol.iterator]` iterators for Map and Set; a Set-like operand's `keys()`
|
||||
may return a built-in iterator or an array.
|
||||
- [x] Materialized `keys`, `values`, and `entries` arrays for Map and Set.
|
||||
- [x] Spread, `for...of`, `Array.from`, and `Object.fromEntries` integration.
|
||||
- [x] Map and Set values serialize to `{}` at host/JSON boundaries.
|
||||
- [x] Set composition and relation methods: `union`, `intersection`, `difference`, `symmetricDifference`, `isSubsetOf`,
|
||||
@@ -436,9 +421,8 @@ ultimate source of truth. Upstream test262 files run verbatim from `test/test262
|
||||
- [x] Writable URL fields except `origin`.
|
||||
- [x] `new URLSearchParams()` from query strings, data objects, synchronous iterables of pairs, and URLSearchParams.
|
||||
- [x] URLSearchParams `append`, `delete`, `get`, `getAll`, `has`, `set`, `sort`, `forEach`, `keys`, `values`,
|
||||
`entries`, `[Symbol.iterator]`, `toString`, and `size`.
|
||||
- [x] URL values are their href in `JSON.stringify` and at the host boundary. URLSearchParams are `{}` in
|
||||
`JSON.stringify` and their query string at the host boundary.
|
||||
`entries`, `toString`, and `size`.
|
||||
- [x] URL values serialize to their href; URLSearchParams serialize to `{}`.
|
||||
|
||||
## Uint8Array
|
||||
|
||||
@@ -450,8 +434,7 @@ with a hint to encode as text first (`TextDecoder`, `toBase64`, `toHex`).
|
||||
- [x] Index reads and writes with JS byte semantics: values wrap modulo 256, out-of-range writes are ignored, indexes
|
||||
cannot be deleted. `length` is a prototype accessor, so `Object.keys` lists only indexes.
|
||||
- [x] `at`, `slice`, `subarray` (a view on the same bytes), `set`, `fill`, `reverse`, `indexOf`, `lastIndexOf`,
|
||||
`includes`, `join`, `toString`, `toBase64`, `toHex`, and live `keys`, `values`, `entries`, and `[Symbol.iterator]`
|
||||
iterators.
|
||||
`includes`, `join`, `toString`, `toBase64`, `toHex`, and materialized `keys`, `values`, and `entries` arrays.
|
||||
- [x] Spread, destructuring, `for...of`, `yield*`, `Array.from`, and `new Set(bytes)`. `Array.isArray` is false.
|
||||
- [x] String coercion joins with commas; `JSON.stringify` gives `{"0":1,...}`; `console.log` prints
|
||||
`Uint8Array(n) [...]`.
|
||||
@@ -467,8 +450,8 @@ with a hint to encode as text first (`TextDecoder`, `toBase64`, `toHex`).
|
||||
`fatal` and `ignoreBOM` options; `decode` takes a Uint8Array or nothing.
|
||||
- [x] `new Headers()` from records, synchronous iterables of pairs, and Headers, wrapping the host's `Headers`: names
|
||||
fold to lowercase, values are normalized and combined, and invalid names or values throw a `TypeError`.
|
||||
- [x] Headers `append`, `delete`, `get`, `getSetCookie`, `has`, `set`, `forEach`, `keys`, `values`, `entries`, and
|
||||
`[Symbol.iterator]`; iteration is live and sorted by name, with `set-cookie` values kept apart.
|
||||
- [x] Headers `append`, `delete`, `get`, `getSetCookie`, `has`, `set`, `forEach`, `keys`, `values`, and `entries`;
|
||||
iteration is live and sorted by name, with `set-cookie` values kept apart.
|
||||
- [x] Headers serialize to a `{ name: value }` object in JSON, in results, and in tool arguments.
|
||||
- [ ] `Request`, `Response`, and `Blob`.
|
||||
- [ ] `crypto.subtle` and `TextDecoder` streaming or non-UTF-8 encodings.
|
||||
@@ -483,8 +466,7 @@ Nothing is exposed unless a host provides it; extension calls are not tool calls
|
||||
- [x] Every value crossing in either direction is converted, never shared: plain objects and arrays are copied,
|
||||
`Date`, `RegExp`, `URL`, `URLSearchParams`, `Headers`, `Map`, `Set`, and `Uint8Array` become fresh copies with
|
||||
their contents converted (a host `ArrayBuffer` comes in as a `Uint8Array`; other typed arrays cannot come out),
|
||||
errors cross as errors with their name, message, `cause`, and own enumerable data, and a `__proto__` key is
|
||||
dropped. Functions, generators,
|
||||
errors cross as errors with their name and message, and a `__proto__` key is dropped. Functions, generators,
|
||||
un-awaited promises, and symbols cannot be passed in; a class instance, a symbol, or a BigInt cannot come out.
|
||||
- [x] A host function inside a result becomes a program function whose calls cross the same way, so a result can
|
||||
carry methods (`res.json()`) whose host closures keep the host state. Diagnostics name it by its path
|
||||
@@ -509,10 +491,8 @@ Nothing is exposed unless a host provides it; extension calls are not tool calls
|
||||
- [x] `AggregateError` with the `(errors, message?)` signature and an own `errors` array, constructed directly or by
|
||||
an all-rejected `Promise.any`; direct construction accepts custom synchronous iterators and generators.
|
||||
- [x] Error `name`/`message`, error inheritance through `instanceof`, and plain-data serialization. `message` is an own
|
||||
non-enumerable property and `name` is inherited, as in JS, so `Object.keys(err)` is `[]` for a plain error. The
|
||||
result boundary still emits `{ name, message, ...own enumerable }`, so a field such as `code` crosses. `cause` is
|
||||
non-enumerable: an extension Error carries it, and this JSON form does not. Errors have no `stack`; the diagnostic
|
||||
carries a 1-based line and column in the submitted source instead.
|
||||
non-enumerable property and `name` is inherited, as in JS, so `Object.keys(err)` is `[]` while the host still
|
||||
receives `{ name, message }`. Errors have no `stack`; the diagnostic carries the source location instead.
|
||||
- [x] `instanceof` against any constructor with a `prototype`, including every built-in and `Function`.
|
||||
- [x] Catchable user throws, runtime failures raised during interpreted evaluation, awaited tool failures, and awaited
|
||||
tool-call-limit failures; parse/compile failures, cooperative timeout, and output bounding remain outside program
|
||||
@@ -521,9 +501,8 @@ Nothing is exposed unless a host provides it; extension calls are not tool calls
|
||||
short orientation to the supported subset; this matrix is the full reference.
|
||||
- [x] Model-visible host failure messages and underlying causes, including output-validation errors.
|
||||
- [x] Caught errors do not distinguish user throws, interpreter failures, and tool failures; a program sees one
|
||||
Error-shaped value in `catch`, rejection handlers, and `Promise.allSettled` reasons. It always has `name` and
|
||||
`message`, plus `cause` and own data when the failure carried them. This is deliberate: the program should
|
||||
handle a failure the same way regardless of where it originated.
|
||||
Error-shaped value with `name` and `message` in `catch`, rejection handlers, and `Promise.allSettled` reasons.
|
||||
This is deliberate: the program should handle a failure the same way regardless of where it originated.
|
||||
- [x] Failures raised by the interpreter are `TypeError`s unless JavaScript names them otherwise (`RangeError`,
|
||||
`ReferenceError`, `SyntaxError`, `URIError`), so `e instanceof TypeError` and `e.constructor === TypeError`
|
||||
hold. Unsupported syntax reached at runtime is a `SyntaxError`; awaited tool failures stay plain `Error`.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/codemode",
|
||||
"version": "2.0.11",
|
||||
"version": "2.0.10",
|
||||
"description": "Effect-native confined code execution over schema-described tools",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Effect, Exit } from "effect"
|
||||
import { coerceToNumber, coerceToString } from "../stdlib/value.js"
|
||||
import type { Interpreter } from "./interpreter.js"
|
||||
import { primitivePrototype } from "./intrinsics.js"
|
||||
import { typeError } from "./model.js"
|
||||
import { Callable, get, Native, DateObj, Obj } from "./objects.js"
|
||||
import { typeofValue } from "./references.js"
|
||||
@@ -44,13 +43,6 @@ export const toPrimitive = <R>(
|
||||
})
|
||||
}
|
||||
|
||||
/** Invoke(value, name): calls the method the value would find through its prototype. */
|
||||
export const invoke = <R>(ctx: Interpreter<R>, value: unknown, name: string, label: string) => {
|
||||
const target = value instanceof Obj ? value : primitivePrototype(ctx.builtins, value)
|
||||
if (target === undefined) throw typeError(`${label} called on null or undefined.`)
|
||||
return ctx.call(get(target, name), value, [])
|
||||
}
|
||||
|
||||
export const toPrimitiveString = <R>(ctx: Interpreter<R>, value: unknown) =>
|
||||
Effect.map(toPrimitive(ctx, value, "string"), coerceToString)
|
||||
|
||||
|
||||
@@ -168,7 +168,6 @@ export const errorGlobal = <R>(type: ErrorType, ctx: Interpreter<R>) => {
|
||||
methods(builtins, prototype, [
|
||||
["toString", 0, (thisValue) => errorToString(receiver(Obj, thisValue, "Error.prototype.toString"))],
|
||||
])
|
||||
methods(builtins, ctor, [["isError", 1, (_, args) => args[0] instanceof ErrorObj]])
|
||||
}
|
||||
return ctor
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
DateObj,
|
||||
ErrorObj,
|
||||
GeneratorObj,
|
||||
IteratorObj,
|
||||
MapObj,
|
||||
Obj,
|
||||
PromiseObj,
|
||||
@@ -62,7 +61,6 @@ export const extensionGlobals = <R>(
|
||||
!(value instanceof Obj) ||
|
||||
value instanceof Callable ||
|
||||
value instanceof GeneratorObj ||
|
||||
value instanceof IteratorObj ||
|
||||
value instanceof PromiseObj
|
||||
) {
|
||||
throw typeError(`${label} contains ${describeValue(value)}, which cannot be passed to an extension.`)
|
||||
|
||||
@@ -12,7 +12,6 @@ import { regexpGlobal } from "../stdlib/regexp.js"
|
||||
import { stringGlobal } from "../stdlib/string.js"
|
||||
import { uriGlobal, urlGlobal, urlSearchParamsGlobal } from "../stdlib/url.js"
|
||||
import { headersGlobal } from "../stdlib/headers.js"
|
||||
import { iteratorGlobals } from "../stdlib/iterator.js"
|
||||
import { coercion } from "../stdlib/value.js"
|
||||
import { base64Global, cryptoGlobal } from "../stdlib/web.js"
|
||||
import { ToolReference } from "../tool-runtime.js"
|
||||
@@ -102,6 +101,5 @@ export const globalNames: ReadonlySet<string> = new Set(Object.keys(table))
|
||||
/** The immutable global bindings of every program, in declaration order. */
|
||||
export const globals = <R>(ctx: Interpreter<R>): ReadonlyArray<readonly [string, unknown]> => {
|
||||
generatorGlobals(ctx)
|
||||
iteratorGlobals(ctx)
|
||||
return Object.entries(table).map(([name, factory]) => [name, factory(ctx)] as const)
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ import {
|
||||
} from "./model.js"
|
||||
import { checkStringLength } from "./limits.js"
|
||||
import { locate, materialize } from "./errors.js"
|
||||
import { type Builtins, primitivePrototype } from "./intrinsics.js"
|
||||
import type { Builtins } from "./intrinsics.js"
|
||||
import { globals } from "./globals.js"
|
||||
import {
|
||||
assign,
|
||||
@@ -79,7 +79,6 @@ import {
|
||||
DateObj,
|
||||
Fn,
|
||||
GeneratorObj,
|
||||
IteratorObj,
|
||||
MapObj,
|
||||
Obj,
|
||||
PromiseObj,
|
||||
@@ -110,25 +109,20 @@ const loopExit = (result: StatementResult, labels: ReadonlySet<string> | undefin
|
||||
return undefined
|
||||
}
|
||||
|
||||
// Native engines name the callee (`search(...).catch is not a function`), including call chains. Returns
|
||||
// undefined when a link cannot be named, so a chain is either named completely or not at all.
|
||||
const calleeDescription = (node: Expression | Super | undefined): string | undefined => {
|
||||
if (node?.type === "Identifier") return node.name
|
||||
if (node?.type === "CallExpression") {
|
||||
const target = calleeDescription(node.callee)
|
||||
return target === undefined ? undefined : `${target}(...)`
|
||||
const calleeDescription = (callee: Expression | Super | undefined): string => {
|
||||
if (callee?.type === "Identifier") return callee.name
|
||||
if (callee?.type === "MemberExpression") {
|
||||
const object = callee.object
|
||||
const property = callee.property
|
||||
const key =
|
||||
!callee.computed && property.type === "Identifier"
|
||||
? property.name
|
||||
: property.type === "Literal" && typeof property.value === "string"
|
||||
? property.value
|
||||
: undefined
|
||||
if (object.type === "Identifier" && key !== undefined) return `${object.name}.${key}`
|
||||
}
|
||||
if (node?.type !== "MemberExpression") return undefined
|
||||
const property = node.property
|
||||
const key =
|
||||
!node.computed && property.type === "Identifier"
|
||||
? property.name
|
||||
: property.type === "Literal" && typeof property.value === "string"
|
||||
? property.value
|
||||
: undefined
|
||||
if (key === undefined) return undefined
|
||||
const object = calleeDescription(node.object)
|
||||
return object === undefined ? undefined : `${object}.${key}`
|
||||
return "The called value"
|
||||
}
|
||||
|
||||
// OrdinaryHasInstance: walk the left operand's chain looking for the constructor's `prototype`.
|
||||
@@ -656,8 +650,8 @@ class Frame<R> {
|
||||
if (declared?.lexical) self.predeclarePattern(declared.pattern, declared.mutable, left)
|
||||
const right = yield* self.evaluateExpression(node.right)
|
||||
|
||||
const cursor = self.hostCursor(right)
|
||||
const iterator = cursor === undefined ? yield* self.customIterator(right, node, awaiting) : undefined
|
||||
const iterator = yield* self.customIterator(right, node, awaiting)
|
||||
const cursor = iterator === undefined ? yield* self.iterate(right, node) : undefined
|
||||
if (iterator === undefined && cursor === undefined) {
|
||||
throw invalidData(
|
||||
`${awaiting ? "for await...of" : "for...of"} requires an array, string, Map, Set, URLSearchParams, or Headers, or custom iterator value.`,
|
||||
@@ -752,20 +746,6 @@ class Frame<R> {
|
||||
}
|
||||
|
||||
iterate(value: unknown, node?: AstNode) {
|
||||
const cursor = this.hostCursor(value)
|
||||
if (cursor !== undefined) return Effect.succeed(cursor)
|
||||
const self = this
|
||||
return Effect.map(this.customIterator(value, node, false), (iterator) =>
|
||||
iterator === undefined
|
||||
? undefined
|
||||
: {
|
||||
next: self.nextIteratorResult(iterator, node, false),
|
||||
close: Effect.suspend(() => self.closeIterator(iterator, node, false)),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private hostCursor(value: unknown) {
|
||||
const iterator =
|
||||
value instanceof Arr
|
||||
? value.items[Symbol.iterator]()
|
||||
@@ -781,21 +761,29 @@ class Frame<R> {
|
||||
? value.headers.entries()
|
||||
: value instanceof Bytes
|
||||
? value.bytes.values()
|
||||
: value instanceof IteratorObj
|
||||
? value.iterator
|
||||
: undefined
|
||||
if (iterator === undefined) return undefined
|
||||
const proto = this.ctx.builtins.Array
|
||||
return {
|
||||
next: Effect.sync(() => {
|
||||
const step = iterator.next()
|
||||
return {
|
||||
done: Boolean(step.done),
|
||||
value: Array.isArray(step.value) ? new Arr(proto, step.value) : step.value,
|
||||
}
|
||||
}),
|
||||
close: Effect.void,
|
||||
: undefined
|
||||
if (iterator !== undefined) {
|
||||
const proto = this.ctx.builtins.Array
|
||||
return Effect.succeed({
|
||||
next: Effect.sync(() => {
|
||||
const step = iterator.next()
|
||||
return {
|
||||
done: Boolean(step.done),
|
||||
value: Array.isArray(step.value) ? new Arr(proto, step.value) : step.value,
|
||||
}
|
||||
}),
|
||||
close: Effect.void,
|
||||
})
|
||||
}
|
||||
const self = this
|
||||
return Effect.map(this.customIterator(value, node, false), (iterator) =>
|
||||
iterator === undefined
|
||||
? undefined
|
||||
: {
|
||||
next: self.nextIteratorResult(iterator, node, false),
|
||||
close: Effect.suspend(() => self.closeIterator(iterator, node, false)),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private customIterator(value: unknown, node: AstNode | undefined, allowAsync = true) {
|
||||
@@ -1310,7 +1298,7 @@ class Frame<R> {
|
||||
// `new` itself is supported, so a non-constructible callee is a TypeError like JS rather than
|
||||
// unsupported syntax. Built-ins like Number are real constructors in JS, so do not claim
|
||||
// otherwise; say `new` is unsupported for them and point at the plain call.
|
||||
const name = calleeDescription(node.callee) ?? "The called value"
|
||||
const name = calleeDescription(node.callee)
|
||||
const message =
|
||||
callee instanceof Fn
|
||||
? `${name} cannot be constructed: user-defined constructors and classes are not supported. Call it as a function that returns a plain object instead.`
|
||||
@@ -1613,7 +1601,7 @@ class Frame<R> {
|
||||
if (callable instanceof Native) {
|
||||
return yield* self.native(() => (callable as Native<R>).call(thisValue, args), node)
|
||||
}
|
||||
throw typeError(`${calleeDescription(callee) ?? "The called value"} is not a function.`, callee ?? node)
|
||||
throw typeError(`${calleeDescription(callee)} is not a function.`, callee ?? node)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2076,14 +2064,16 @@ class Frame<R> {
|
||||
|
||||
if (objectValue instanceof Obj) return { target: objectValue, key, receiver: objectValue }
|
||||
|
||||
// Strings own length and indexes; every other primitive property reads through the wrapper prototype.
|
||||
// Primitives read through their wrapper prototype without being boxed; strings own length and indexes.
|
||||
const builtins = self.ctx.builtins
|
||||
if (typeof objectValue === "string") {
|
||||
if (key === "length") return { value: objectValue.length }
|
||||
const index = typeof key === "symbol" ? undefined : parseArrayIndex(key)
|
||||
if (index !== undefined) return { value: objectValue[index] }
|
||||
return { target: builtins.String, key, receiver: objectValue }
|
||||
}
|
||||
const proto = primitivePrototype(self.ctx.builtins, objectValue)
|
||||
if (proto !== undefined) return { target: proto, key, receiver: objectValue }
|
||||
if (typeof objectValue === "number") return { target: builtins.Number, key, receiver: objectValue }
|
||||
if (typeof objectValue === "boolean") return { target: builtins.Boolean, key, receiver: objectValue }
|
||||
|
||||
if (objectValue === null || objectValue === undefined) {
|
||||
throw typeError(`Cannot read properties of ${objectValue} (reading '${String(key)}').`, objectNode)
|
||||
|
||||
@@ -52,14 +52,6 @@ export const createErrorValue = (prototype: Obj, message: string | undefined): E
|
||||
return value
|
||||
}
|
||||
|
||||
/** The prototype a primitive reads its methods from without being boxed; none for null, undefined, and symbols. */
|
||||
export const primitivePrototype = (builtins: Builtins, value: unknown): Obj | undefined => {
|
||||
if (typeof value === "string") return builtins.String
|
||||
if (typeof value === "number") return builtins.Number
|
||||
if (typeof value === "boolean") return builtins.Boolean
|
||||
return undefined
|
||||
}
|
||||
|
||||
export const createBuiltins = (): Builtins => {
|
||||
const object = new Obj(null)
|
||||
// Function.prototype is itself callable and returns undefined.
|
||||
|
||||
@@ -86,10 +86,9 @@ export const unsupportedSyntax = (kind: string, node: AstNode): PendingThrow =>
|
||||
export const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
typeof value === "object" && value !== null
|
||||
|
||||
// Acorn lines are 1-based and its columns are 0-based. Diagnostics use 1-based columns of the submitted source.
|
||||
export const sourceLocation = (node: AstNode): { readonly line: number; readonly column: number } => ({
|
||||
line: node.loc?.start.line ?? 1,
|
||||
column: (node.loc?.start.column ?? 0) + 1,
|
||||
line: Math.max(1, (node.loc?.start.line ?? 2) - 1),
|
||||
column: Math.max(1, (node.loc?.start.column ?? 4) - 3),
|
||||
})
|
||||
|
||||
export const formatLocation = (node?: AstNode): string => {
|
||||
|
||||
@@ -122,16 +122,6 @@ export class GeneratorObj extends Obj {
|
||||
}
|
||||
}
|
||||
|
||||
/** A built-in collection iterator: live over the host collection, yielding program values. */
|
||||
export class IteratorObj extends Obj {
|
||||
constructor(
|
||||
proto: Obj,
|
||||
readonly iterator: IteratorObject<unknown>,
|
||||
) {
|
||||
super(proto)
|
||||
}
|
||||
}
|
||||
|
||||
export class DateObj extends Obj {
|
||||
constructor(
|
||||
proto: Obj,
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
Bytes,
|
||||
DateObj,
|
||||
GeneratorObj,
|
||||
IteratorObj,
|
||||
MapObj,
|
||||
Obj,
|
||||
PromiseObj,
|
||||
@@ -24,7 +23,6 @@ import {
|
||||
export const isRuntimeReference = (value: unknown): boolean =>
|
||||
value instanceof Callable ||
|
||||
value instanceof GeneratorObj ||
|
||||
value instanceof IteratorObj ||
|
||||
value instanceof ToolReference ||
|
||||
value instanceof PromiseObj ||
|
||||
isWrapper(value)
|
||||
@@ -91,7 +89,6 @@ export const describeValue = (value: unknown): string => {
|
||||
if (value instanceof HeadersObj) return "a Headers"
|
||||
if (value instanceof Bytes) return "a Uint8Array"
|
||||
if (value instanceof GeneratorObj) return "a generator"
|
||||
if (value instanceof IteratorObj) return "an iterator"
|
||||
if (isRuntimeReference(value)) return "a function"
|
||||
if (typeof value === "object") return "a data object"
|
||||
return `a ${typeof value}`
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Effect } from "effect"
|
||||
import { constructor, type Method, methods, prototypeFrom, receiver } from "../interpreter/native.js"
|
||||
import { checkArrayLength, checkStringLength, MAX_ARRAY_LENGTH } from "../interpreter/limits.js"
|
||||
import { invalidData, IteratorSymbol, rangeError, typeError } from "../interpreter/model.js"
|
||||
import { define, get, hidden, Arr, GeneratorObj, IteratorObj, Obj } from "../interpreter/objects.js"
|
||||
import { invalidData, rangeError, typeError } from "../interpreter/model.js"
|
||||
import { get, Arr, GeneratorObj, Obj } from "../interpreter/objects.js"
|
||||
import { describeValue, rejectCircularInsertion } from "../interpreter/references.js"
|
||||
import { applyCollectionCallback, invoke, preserveConsumerError } from "../interpreter/callback.js"
|
||||
import { applyCollectionCallback, preserveConsumerError } from "../interpreter/callback.js"
|
||||
import type { Interpreter } from "../interpreter/interpreter.js"
|
||||
import { compareText } from "../tool-runtime.js"
|
||||
import { coerceToNumber, coerceToString } from "./value.js"
|
||||
@@ -340,31 +340,13 @@ export const arrayGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
return target
|
||||
},
|
||||
],
|
||||
[
|
||||
"toLocaleString",
|
||||
0,
|
||||
(thisValue) =>
|
||||
Effect.map(
|
||||
Effect.forEach(self(thisValue, "toLocaleString").items, (item) =>
|
||||
item === null || item === undefined
|
||||
? Effect.succeed("")
|
||||
: Effect.map(invoke(ctx, item, "toLocaleString", "Array.prototype.toLocaleString"), coerceToString),
|
||||
),
|
||||
(parts) => parts.join(","),
|
||||
),
|
||||
],
|
||||
["keys", 0, (thisValue) => new IteratorObj(builtins.Iterator, self(thisValue, "keys").items.keys())],
|
||||
["values", 0, (thisValue) => new IteratorObj(builtins.Iterator, self(thisValue, "values").items.values())],
|
||||
["keys", 0, (thisValue) => wrap(Array.from(self(thisValue, "keys").items.keys()))],
|
||||
["values", 0, (thisValue) => wrap([...self(thisValue, "values").items])],
|
||||
[
|
||||
"entries",
|
||||
0,
|
||||
(thisValue) =>
|
||||
new IteratorObj(
|
||||
builtins.Iterator,
|
||||
self(thisValue, "entries")
|
||||
.items.entries()
|
||||
.map(([index, item]) => wrap([index, item])),
|
||||
),
|
||||
wrap(Array.from(self(thisValue, "entries").items.entries(), ([index, item]) => wrap([index, item]))),
|
||||
],
|
||||
iterate("map", 1, (target, receiver, apply) =>
|
||||
Effect.gen(function* () {
|
||||
@@ -508,6 +490,5 @@ export const arrayGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
}),
|
||||
),
|
||||
])
|
||||
define(proto, IteratorSymbol, get(proto, "values"), hidden)
|
||||
return array
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Effect } from "effect"
|
||||
import { checkArrayLength, checkStringLength } from "../interpreter/limits.js"
|
||||
import { constructor, methods, prototypeFrom, receiver, requiresNew } from "../interpreter/native.js"
|
||||
import { IteratorSymbol, rangeError, syntaxError, typeError } from "../interpreter/model.js"
|
||||
import { define, defineAccessor, get, hidden, Arr, Bytes, IteratorObj, Obj } from "../interpreter/objects.js"
|
||||
import { rangeError, syntaxError, typeError } from "../interpreter/model.js"
|
||||
import { defineAccessor, get, Arr, Bytes, Obj } from "../interpreter/objects.js"
|
||||
import { describeValue } from "../interpreter/references.js"
|
||||
import type { Interpreter } from "../interpreter/interpreter.js"
|
||||
import { coerceToNumber, coerceToString } from "./value.js"
|
||||
@@ -173,21 +173,15 @@ export const uint8ArrayGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
["toString", 0, (thisValue) => self(thisValue, "toString").bytes.join(",")],
|
||||
["toBase64", 0, (thisValue) => self(thisValue, "toBase64").bytes.toBase64()],
|
||||
["toHex", 0, (thisValue) => self(thisValue, "toHex").bytes.toHex()],
|
||||
["keys", 0, (thisValue) => new IteratorObj(builtins.Iterator, self(thisValue, "keys").bytes.keys())],
|
||||
["values", 0, (thisValue) => new IteratorObj(builtins.Iterator, self(thisValue, "values").bytes.values())],
|
||||
["keys", 0, (thisValue) => wrapAll(Array.from(self(thisValue, "keys").bytes.keys()))],
|
||||
["values", 0, (thisValue) => wrapAll(Array.from(self(thisValue, "values").bytes.values()))],
|
||||
[
|
||||
"entries",
|
||||
0,
|
||||
(thisValue) =>
|
||||
new IteratorObj(
|
||||
builtins.Iterator,
|
||||
self(thisValue, "entries")
|
||||
.bytes.entries()
|
||||
.map(([index, byte]) => wrapAll([index, byte])),
|
||||
),
|
||||
wrapAll(Array.from(self(thisValue, "entries").bytes.entries(), ([index, byte]) => wrapAll([index, byte]))),
|
||||
],
|
||||
])
|
||||
define(proto, IteratorSymbol, get(proto, "values"), hidden)
|
||||
return uint8Array
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Effect } from "effect"
|
||||
import { constructor, fn, type Method, methods, prototypeFrom, receiver, requiresNew } from "../interpreter/native.js"
|
||||
import { invalidData, IteratorSymbol, typeError } from "../interpreter/model.js"
|
||||
import { invalidData, typeError } from "../interpreter/model.js"
|
||||
import {
|
||||
define,
|
||||
defineAccessor,
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
hidden,
|
||||
isWrapper,
|
||||
Arr,
|
||||
IteratorObj,
|
||||
MapObj,
|
||||
Obj,
|
||||
PromiseObj,
|
||||
@@ -145,29 +144,6 @@ export const mapGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
return target
|
||||
},
|
||||
],
|
||||
[
|
||||
"getOrInsert",
|
||||
2,
|
||||
(thisValue, args) => {
|
||||
const target = self(thisValue, "getOrInsert").map
|
||||
if (!target.has(args[0])) target.set(args[0], args[1])
|
||||
return target.get(args[0])
|
||||
},
|
||||
],
|
||||
[
|
||||
"getOrInsertComputed",
|
||||
2,
|
||||
(thisValue, args) => {
|
||||
const target = self(thisValue, "getOrInsertComputed").map
|
||||
const apply = applyCollectionCallback(ctx, args[1], "Map.getOrInsertComputed")
|
||||
if (target.has(args[0])) return target.get(args[0])
|
||||
// The callback sees the stored key (-0 is +0) and its result wins over anything it inserted itself.
|
||||
return Effect.map(apply([args[0] === 0 ? 0 : args[0]]), (value) => {
|
||||
target.set(args[0], value)
|
||||
return value
|
||||
})
|
||||
},
|
||||
],
|
||||
["delete", 1, (thisValue, args) => self(thisValue, "delete").map.delete(args[0])],
|
||||
[
|
||||
"clear",
|
||||
@@ -177,18 +153,12 @@ export const mapGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
return undefined
|
||||
},
|
||||
],
|
||||
["keys", 0, (thisValue) => new IteratorObj(builtins.Iterator, self(thisValue, "keys").map.keys())],
|
||||
["values", 0, (thisValue) => new IteratorObj(builtins.Iterator, self(thisValue, "values").map.values())],
|
||||
["keys", 0, (thisValue) => wrap(Array.from(self(thisValue, "keys").map.keys()))],
|
||||
["values", 0, (thisValue) => wrap(Array.from(self(thisValue, "values").map.values()))],
|
||||
[
|
||||
"entries",
|
||||
0,
|
||||
(thisValue) =>
|
||||
new IteratorObj(
|
||||
builtins.Iterator,
|
||||
self(thisValue, "entries")
|
||||
.map.entries()
|
||||
.map(([key, item]) => wrap([key, item])),
|
||||
),
|
||||
(thisValue) => wrap(Array.from(self(thisValue, "entries").map.entries(), ([key, item]) => wrap([key, item]))),
|
||||
],
|
||||
[
|
||||
"forEach",
|
||||
@@ -203,7 +173,6 @@ export const mapGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
},
|
||||
],
|
||||
])
|
||||
define(proto, IteratorSymbol, get(proto, "entries"), hidden)
|
||||
return map
|
||||
}
|
||||
|
||||
@@ -249,8 +218,7 @@ const loadSetRecord = <R>(
|
||||
size: Math.max(Math.trunc(size), 0),
|
||||
has: (item: unknown) => Effect.map(ctx.call(has, source, [item]), Boolean),
|
||||
keys: () =>
|
||||
Effect.flatMap(ctx.call(keys, source, []), (result): Effect.Effect<Iterable<unknown>> => {
|
||||
if (result instanceof IteratorObj) return Effect.succeed(result.iterator)
|
||||
Effect.flatMap(ctx.call(keys, source, []), (result) => {
|
||||
if (result instanceof Arr) return Effect.succeed(result.items)
|
||||
throw typeError(`Set.${name} expected 'keys' to return an iterator.`)
|
||||
}),
|
||||
@@ -370,18 +338,12 @@ export const setGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
return undefined
|
||||
},
|
||||
],
|
||||
["keys", 0, (thisValue) => new IteratorObj(builtins.Iterator, self(thisValue, "keys").set.values())],
|
||||
["values", 0, (thisValue) => new IteratorObj(builtins.Iterator, self(thisValue, "values").set.values())],
|
||||
["keys", 0, (thisValue) => wrap(Array.from(self(thisValue, "keys").set.values()))],
|
||||
["values", 0, (thisValue) => wrap(Array.from(self(thisValue, "values").set.values()))],
|
||||
[
|
||||
"entries",
|
||||
0,
|
||||
(thisValue) =>
|
||||
new IteratorObj(
|
||||
builtins.Iterator,
|
||||
self(thisValue, "entries")
|
||||
.set.values()
|
||||
.map((item) => wrap([item, item])),
|
||||
),
|
||||
(thisValue) => wrap(Array.from(self(thisValue, "entries").set.values(), (item) => wrap([item, item]))),
|
||||
],
|
||||
[
|
||||
"forEach",
|
||||
@@ -403,6 +365,5 @@ export const setGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
operation("isSupersetOf"),
|
||||
operation("isDisjointFrom"),
|
||||
])
|
||||
define(proto, IteratorSymbol, get(proto, "values"), hidden)
|
||||
return set
|
||||
}
|
||||
|
||||
@@ -105,13 +105,6 @@ export const dateGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
["toTimeString", 0, (thisValue) => new Date(self(thisValue, "toTimeString").time).toTimeString()],
|
||||
["toUTCString", 0, (thisValue) => new Date(self(thisValue, "toUTCString").time).toUTCString()],
|
||||
["toGMTString", 0, (thisValue) => new Date(self(thisValue, "toGMTString").time).toUTCString()],
|
||||
...(["toLocaleString", "toLocaleDateString", "toLocaleTimeString"] as const).map(
|
||||
(name): Method => [
|
||||
name,
|
||||
0,
|
||||
(thisValue) => new Date(self(thisValue, name).time)[name]("en-US", { timeZone: "UTC" }),
|
||||
],
|
||||
),
|
||||
...getters.map((name): Method => [name, 0, (thisValue) => new Date(self(thisValue, name).time)[name]()]),
|
||||
...setters.map(
|
||||
([name, length]): Method => [
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import { constructor, methods, prototypeFrom, receiver, requiresNew } from "../interpreter/native.js"
|
||||
import { IteratorSymbol, typeError } from "../interpreter/model.js"
|
||||
import { define, entries, get, hidden, Arr, HeadersObj, IteratorObj, Obj } from "../interpreter/objects.js"
|
||||
import { typeError } from "../interpreter/model.js"
|
||||
import { entries, Arr, HeadersObj, Obj } from "../interpreter/objects.js"
|
||||
import { applyCollectionCallback } from "../interpreter/callback.js"
|
||||
import { isRuntimeReference } from "../interpreter/references.js"
|
||||
import type { Interpreter } from "../interpreter/interpreter.js"
|
||||
@@ -93,25 +93,13 @@ export const headersGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
return attempt(() => target.set(arg(args, 0), arg(args, 1)))
|
||||
},
|
||||
],
|
||||
// Iterator.from because Bun's Headers typings predate iterator helpers; the runtime iterators already have them.
|
||||
[
|
||||
"keys",
|
||||
0,
|
||||
(thisValue) => new IteratorObj(builtins.Iterator, Iterator.from(self(thisValue, "keys").headers.keys())),
|
||||
],
|
||||
[
|
||||
"values",
|
||||
0,
|
||||
(thisValue) => new IteratorObj(builtins.Iterator, Iterator.from(self(thisValue, "values").headers.values())),
|
||||
],
|
||||
["keys", 0, (thisValue) => wrap(Array.from(self(thisValue, "keys").headers.keys()))],
|
||||
["values", 0, (thisValue) => wrap(Array.from(self(thisValue, "values").headers.values()))],
|
||||
[
|
||||
"entries",
|
||||
0,
|
||||
(thisValue) =>
|
||||
new IteratorObj(
|
||||
builtins.Iterator,
|
||||
Iterator.from(self(thisValue, "entries").headers.entries()).map(([key, value]) => wrap([key, value])),
|
||||
),
|
||||
wrap(Array.from(self(thisValue, "entries").headers.entries(), ([key, value]) => wrap([key, value]))),
|
||||
],
|
||||
[
|
||||
"forEach",
|
||||
@@ -127,6 +115,5 @@ export const headersGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
},
|
||||
],
|
||||
])
|
||||
define(proto, IteratorSymbol, get(proto, "entries"), hidden)
|
||||
return headers
|
||||
}
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import { methods, receiver } from "../interpreter/native.js"
|
||||
import { IteratorObj, record } from "../interpreter/objects.js"
|
||||
import type { Interpreter } from "../interpreter/interpreter.js"
|
||||
|
||||
// Every built-in collection iterator shares the Iterator prototype; JS gives each collection its own, which is only
|
||||
// observable through getPrototypeOf.
|
||||
export const iteratorGlobals = <R>(ctx: Interpreter<R>): void => {
|
||||
const builtins = ctx.builtins
|
||||
methods(builtins, builtins.Iterator, [
|
||||
[
|
||||
"next",
|
||||
0,
|
||||
(thisValue) => {
|
||||
const step = receiver(IteratorObj, thisValue, "Iterator.prototype.next").iterator.next()
|
||||
return record(builtins.Object, { value: step.value, done: Boolean(step.done) })
|
||||
},
|
||||
],
|
||||
])
|
||||
}
|
||||
@@ -50,7 +50,6 @@ export const numberGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
}
|
||||
methods(builtins, builtins.Number, [
|
||||
["toFixed", 1, (thisValue, args) => self(thisValue, "toFixed").toFixed(optNum("toFixed", args[0]))],
|
||||
["toLocaleString", 0, (thisValue) => self(thisValue, "toLocaleString").toLocaleString("en-US")],
|
||||
[
|
||||
"toExponential",
|
||||
1,
|
||||
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
set,
|
||||
} from "../interpreter/objects.js"
|
||||
import { containsOpaqueReference, describeValue, rejectCircularInsertion } from "../interpreter/references.js"
|
||||
import { invoke, preserveConsumerError } from "../interpreter/callback.js"
|
||||
import { preserveConsumerError } from "../interpreter/callback.js"
|
||||
import type { Interpreter } from "../interpreter/interpreter.js"
|
||||
import { ToolReference } from "../tool-runtime.js"
|
||||
import { groupBy } from "./collections.js"
|
||||
@@ -196,7 +196,7 @@ export const objectGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
true,
|
||||
],
|
||||
["toString", 0, (thisValue) => `[object ${classTag(thisValue)}]`],
|
||||
["toLocaleString", 0, (thisValue) => invoke(ctx, thisValue, "toString", "Object.prototype.toLocaleString")],
|
||||
["toLocaleString", 0, (thisValue) => `[object ${classTag(thisValue)}]`],
|
||||
[
|
||||
"valueOf",
|
||||
0,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Effect } from "effect"
|
||||
import { constructor, fn, type Method, methods } from "../interpreter/native.js"
|
||||
import { constructor, type Method, methods } from "../interpreter/native.js"
|
||||
import { checkArrayLength, checkStringLength } from "../interpreter/limits.js"
|
||||
import { invalidData, IteratorSymbol, rangeError, typeError } from "../interpreter/model.js"
|
||||
import { define, hidden, Arr, IteratorObj, PromiseObj, RegExpObj, record } from "../interpreter/objects.js"
|
||||
import { invalidData, rangeError, typeError } from "../interpreter/model.js"
|
||||
import { Arr, PromiseObj, RegExpObj, record } from "../interpreter/objects.js"
|
||||
import { containsOpaqueReference, typeofValue } from "../interpreter/references.js"
|
||||
import { applyCollectionCallback, isSupportedCallback } from "../interpreter/callback.js"
|
||||
import type { Interpreter } from "../interpreter/interpreter.js"
|
||||
@@ -150,8 +150,6 @@ export const stringGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
simple("valueOf", 0, (value) => value),
|
||||
simple("toLowerCase", 0, (value) => value.toLowerCase()),
|
||||
simple("toUpperCase", 0, (value) => value.toUpperCase()),
|
||||
simple("toLocaleLowerCase", 0, (value) => value.toLowerCase()),
|
||||
simple("toLocaleUpperCase", 0, (value) => value.toUpperCase()),
|
||||
simple("trim", 0, (value) => value.trim()),
|
||||
simple("trimStart", 0, (value) => value.trimStart()),
|
||||
simple("trimLeft", 0, (value) => value.trimStart()),
|
||||
@@ -260,16 +258,5 @@ export const stringGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
return joined
|
||||
}),
|
||||
])
|
||||
define(
|
||||
builtins.String,
|
||||
IteratorSymbol,
|
||||
fn(
|
||||
builtins,
|
||||
"[Symbol.iterator]",
|
||||
0,
|
||||
(thisValue) => new IteratorObj(builtins.Iterator, self(thisValue, "[Symbol.iterator]")[Symbol.iterator]()),
|
||||
),
|
||||
hidden,
|
||||
)
|
||||
return string
|
||||
}
|
||||
|
||||
@@ -1,19 +1,7 @@
|
||||
import { Effect } from "effect"
|
||||
import { constructor, fn, type Method, methods, prototypeFrom, receiver, requiresNew } from "../interpreter/native.js"
|
||||
import { IteratorSymbol, PendingThrow, typeError, uriError } from "../interpreter/model.js"
|
||||
import {
|
||||
define,
|
||||
defineAccessor,
|
||||
entries,
|
||||
get,
|
||||
hidden,
|
||||
isWrapper,
|
||||
Arr,
|
||||
IteratorObj,
|
||||
Obj,
|
||||
URLObj,
|
||||
URLSearchParamsObj,
|
||||
} from "../interpreter/objects.js"
|
||||
import { PendingThrow, typeError, uriError } from "../interpreter/model.js"
|
||||
import { defineAccessor, entries, isWrapper, Arr, Obj, URLObj, URLSearchParamsObj } from "../interpreter/objects.js"
|
||||
import { isRuntimeReference } from "../interpreter/references.js"
|
||||
import { applyCollectionCallback, preserveConsumerError } from "../interpreter/callback.js"
|
||||
import type { Interpreter } from "../interpreter/interpreter.js"
|
||||
@@ -270,18 +258,13 @@ export const urlSearchParamsGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
return undefined
|
||||
},
|
||||
],
|
||||
["keys", 0, (thisValue) => new IteratorObj(builtins.Iterator, self(thisValue, "keys").params.keys())],
|
||||
["values", 0, (thisValue) => new IteratorObj(builtins.Iterator, self(thisValue, "values").params.values())],
|
||||
["keys", 0, (thisValue) => wrap(Array.from(self(thisValue, "keys").params.keys()))],
|
||||
["values", 0, (thisValue) => wrap(Array.from(self(thisValue, "values").params.values()))],
|
||||
[
|
||||
"entries",
|
||||
0,
|
||||
(thisValue) =>
|
||||
new IteratorObj(
|
||||
builtins.Iterator,
|
||||
self(thisValue, "entries")
|
||||
.params.entries()
|
||||
.map(([key, value]) => wrap([key, value])),
|
||||
),
|
||||
wrap(Array.from(self(thisValue, "entries").params.entries(), ([key, value]) => wrap([key, value]))),
|
||||
],
|
||||
["toString", 0, (thisValue) => self(thisValue, "toString").params.toString()],
|
||||
[
|
||||
@@ -298,6 +281,5 @@ export const urlSearchParamsGlobal = <R>(ctx: Interpreter<R>) => {
|
||||
},
|
||||
],
|
||||
])
|
||||
define(proto, IteratorSymbol, get(proto, "entries"), hidden)
|
||||
return searchParams
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { JsonPointer, Schema, SchemaAST } from "effect"
|
||||
import { JsonPointer, Schema } from "effect"
|
||||
import type { Tool, JsonSchema, SchemaType } from "./tool.js"
|
||||
|
||||
const isEffectSchema = (schema: SchemaType): schema is Schema.Decoder<unknown> & Schema.Top => Schema.isSchema(schema)
|
||||
@@ -203,7 +203,7 @@ const renderSchema = (
|
||||
export const toTypeScript = (schema: Schema.Top, decoded = false, pretty = false): string => {
|
||||
try {
|
||||
const visible = decoded ? Schema.toType(schema) : schema
|
||||
const document = Schema.toJsonSchemaDocument(visible, { onExcessProperty: "error" }) as {
|
||||
const document = Schema.toJsonSchemaDocument(visible) as {
|
||||
readonly schema: JsonSchema
|
||||
readonly definitions?: Readonly<Record<string, JsonSchema>>
|
||||
}
|
||||
@@ -230,7 +230,7 @@ export type InputProperty = {
|
||||
export const inputProperties = <R>(tool: Tool<R>): Array<InputProperty> => {
|
||||
try {
|
||||
const document = isEffectSchema(tool.input)
|
||||
? (Schema.toJsonSchemaDocument(tool.input, { onExcessProperty: "error" }) as {
|
||||
? (Schema.toJsonSchemaDocument(tool.input) as {
|
||||
readonly schema: JsonSchema
|
||||
readonly definitions?: Readonly<Record<string, JsonSchema>>
|
||||
})
|
||||
@@ -261,16 +261,10 @@ export const inputProperties = <R>(tool: Tool<R>): Array<InputProperty> => {
|
||||
export const inputTypeScript = <R>(tool: Tool<R>, pretty = false): string =>
|
||||
isEffectSchema(tool.input) ? toTypeScript(tool.input, false, pretty) : jsonSchemaToTypeScript(tool.input, pretty)
|
||||
|
||||
// Effect 4.0 models an empty Struct as a non-nullish object, so inspect its AST.
|
||||
// Raw JSON Schema inputs retain the compact `{}` rendering check.
|
||||
export const isEmptyInput = <R>(tool: Tool<R>): boolean => {
|
||||
if (!isEffectSchema(tool.input)) return inputTypeScript(tool) === "{}"
|
||||
return (
|
||||
SchemaAST.isObjects(tool.input.ast) &&
|
||||
tool.input.ast.propertySignatures.length === 0 &&
|
||||
tool.input.ast.indexSignatures.length === 0
|
||||
)
|
||||
}
|
||||
// Empty object schemas render as `{}` in compact form; anything with properties,
|
||||
// an index signature, or union members renders differently, so equality is a
|
||||
// conservative emptiness test for both Effect and JSON Schema inputs.
|
||||
export const isEmptyInput = <R>(tool: Tool<R>): boolean => inputTypeScript(tool) === "{}"
|
||||
|
||||
export const outputTypeScript = <R>(tool: Tool<R>, pretty = false): string =>
|
||||
tool.output === undefined
|
||||
|
||||
@@ -298,18 +298,21 @@ const cases = [
|
||||
},
|
||||
{
|
||||
path: "test/built-ins/Array/prototype/keys/iteration.js",
|
||||
code: `const it = ["a", "b", "c"].keys(); return [it.next(), it.next(), it.next(), it.next()]`,
|
||||
expected: [{ value: 0, done: false }, { value: 1, done: false }, { value: 2, done: false }, { done: true }],
|
||||
code: `return ["a", "b", "c"].keys()`,
|
||||
expected: [0, 1, 2],
|
||||
},
|
||||
{
|
||||
path: "test/built-ins/Array/prototype/values/iteration.js",
|
||||
code: `const it = ["a", "b", "c"].values(); return [it.next(), it.next(), it.next(), it.next()]`,
|
||||
expected: [{ value: "a", done: false }, { value: "b", done: false }, { value: "c", done: false }, { done: true }],
|
||||
code: `return ["a", "b", "c"].values()`,
|
||||
expected: ["a", "b", "c"],
|
||||
},
|
||||
{
|
||||
path: "test/built-ins/Array/prototype/entries/iteration.js",
|
||||
code: `const it = ["a", "b"].entries(); return [it.next(), it.next(), it.next()]`,
|
||||
expected: [{ value: [0, "a"], done: false }, { value: [1, "b"], done: false }, { done: true }],
|
||||
code: `return ["a", "b"].entries()`,
|
||||
expected: [
|
||||
[0, "a"],
|
||||
[1, "b"],
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "test/built-ins/Array/isArray/15.4.3.2-0-3.js",
|
||||
|
||||
@@ -22,16 +22,6 @@ describe("source syntax", () => {
|
||||
})
|
||||
|
||||
describe("error identity", () => {
|
||||
test("Error.isError is true for every Error value and nothing else", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
const caught = (() => { try { null.foo } catch (error) { return error } })()
|
||||
return [Error.isError(new Error("x")), Error.isError(new RangeError("x")), Error.isError(caught),
|
||||
Error.isError({ name: "Error", message: "x" }), Error.isError("Error"), Error.isError(null)]
|
||||
`),
|
||||
).toEqual([true, true, true, false, false, false])
|
||||
})
|
||||
|
||||
test("awaiting the same rejected promise twice yields the same error object", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
@@ -105,24 +95,6 @@ describe("uncaught program throws", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("source locations", () => {
|
||||
test("uses the submitted line and 1-based column", async () => {
|
||||
const failure = await error("const value = 1\nreturn value()")
|
||||
expect(failure.location).toEqual({ line: 2, column: 8 })
|
||||
expect(failure.message).toBe("TypeError: value is not a function. (line 2, col 8)")
|
||||
})
|
||||
|
||||
test("names a missing method on a call instead of the previous line", async () => {
|
||||
const failure = await error(`// Try search with different namespaces
|
||||
for (const ns of ["github", "tools.github", "tools", ""]) {
|
||||
const s = await search({query: "star", namespace: ns, limit: 100}).catch(e=>({items:[],error:String(e)}));
|
||||
return s
|
||||
}`)
|
||||
expect(failure.location).toEqual({ line: 3, column: 19 })
|
||||
expect(failure.message).toBe("TypeError: search(...).catch is not a function. (line 3, col 19)")
|
||||
})
|
||||
})
|
||||
|
||||
describe("host errors escaping built-ins", () => {
|
||||
test("become the same-named program error", async () => {
|
||||
expect(
|
||||
@@ -135,7 +107,7 @@ describe("host errors escaping built-ins", () => {
|
||||
test("report the location of the call that raised them", async () => {
|
||||
const failure = await error(`return [1].map((n) => n.toFixed(200))`)
|
||||
expect(failure.kind).toBe("ExecutionFailure")
|
||||
expect(failure.message).toBe("RangeError: toFixed() argument must be between 0 and 100 (line 1, col 23)")
|
||||
expect(failure.message).toBe("RangeError: toFixed() argument must be between 0 and 100 (line 1, col 19)")
|
||||
})
|
||||
|
||||
test("a built-in that rejects its arguments before doing any work is located at the call", async () => {
|
||||
@@ -143,13 +115,13 @@ describe("host errors escaping built-ins", () => {
|
||||
})
|
||||
|
||||
test("a rejection born inside a promise the built-in created is located at the creating call", async () => {
|
||||
expect((await error(`return await Promise.all(1)`)).message).toEndWith("(line 1, col 14)")
|
||||
expect((await error(`return await Promise.race([])`)).message).toEndWith("(line 1, col 14)")
|
||||
expect((await error(`return await Promise.all(1)`)).message).toEndWith("(line 1, col 10)")
|
||||
expect((await error(`return await Promise.race([])`)).message).toEndWith("(line 1, col 10)")
|
||||
expect((await error(`return await Promise.all({ [Symbol.iterator]: () => ({ next: 1 }) })`)).message).toEndWith(
|
||||
"(line 1, col 14)",
|
||||
"(line 1, col 10)",
|
||||
)
|
||||
expect((await error(`let p; p = Promise.resolve().then(() => p); return await p`)).message).toEndWith(
|
||||
"(line 1, col 12)",
|
||||
"(line 1, col 8)",
|
||||
)
|
||||
})
|
||||
|
||||
@@ -162,7 +134,7 @@ describe("host errors escaping built-ins", () => {
|
||||
|
||||
test("a failure inside a built-in called by another built-in is located at the outer call", async () => {
|
||||
const failure = await error(`return Array.from({ [Symbol.iterator]: () => ({ next: 1 }) })`)
|
||||
expect(failure.message).toBe("TypeError: Iterator next must be a function. (line 1, col 8)")
|
||||
expect(failure.message).toBe("TypeError: Iterator next must be a function. (line 1, col 4)")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -181,7 +153,7 @@ describe("call depth", () => {
|
||||
test("uncaught overflow reports the call that overflowed", async () => {
|
||||
const failure = await error(`const f = (n) => f(n + 1); return f(0)`)
|
||||
expect(failure.kind).toBe("ExecutionFailure")
|
||||
expect(failure.message).toBe("RangeError: Maximum call stack size exceeded (line 1, col 18)")
|
||||
expect(failure.message).toBe("RangeError: Maximum call stack size exceeded (line 1, col 14)")
|
||||
})
|
||||
|
||||
test("the limit is 10000 nested calls", async () => {
|
||||
|
||||
@@ -220,9 +220,8 @@ describe("values are converted at the boundary, never shared", () => {
|
||||
expect(held[1]).toBeInstanceOf(Error)
|
||||
})
|
||||
|
||||
test("functions, promises, iterators, and symbols cannot be passed in", async () => {
|
||||
test("functions, promises, and symbols cannot be passed in", async () => {
|
||||
expect((await failure(`keep(() => 1)`)).message).toContain("Argument 1 to keep contains a function")
|
||||
expect((await failure(`keep([1].keys())`)).message).toContain("Argument 1 to keep contains an iterator")
|
||||
expect((await failure(`keep(later(1))`)).message).toContain("un-awaited Promise")
|
||||
expect((await failure(`keep(Symbol.iterator)`)).message).toContain("Argument 1 to keep contains a symbol")
|
||||
})
|
||||
|
||||
@@ -122,10 +122,10 @@ describe("Map.groupBy Test262 parity", () => {
|
||||
grouped.get(1),
|
||||
grouped.get("1"),
|
||||
grouped.has(stringable),
|
||||
[...grouped.keys()].length,
|
||||
grouped.keys().length,
|
||||
parity.get("even"),
|
||||
parity.get("odd"),
|
||||
[...lengths.keys()],
|
||||
lengths.keys(),
|
||||
lengths.get(5),
|
||||
lengths.get(4),
|
||||
]
|
||||
@@ -162,7 +162,7 @@ describe("Map.groupBy Test262 parity", () => {
|
||||
await value(`
|
||||
const grouped = Map.groupBy("🥰💩🙏😈", (char) => char < "🙏" ? "before" : "after")
|
||||
const empty = Map.groupBy([], () => { throw new Error("not called") })
|
||||
return [[...grouped.keys()], grouped.get("before"), grouped.get("after"), empty.size]
|
||||
return [grouped.keys(), grouped.get("before"), grouped.get("after"), empty.size]
|
||||
`),
|
||||
).toEqual([["after", "before"], ["💩", "😈"], ["🥰", "🙏"], 0])
|
||||
})
|
||||
|
||||
@@ -502,15 +502,9 @@ describe("CodeMode-specific array behavior", () => {
|
||||
expect(err.message).toContain("circular")
|
||||
})
|
||||
|
||||
test("indexOf and lastIndexOf with no argument search for undefined", async () => {
|
||||
expect(await value(`return [1, undefined, 3].indexOf()`)).toBe(1)
|
||||
expect(await value(`return [1, undefined, 3].lastIndexOf()`)).toBe(1)
|
||||
expect(await value(`return [1, 2, 3].indexOf()`)).toBe(-1)
|
||||
})
|
||||
|
||||
test("keys/values/entries return iterators usable with for...of and spread", async () => {
|
||||
test("keys/values/entries return arrays usable with for...of and spread", async () => {
|
||||
expect(await value(`return [...["x","y","z"].keys()]`)).toEqual([0, 1, 2])
|
||||
expect(await value(`return [...["x","y"].values()]`)).toEqual(["x", "y"])
|
||||
expect(await value(`return ["x","y"].values()`)).toEqual(["x", "y"])
|
||||
expect(
|
||||
await value(`
|
||||
const out = []
|
||||
@@ -946,12 +940,6 @@ describe("coercion parity: unknown static members read as undefined", () => {
|
||||
expect(await value(`try { JSON.rawJSON("1") } catch (e) { return e.message }`)).toBe(
|
||||
"JSON.rawJSON is not a function.",
|
||||
)
|
||||
expect(await value(`try { search({ query: "star" }).catch(() => 1) } catch (e) { return e.message }`)).toBe(
|
||||
"search(...).catch is not a function.",
|
||||
)
|
||||
expect(
|
||||
await value(`const foo = () => ({ bar: () => ({}) }); try { foo().bar().baz() } catch (e) { return e.message }`),
|
||||
).toBe("foo(...).bar(...).baz is not a function.")
|
||||
})
|
||||
|
||||
test("built-ins are objects on a real prototype chain", async () => {
|
||||
@@ -973,17 +961,6 @@ describe("coercion parity: unknown static members read as undefined", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("async function line breaks", () => {
|
||||
test("a line break between function and the name is an async function", async () => {
|
||||
expect(await value(`async function\nfoo() { return 1 }\nreturn await foo()`)).toBe(1)
|
||||
})
|
||||
|
||||
test("a line break between async and function is not an async function", async () => {
|
||||
const failure = await error(`async\nfunction foo() { return 1 }\nreturn foo()`)
|
||||
expect(failure.message).toContain("Unknown identifier 'async'")
|
||||
})
|
||||
})
|
||||
|
||||
describe("functions are objects", () => {
|
||||
test("name follows NamedEvaluation and length counts required parameters", async () => {
|
||||
expect(
|
||||
|
||||
@@ -198,10 +198,10 @@ describe("Set composition Test262 parity", () => {
|
||||
keys: () => [-0],
|
||||
}
|
||||
return [
|
||||
1 / [...new Set([1]).union(setlike)][1] === Infinity,
|
||||
1 / [...new Set([0, 1, 2]).intersection(setlike)][0] === Infinity,
|
||||
1 / new Set([1]).union(setlike).values()[1] === Infinity,
|
||||
1 / new Set([0, 1, 2]).intersection(setlike).values()[0] === Infinity,
|
||||
[...new Set([0, 1]).difference(setlike)],
|
||||
1 / [...new Set([1, 2]).symmetricDifference(setlike)][2] === Infinity,
|
||||
1 / new Set([1, 2]).symmetricDifference(setlike).values()[2] === Infinity,
|
||||
]
|
||||
`),
|
||||
).toEqual([true, true, [1], true])
|
||||
|
||||
@@ -20,9 +20,9 @@ import { CodeMode, Tool } from "../src/index.js"
|
||||
|
||||
// Standard-library value types: Date, RegExp, Map, Set. Programs use them as ordinary JS;
|
||||
// intra-CodeMode checkpoints (Object.* helpers, spread, coercion inputs) preserve the live
|
||||
// values. JSON.stringify keeps Date -> ISO string (invalid -> null), URL -> href, and
|
||||
// RegExp/Map/Set/URLSearchParams -> {}. The host boundary matches that except URLSearchParams,
|
||||
// which cross as their query string, and Set, which crosses as an array.
|
||||
// values, while at the host boundary (final result, tool arguments, JSON.stringify) they
|
||||
// serialize exactly as JSON.stringify would: Date -> ISO string (invalid -> null),
|
||||
// URL -> href, and RegExp/Map/Set/URLSearchParams -> {}.
|
||||
const run = (code: string) => Effect.runPromise(CodeMode.execute({ code, tools: {} }))
|
||||
const value = async (code: string) => {
|
||||
const result = await run(code)
|
||||
@@ -655,9 +655,9 @@ describe("Headers", () => {
|
||||
copied: [headers.get("content-type"), copy.get("content-type")],
|
||||
pairs: [...new Headers([["b", "2"], ["A", "1"]])],
|
||||
map: [...new Headers(new Map([["k", "v"]]))],
|
||||
keys: [...headers.keys()],
|
||||
values: [...headers.values()],
|
||||
entries: [...headers.entries()],
|
||||
keys: headers.keys(),
|
||||
values: headers.values(),
|
||||
entries: headers.entries(),
|
||||
}
|
||||
`),
|
||||
).toEqual({
|
||||
@@ -816,28 +816,18 @@ describe("Map", () => {
|
||||
expect((await error(`return new Map(["flat"])`)).message).toMatch(/\[key, value\] pairs/)
|
||||
})
|
||||
|
||||
test("keys/values/entries return live iterators", async () => {
|
||||
test("keys/values/entries return arrays", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
const m = new Map([["a", 1], ["b", 2]])
|
||||
const keys = m.keys()
|
||||
const first = keys.next()
|
||||
m.set("c", 3)
|
||||
return { first, rest: [...keys], values: [...m.values()], entries: [...m.entries()], same: [...m[Symbol.iterator]()] }
|
||||
return { keys: m.keys(), values: m.values(), entries: m.entries() }
|
||||
`),
|
||||
).toEqual({
|
||||
first: { value: "a", done: false },
|
||||
rest: ["b", "c"],
|
||||
values: [1, 2, 3],
|
||||
keys: ["a", "b"],
|
||||
values: [1, 2],
|
||||
entries: [
|
||||
["a", 1],
|
||||
["b", 2],
|
||||
["c", 3],
|
||||
],
|
||||
same: [
|
||||
["a", 1],
|
||||
["b", 2],
|
||||
["c", 3],
|
||||
],
|
||||
})
|
||||
})
|
||||
@@ -898,42 +888,6 @@ describe("Map", () => {
|
||||
).toEqual({ a: 3, b: 1, c: 1 })
|
||||
})
|
||||
|
||||
test("getOrInsert and getOrInsertComputed insert only when the key is missing", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
const groups = new Map()
|
||||
groups.getOrInsert("a", []).push(1)
|
||||
groups.getOrInsert("a", []).push(2)
|
||||
let calls = 0
|
||||
const computed = (key) => { calls++; return key + "!" }
|
||||
const first = groups.getOrInsertComputed("b", computed)
|
||||
const second = groups.getOrInsertComputed("b", computed)
|
||||
const zero = groups.getOrInsertComputed(-0, (key) => 1 / key === Infinity)
|
||||
return [[...groups], first, second, calls, zero]
|
||||
`),
|
||||
).toEqual([
|
||||
[
|
||||
["a", [1, 2]],
|
||||
["b", "b!"],
|
||||
[0, true],
|
||||
],
|
||||
"b!",
|
||||
"b!",
|
||||
1,
|
||||
true,
|
||||
])
|
||||
expect(
|
||||
await value(`
|
||||
const m = new Map()
|
||||
const outer = m.getOrInsertComputed("k", () => { m.set("k", "inner"); return "outer" })
|
||||
let thrown
|
||||
try { m.getOrInsertComputed("j", () => { throw new Error("boom") }) } catch (error) { thrown = error.message }
|
||||
return [outer, m.get("k"), thrown, m.has("j")]
|
||||
`),
|
||||
).toEqual(["outer", "outer", "boom", false])
|
||||
expect((await error(`new Map().getOrInsertComputed("k", 5)`)).message).toContain("expects a function callback")
|
||||
})
|
||||
|
||||
test("maps serialize to {} at the boundary, like JSON", async () => {
|
||||
expect(await value(`return new Map([["a", 1]])`)).toEqual({})
|
||||
expect(await value(`return JSON.stringify(new Map([["a", 1]]))`)).toBe("{}")
|
||||
@@ -1097,7 +1051,7 @@ describe("Uint8Array", () => {
|
||||
console.log(b, new Uint8Array())
|
||||
return [String(b), b + "", +new Uint8Array([5]), Number.isNaN(Number(b)), b == "1,2", JSON.stringify(b), b.toLocaleString(), typeof b, b instanceof Uint8Array]
|
||||
`),
|
||||
).toEqual(["1,2", "1,2", 5, true, true, '{"0":1,"1":2}', "1,2", "object", true])
|
||||
).toEqual(["1,2", "1,2", 5, true, true, '{"0":1,"1":2}', "[object Uint8Array]", "object", true])
|
||||
expect((await run(`console.log(new Uint8Array([1, 2]), new Uint8Array())`)).logs).toEqual([
|
||||
"Uint8Array(2) [1,2] Uint8Array(0) []",
|
||||
])
|
||||
@@ -1165,151 +1119,6 @@ describe("TextEncoder and TextDecoder", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("built-in iterators", () => {
|
||||
test("keys/values/entries and [Symbol.iterator] step with next() and stay live", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
const items = ["a"]
|
||||
const it = items.entries()
|
||||
items.push("b")
|
||||
const steps = [it.next(), it.next(), it.next()]
|
||||
items.push("c")
|
||||
return { steps, after: it.next(), same: items[Symbol.iterator] === items.values }
|
||||
`),
|
||||
).toEqual({
|
||||
steps: [{ value: [0, "a"], done: false }, { value: [1, "b"], done: false }, { done: true }],
|
||||
after: { done: true },
|
||||
same: true,
|
||||
})
|
||||
expect(
|
||||
await value(`
|
||||
const s = new Set([1, 2])
|
||||
const u = new URLSearchParams("a=1&b=2")
|
||||
const h = new Headers({ b: "2", a: "1" })
|
||||
const bytes = new Uint8Array([7, 8])
|
||||
return [
|
||||
[...s.entries()], [...s[Symbol.iterator]()], s[Symbol.iterator] === s.values,
|
||||
[...u.keys()], [...u[Symbol.iterator]()], u[Symbol.iterator] === u.entries,
|
||||
[...h.values()], [...h[Symbol.iterator]()], h[Symbol.iterator] === h.entries,
|
||||
[...bytes.entries()], [...bytes[Symbol.iterator]()], bytes[Symbol.iterator] === bytes.values,
|
||||
[..."ab"[Symbol.iterator]()],
|
||||
]
|
||||
`),
|
||||
).toEqual([
|
||||
[
|
||||
[1, 1],
|
||||
[2, 2],
|
||||
],
|
||||
[1, 2],
|
||||
true,
|
||||
["a", "b"],
|
||||
[
|
||||
["a", "1"],
|
||||
["b", "2"],
|
||||
],
|
||||
true,
|
||||
["1", "2"],
|
||||
[
|
||||
["a", "1"],
|
||||
["b", "2"],
|
||||
],
|
||||
true,
|
||||
[
|
||||
[0, 7],
|
||||
[1, 8],
|
||||
],
|
||||
[7, 8],
|
||||
true,
|
||||
["a", "b"],
|
||||
])
|
||||
})
|
||||
|
||||
test("iterators are consumed once by every iteration site", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
const it = [1, 2, 3, 4].values()
|
||||
const picked = []
|
||||
for (const item of it) { picked.push(item); if (item === 2) break }
|
||||
const [third] = it
|
||||
return { picked, third, rest: [...it], spent: Array.from(it), again: it[Symbol.iterator]() === it }
|
||||
`),
|
||||
).toEqual({ picked: [1, 2], third: 3, rest: [4], spent: [], again: true })
|
||||
expect(
|
||||
await value(`
|
||||
const m = new Map([["a", 1], ["b", 2]])
|
||||
return [
|
||||
Object.fromEntries(m.entries()), Array.from(m.keys(), (k) => k + "!"), new Set(m.values()).size,
|
||||
await Promise.all([Promise.resolve(1), 2].values()),
|
||||
]
|
||||
`),
|
||||
).toEqual([{ a: 1, b: 2 }, ["a!", "b!"], 2, [1, 2]])
|
||||
expect(await value(`let s = 0; for await (const v of [Promise.resolve(1), 2].values()) s += v; return s`)).toBe(3)
|
||||
expect(
|
||||
await value(`return new Set([1, 2]).union({ size: 1, has: () => false, keys: () => new Set([3]).keys() })`),
|
||||
).toEqual([1, 2, 3])
|
||||
})
|
||||
|
||||
test("iterators are opaque references", async () => {
|
||||
expect(await value(`return [1].keys()`)).toEqual({})
|
||||
expect(await value(`return JSON.stringify({ it: [1].keys() })`)).toBe('{"it":{}}')
|
||||
expect(await value(`return [typeof [1].keys(), Array.isArray([1].keys()), Object.keys([1].keys())]`)).toEqual([
|
||||
"object",
|
||||
false,
|
||||
[],
|
||||
])
|
||||
const logged = await run(`console.log([1].keys()); return null`)
|
||||
expect(logged.logs?.[0]).toBe("[opaque reference]")
|
||||
expect((await error(`return [1].keys() + ""`)).message).toContain("Binary operators require data values")
|
||||
expect((await error(`return [1].keys().next.call({})`)).message).toContain("is not a function")
|
||||
expect((await error(`const it = [1].keys(); const next = it.next; return next()`)).message).toContain(
|
||||
"Iterator.prototype.next called on incompatible receiver undefined",
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("toLocaleString", () => {
|
||||
test("numbers and dates format as en-US in UTC; everything else falls back to toString", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
return [
|
||||
(1234567.891).toLocaleString(), new Date(0).toLocaleString(), new Date(0).toLocaleDateString(),
|
||||
new Date(0).toLocaleTimeString(), "a".toLocaleString(), true.toLocaleString(), ({}).toLocaleString(),
|
||||
({ toString: () => "custom" }).toLocaleString(), new Uint8Array([1, 2]).toLocaleString(),
|
||||
]
|
||||
`),
|
||||
).toEqual([
|
||||
"1,234,567.891",
|
||||
"1/1/1970, 12:00:00 AM",
|
||||
"1/1/1970",
|
||||
"12:00:00 AM",
|
||||
"a",
|
||||
"true",
|
||||
"[object Object]",
|
||||
"custom",
|
||||
"1,2",
|
||||
])
|
||||
})
|
||||
|
||||
test("arrays join each element's toLocaleString, skipping holes and nullish elements", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
let calls = 0
|
||||
const item = { toLocaleString() { calls++; return "o" } }
|
||||
return [[1234.5, "x", null, undefined, item, new Date(0)].toLocaleString(), [, item, , item].toLocaleString(), calls]
|
||||
`),
|
||||
).toEqual(["1,234.5,x,,,o,1/1/1970, 12:00:00 AM", ",o,,o", 3])
|
||||
expect((await error(`const f = ({}).toLocaleString; f()`)).message).toContain(
|
||||
"Object.prototype.toLocaleString called on null or undefined",
|
||||
)
|
||||
})
|
||||
|
||||
test("toLocaleLowerCase and toLocaleUpperCase ignore the locale argument", async () => {
|
||||
expect(
|
||||
await value(`return ["ABC".toLocaleLowerCase("tr"), "abc".toLocaleUpperCase(), "İ".toLocaleLowerCase()]`),
|
||||
).toEqual(["abc", "ABC", "i̇"])
|
||||
})
|
||||
})
|
||||
|
||||
describe("stdlib integration", () => {
|
||||
test("constructor follows own keys, shadowing, writes, and new", async () => {
|
||||
expect(
|
||||
|
||||
@@ -13,6 +13,8 @@ built-ins/Array/prototype/copyWithin/coerced-values-target.js # Array.copyWithi
|
||||
built-ins/Array/prototype/copyWithin/return-abrupt-from-end.js # Expected a Test262Error but got a TypeError
|
||||
built-ins/Array/prototype/copyWithin/return-abrupt-from-start.js # Expected a Test262Error but got a TypeError
|
||||
built-ins/Array/prototype/copyWithin/return-abrupt-from-target.js # Expected a Test262Error but got a TypeError
|
||||
built-ins/Array/prototype/entries/iteration-mutable.js # .next is not a function.
|
||||
built-ins/Array/prototype/entries/iteration.js # .next is not a function.
|
||||
built-ins/Array/prototype/every/15.4.4.16-7-6.js # res Expected SameValue(«true», «false») to be true
|
||||
built-ins/Array/prototype/every/15.4.4.16-7-c-i-8.js # [, , , ].every(callbackfn) Expected SameValue(«true», «false») to be true
|
||||
built-ins/Array/prototype/every/15.4.4.16-8-10.js # … cannot be constructed: user-defined constructors and classes are not supported. Call it as a funct
|
||||
@@ -84,6 +86,8 @@ built-ins/Array/prototype/join/S15.4.4.5_A3.1_T2.js # Array.join expects zero a
|
||||
built-ins/Array/prototype/join/S15.4.4.5_A3.2_T2.js # x.join() must return "*" Expected SameValue(«"[object Object]"», «"*"») to be true
|
||||
built-ins/Array/prototype/join/S15.4.4.5_A4_T3.js # Array.prototype.join called on incompatible receiver a data object.
|
||||
built-ins/Array/prototype/join/S15.4.4.5_A5_T1.js # #1: Array.prototype[1] = 1; x = [0]; x.length = 2; x.join() === "0,1". Actual: 0,
|
||||
built-ins/Array/prototype/keys/iteration-mutable.js # .next is not a function.
|
||||
built-ins/Array/prototype/keys/iteration.js # .next is not a function.
|
||||
built-ins/Array/prototype/lastIndexOf/15.4.4.15-5-1.js # Array.lastIndexOf expects start index to be a number.
|
||||
built-ins/Array/prototype/lastIndexOf/15.4.4.15-5-15.js # Array.lastIndexOf expects start index to be a number.
|
||||
built-ins/Array/prototype/lastIndexOf/15.4.4.15-5-16.js # Array.lastIndexOf expects start index to be a number.
|
||||
@@ -225,6 +229,7 @@ built-ins/Array/prototype/splice/S15.4.4.12_A4_T1.js # Array.prototype.splice c
|
||||
built-ins/Array/prototype/splice/S15.4.4.12_A4_T2.js # Array.prototype.splice called on incompatible receiver a data object.
|
||||
built-ins/Array/prototype/splice/S15.4.4.12_A4_T3.js # Array.prototype.splice called on incompatible receiver a data object.
|
||||
built-ins/Array/prototype/splice/create-ctor-non-object.js # null value Expected a TypeError to be thrown but no exception was thrown at all
|
||||
built-ins/Array/prototype/toLocaleString/S15.4.4.3_A1_T1.js # #1: var n = 0; var obj = {toLocaleString: function() {n++}}; var arr = [undefined, obj, null, obj, o
|
||||
built-ins/Array/prototype/toLocaleString/S15.4.4.3_A3_T1.js # #1: var n = 0; var obj = {toLocaleString: function() {n++}}; Array.prototype[1] = obj; x = [obj]; x.
|
||||
built-ins/Array/prototype/toReversed/holes-not-preserved.js # Actual [4, undefined, 2, undefined, 0] and expected [4, 3, 2, undefined, 0] should have the same con
|
||||
built-ins/Array/prototype/toSorted/holes-not-preserved.js # Actual [1, 3, 4, undefined, undefined] and expected [1, 2, 3, 4, undefined] should have the same con
|
||||
@@ -236,6 +241,9 @@ built-ins/Array/prototype/unshift/S15.4.4.13_A2_T3.js # Array.prototype.unshift
|
||||
built-ins/Array/prototype/unshift/S15.4.4.13_A3_T2.js # Array.prototype.unshift called on incompatible receiver a data object.
|
||||
built-ins/Array/prototype/unshift/S15.4.4.13_A4_T1.js # Array.prototype.unshift called on incompatible receiver a data object.
|
||||
built-ins/Array/prototype/unshift/S15.4.4.13_A4_T2.js # #3: Array.prototype[0] = 1; x = []; x.length = 1; x.unshift(0); x[1] === 1. Actual: undefined
|
||||
built-ins/Array/prototype/values/iteration-mutable.js # .next is not a function.
|
||||
built-ins/Array/prototype/values/iteration.js # .next is not a function.
|
||||
language/statements/async-function/rest-params-trailing-comma-early-error.js # expected SyntaxError but the program ran
|
||||
language/statements/async-generator/dflt-params-abrupt.js # Expected a Test262Error to be thrown but no exception was thrown at all
|
||||
language/statements/async-generator/dflt-params-ref-later.js # Expected a ReferenceError to be thrown but no exception was thrown at all
|
||||
language/statements/async-generator/dflt-params-ref-self.js # Expected a ReferenceError to be thrown but no exception was thrown at all
|
||||
@@ -283,13 +291,16 @@ language/statements/async-generator/dstr/obj-ptrn-prop-id-init-throws.js # Expe
|
||||
language/statements/async-generator/dstr/obj-ptrn-prop-id-init-unresolvable.js # Expected a ReferenceError to be thrown but no exception was thrown at all
|
||||
language/statements/async-generator/dstr/obj-ptrn-prop-obj-value-null.js # Expected a TypeError to be thrown but no exception was thrown at all
|
||||
language/statements/async-generator/dstr/obj-ptrn-prop-obj-value-undef.js # Expected a TypeError to be thrown but no exception was thrown at all
|
||||
language/statements/async-generator/rest-params-trailing-comma-early-error.js # expected SyntaxError but the program ran
|
||||
language/statements/async-generator/return-undefined-implicit-and-explicit.js # Actual ["tick 1", "tick 2", "g1 ret", "g2 ret", "g3 ret", "g4 ret"] and expected ["tick 1", "g1 ret"
|
||||
language/statements/const/dstr/ary-init-iter-get-err-array-prototype.js # Expected a TypeError to be thrown but no exception was thrown at all
|
||||
language/statements/for-await-of/async-func-decl-dstr-array-elem-init-in.js # Failed to parse TypeScript: '…' expected.
|
||||
language/statements/for-await-of/async-func-decl-dstr-obj-empty-bool.js # TypeError: Object destructuring requires a data object or array value, received a boolean.
|
||||
language/statements/for-await-of/async-func-decl-dstr-obj-empty-num.js # TypeError: Object destructuring requires a data object or array value, received a number.
|
||||
language/statements/for-await-of/async-func-decl-dstr-obj-empty-string.js # TypeError: Object destructuring requires a data object or array value, received a string.
|
||||
language/statements/for-await-of/async-func-decl-dstr-obj-rest-number.js # TypeError: Object destructuring requires a data object or array value, received a number.
|
||||
language/statements/for-await-of/async-func-decl-dstr-obj-rest-str-val.js # TypeError: Object destructuring requires a data object or array value, received a string.
|
||||
language/statements/for-await-of/async-gen-decl-dstr-array-elem-init-in.js # Failed to parse TypeScript: '…' expected.
|
||||
language/statements/for-await-of/async-gen-decl-dstr-array-elem-iter-rtrn-close-null.js # "Promise incorrectly fulfilled."
|
||||
language/statements/for-await-of/async-gen-decl-dstr-obj-empty-bool.js # TypeError: Object destructuring requires a data object or array value, received a boolean.
|
||||
language/statements/for-await-of/async-gen-decl-dstr-obj-empty-num.js # TypeError: Object destructuring requires a data object or array value, received a number.
|
||||
@@ -297,6 +308,8 @@ language/statements/for-await-of/async-gen-decl-dstr-obj-empty-string.js # Type
|
||||
language/statements/for-await-of/async-gen-decl-dstr-obj-rest-number.js # TypeError: Object destructuring requires a data object or array value, received a number.
|
||||
language/statements/for-await-of/async-gen-decl-dstr-obj-rest-str-val.js # TypeError: Object destructuring requires a data object or array value, received a string.
|
||||
language/statements/for-in/head-lhs-member.js # Unsupported for...in binding.
|
||||
language/statements/for-of/Array.prototype.Symbol.iterator.js # The called value is not a function.
|
||||
language/statements/for-of/dstr/array-elem-init-in.js # Failed to parse TypeScript: '…' expected.
|
||||
language/statements/for-of/dstr/array-elem-iter-rtrn-close-err.js # Iterator next must be a function.
|
||||
language/statements/for-of/dstr/array-elem-iter-rtrn-close-null.js # Expected a TypeError to be thrown but no exception was thrown at all
|
||||
language/statements/for-of/dstr/array-elem-iter-thrw-close-err.js # Expected SameValue(«1», «0») to be true
|
||||
@@ -342,6 +355,7 @@ language/statements/function/S14_A5_T1.js # Identifier '…' has already been d
|
||||
language/statements/function/S14_A5_T2.js # Identifier '…' has already been declared.
|
||||
language/statements/function/dstr/ary-init-iter-get-err-array-prototype.js # Expected a TypeError to be thrown but no exception was thrown at all
|
||||
language/statements/function/dstr/dflt-ary-init-iter-get-err-array-prototype.js # Expected a TypeError to be thrown but no exception was thrown at all
|
||||
language/statements/function/rest-params-trailing-comma-early-error.js # expected SyntaxError but the program ran
|
||||
language/statements/generators/dflt-params-abrupt.js # Expected a Test262Error to be thrown but no exception was thrown at all
|
||||
language/statements/generators/dflt-params-ref-later.js # Expected a ReferenceError to be thrown but no exception was thrown at all
|
||||
language/statements/generators/dflt-params-ref-self.js # Expected a ReferenceError to be thrown but no exception was thrown at all
|
||||
@@ -392,6 +406,8 @@ language/statements/generators/dstr/obj-ptrn-prop-obj-value-undef.js # Expected
|
||||
language/statements/generators/has-instance.js # The right-hand side of '…' has no '…' object.
|
||||
language/statements/generators/prototype-typeof.js # Expected SameValue(«"undefined"», «"object"») to be true
|
||||
language/statements/generators/prototype-uniqueness.js # Expected true but got false
|
||||
language/statements/generators/rest-params-trailing-comma-early-error.js # expected SyntaxError but the program ran
|
||||
language/statements/labeled/value-await-non-module-escaped.js # Failed to parse TypeScript: Keywords cannot contain escape characters.
|
||||
language/statements/labeled/value-await-non-module.js # Failed to parse TypeScript: Expression expected.
|
||||
language/statements/let/dstr/ary-init-iter-get-err-array-prototype.js # Expected a TypeError to be thrown but no exception was thrown at all
|
||||
language/statements/return/S12.9_A1_T1.js # expected SyntaxError but the program ran
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
* Copyright © web-platform-tests contributors. Governed by the 3-Clause BSD license in LICENSE.wpt.
|
||||
*
|
||||
* `assert_throws_dom("InvalidCharacterError", …)` becomes a check for a TypeError: CodeMode has no DOMException.
|
||||
* `checkIteratorProperties` (prototype chain and property descriptors) and the custom iterator on a Headers
|
||||
* instance are left out.
|
||||
* Headers cases that need `Symbol.iterator`, iterator objects from `keys()`/`values()`/`entries()` (CodeMode returns
|
||||
* arrays), or a custom iterator on a Headers instance are left out.
|
||||
*/
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
@@ -253,64 +253,33 @@ describe("Headers WPT parity (fetch/api/headers)", () => {
|
||||
}).sort()
|
||||
test(function() {
|
||||
var headers = new Headers(headerEntriesDict)
|
||||
var actual = headers.keys()
|
||||
sortedHeaderKeys.forEach(function(key) {
|
||||
const entry = actual.next()
|
||||
assert_false(entry.done)
|
||||
assert_equals(entry.value, key)
|
||||
})
|
||||
assert_true(actual.next().done)
|
||||
assert_true(actual.next().done)
|
||||
assert_array_equals(headers.keys(), sortedHeaderKeys)
|
||||
for (const key of headers.keys()) assert_true(sortedHeaderKeys.indexOf(key) != -1)
|
||||
}, "Check keys method")
|
||||
test(function() {
|
||||
var headers = new Headers(headerEntriesDict)
|
||||
var actual = headers.values()
|
||||
sortedHeaderKeys.forEach(function(key) {
|
||||
const entry = actual.next()
|
||||
assert_false(entry.done)
|
||||
assert_equals(entry.value, sortedHeaderDict[key])
|
||||
})
|
||||
assert_true(actual.next().done)
|
||||
assert_true(actual.next().done)
|
||||
assert_array_equals(headers.values(), sortedHeaderKeys.map((key) => sortedHeaderDict[key]))
|
||||
for (const value of headers.values()) assert_true(headerValues.indexOf(value) != -1)
|
||||
}, "Check values method")
|
||||
test(function() {
|
||||
var headers = new Headers(headerEntriesDict)
|
||||
var actual = headers.entries()
|
||||
sortedHeaderKeys.forEach(function(key) {
|
||||
const entry = actual.next()
|
||||
assert_false(entry.done)
|
||||
assert_equals(entry.value[0], key)
|
||||
assert_equals(entry.value[1], sortedHeaderDict[key])
|
||||
})
|
||||
assert_true(actual.next().done)
|
||||
assert_true(actual.next().done)
|
||||
assert_array_equals(headers.entries(), sortedHeaderKeys.map((key) => [key, sortedHeaderDict[key]]))
|
||||
for (const entry of headers.entries()) assert_equals(entry[1], sortedHeaderDict[entry[0]])
|
||||
}, "Check entries method")
|
||||
test(function() {
|
||||
var headers = new Headers(headerEntriesDict)
|
||||
var actual = headers[Symbol.iterator]()
|
||||
sortedHeaderKeys.forEach(function(key) {
|
||||
const entry = actual.next()
|
||||
assert_false(entry.done)
|
||||
assert_equals(entry.value[0], key)
|
||||
assert_equals(entry.value[1], sortedHeaderDict[key])
|
||||
})
|
||||
assert_true(actual.next().done)
|
||||
assert_true(actual.next().done)
|
||||
assert_array_equals([...headers], sortedHeaderKeys.map((key) => [key, sortedHeaderDict[key]]))
|
||||
}, "Check Symbol.iterator method")
|
||||
test(function() {
|
||||
var headers = new Headers(headerEntriesDict)
|
||||
var reference = sortedHeaderKeys[Symbol.iterator]()
|
||||
var index = 0
|
||||
headers.forEach(function(value, key, container) {
|
||||
assert_equals(headers, container)
|
||||
const entry = reference.next()
|
||||
assert_false(entry.done)
|
||||
assert_equals(key, entry.value)
|
||||
assert_equals(value, sortedHeaderDict[entry.value])
|
||||
assert_equals(key, sortedHeaderKeys[index])
|
||||
assert_equals(value, sortedHeaderDict[sortedHeaderKeys[index]])
|
||||
index++
|
||||
})
|
||||
assert_true(reference.next().done)
|
||||
assert_equals(index, sortedHeaderKeys.length)
|
||||
}, "Check forEach method")
|
||||
test(() => {
|
||||
const headers = new Headers({"foo": "2", "baz": "1", "BAR": "0"})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode/console-app",
|
||||
"version": "2.0.11",
|
||||
"version": "2.0.10",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/console-core",
|
||||
"version": "2.0.11",
|
||||
"version": "2.0.10",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode/console-function",
|
||||
"version": "2.0.11",
|
||||
"version": "2.0.10",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode/console-mail",
|
||||
"version": "2.0.11",
|
||||
"version": "2.0.10",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode/console-support",
|
||||
"version": "2.0.11",
|
||||
"version": "2.0.10",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "2.0.11",
|
||||
"version": "2.0.10",
|
||||
"name": "@opencode/core",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
@@ -98,8 +98,7 @@
|
||||
"@types/bun": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
"@types/which": "3.0.4",
|
||||
"drizzle-kit": "catalog:",
|
||||
"fast-check": "catalog:"
|
||||
"drizzle-kit": "catalog:"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/cohere": "3.0.27",
|
||||
|
||||
@@ -99,7 +99,7 @@ export const layer = (options?: Options) =>
|
||||
const wellknown = yield* WellKnown.Service
|
||||
const reloadLock = Semaphore.makeUnsafe(1)
|
||||
const updateLock = Semaphore.makeUnsafe(1)
|
||||
const decodeOptions = { errors: "all", onExcessProperty: "ignore" } as const
|
||||
const decodeOptions = { errors: "all", onExcessProperty: "ignore", propertyOrder: "original" } as const
|
||||
const decodeInfo = Schema.decodeUnknownOption(Info, decodeOptions)
|
||||
const parseInfo = Effect.fn("Config.parseInfo")(function* (text: string, source: string) {
|
||||
const errors: ParseError[] = []
|
||||
@@ -330,8 +330,7 @@ export const layer = (options?: Options) =>
|
||||
function* (patch: Patch) {
|
||||
const directory = initial.global ?? AbsolutePath.make(globalService.config)
|
||||
const candidates = ConfigDiscovery.names.map((name) => path.join(directory, name))
|
||||
const filepath =
|
||||
(yield* Effect.filter(candidates, fs.isFile)).at(-1) ?? path.join(directory, "opencode.jsonc")
|
||||
const filepath = (yield* Effect.filter(candidates, fs.isFile)).at(-1) ?? path.join(directory, "opencode.jsonc")
|
||||
const text = (yield* fs.readFileStringSafe(filepath)) ?? "{}\n"
|
||||
const updated = yield* Effect.try({
|
||||
try: () =>
|
||||
|
||||
@@ -42,7 +42,7 @@ export type Result =
|
||||
}
|
||||
| { readonly type: "rejected"; readonly diagnostics: readonly Diagnostic[] }
|
||||
|
||||
const options = { errors: "all", onExcessProperty: "ignore" } as const
|
||||
const options = { errors: "all", onExcessProperty: "ignore", propertyOrder: "original" } as const
|
||||
const unsupportedTopLevel = ["logLevel", "server", "subagent_depth", "layout"] as const
|
||||
const unsupportedExperimental = [
|
||||
"disable_paste_summary",
|
||||
|
||||
@@ -196,11 +196,11 @@ function decode(file: { directory: string; filepath: string; primary: boolean },
|
||||
const agent = legacy
|
||||
? Option.getOrUndefined(
|
||||
Option.map(
|
||||
decodeLegacyAgent({ name, ...markdown.data, prompt: body }, { errors: "all" }),
|
||||
decodeLegacyAgent({ name, ...markdown.data, prompt: body }, { errors: "all", propertyOrder: "original" }),
|
||||
ConfigMigrateV1.migrateAgent,
|
||||
),
|
||||
)
|
||||
: Option.getOrUndefined(decodeAgent({ ...data, system: body }, { errors: "all" }))
|
||||
: Option.getOrUndefined(decodeAgent({ ...data, system: body }, { errors: "all", propertyOrder: "original" }))
|
||||
if (!agent) return
|
||||
const info = Option.getOrUndefined(
|
||||
decodeConfig({
|
||||
|
||||
@@ -48,9 +48,10 @@ const proxy = (value: string, environment: Environment = process.env) => {
|
||||
)
|
||||
}
|
||||
|
||||
const constructorOptions = (input: Socket.WebSocketConstructorOptions | undefined): WebSocketOptions => {
|
||||
const constructorOptions = (input: string | Array<string> | undefined): WebSocketOptions => {
|
||||
if (typeof input === "string" || Array.isArray(input)) return { protocols: input }
|
||||
return { headers: input?.headers === undefined ? undefined : Headers.fromInput(input.headers) }
|
||||
// AI routes pass handshake options through Effect's browser-shaped constructor.
|
||||
return (input ?? {}) as WebSocketOptions
|
||||
}
|
||||
|
||||
const proxyAgent = (url: string, selectedProxy: string | undefined) => {
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
@@ -57,19 +57,16 @@ export const ModelsDevPlugin = define({
|
||||
})
|
||||
}
|
||||
})
|
||||
const apply = (data: readonly ModelsDev.Snapshot[]) => {
|
||||
loaded.data = snapshots(data)
|
||||
return ctx.integration.reload().pipe(Effect.andThen(ctx.provider.reload()))
|
||||
}
|
||||
yield* bus.subscribe(ModelsDev.Event.Refreshed).pipe(
|
||||
Stream.runForEach(() => modelsDev.get().pipe(Effect.flatMap(apply))),
|
||||
Stream.runForEach(() =>
|
||||
modelsDev.get().pipe(
|
||||
Effect.tap((data) => Effect.sync(() => (loaded.data = snapshots(data)))),
|
||||
Effect.andThen(ctx.integration.reload()),
|
||||
Effect.andThen(ctx.provider.reload()),
|
||||
),
|
||||
),
|
||||
Effect.forkScoped({ startImmediately: true }),
|
||||
)
|
||||
// A refresh that landed between the initial read and the subscription above published
|
||||
// Refreshed to nobody here. On a cold cache that read served the bundled snapshot, so
|
||||
// re-read now instead of waiting for the next TTL refresh.
|
||||
const latest = yield* modelsDev.get()
|
||||
if (snapshots(latest) !== loaded.data) yield* apply(latest)
|
||||
}),
|
||||
})
|
||||
|
||||
|
||||
@@ -170,6 +170,10 @@ export interface Interface {
|
||||
readonly switchAgent: (input: { sessionID: SessionSchema.ID; agent: Agent.ID }) => Effect.Effect<void, NotFoundError>
|
||||
readonly switchModel: (input: { sessionID: SessionSchema.ID; model: Model.Ref }) => Effect.Effect<void, NotFoundError>
|
||||
readonly rename: (input: { sessionID: SessionSchema.ID; title: string }) => Effect.Effect<void, NotFoundError>
|
||||
readonly setMetadata: (input: {
|
||||
sessionID: SessionSchema.ID
|
||||
metadata: SessionSchema.Metadata
|
||||
}) => Effect.Effect<void, NotFoundError>
|
||||
readonly setPermissions: (input: {
|
||||
sessionID: SessionSchema.ID
|
||||
permissions: Permission.Ruleset
|
||||
@@ -417,6 +421,7 @@ const layer = Layer.effect(
|
||||
switchAgent: (input) => sessions.forSession(input.sessionID).switchAgent(input),
|
||||
switchModel: (input) => sessions.forSession(input.sessionID).switchModel(input),
|
||||
rename: (input) => sessions.forSession(input.sessionID).rename(input),
|
||||
setMetadata: (input) => sessions.forSession(input.sessionID).setMetadata(input),
|
||||
setPermissions: (input) => sessions.forSession(input.sessionID).setPermissions(input),
|
||||
move: moves.move,
|
||||
compact: (input) => sessions.forSession(input.sessionID).compact(input),
|
||||
|
||||
@@ -131,6 +131,7 @@ export function update(adapter: Adapter, event: SessionEvent.DurableEvent) {
|
||||
)
|
||||
}),
|
||||
"session.renamed": () => Effect.void,
|
||||
"session.metadata.updated": () => Effect.void,
|
||||
"session.permissions": () => Effect.void,
|
||||
"session.deleted": () => Effect.void,
|
||||
"session.forked": () => Effect.void,
|
||||
|
||||
@@ -20,10 +20,9 @@ import { SessionSchema } from "./schema.js"
|
||||
import { webSocketConstructor } from "../effect/app-node-platform.js"
|
||||
|
||||
const ROTATE_AFTER_MS = 55 * 60 * 1000
|
||||
const CONNECT_TIMEOUT = "15 seconds"
|
||||
const INBOUND_CAPACITY = 128
|
||||
const CONNECT_TIMEOUT = "10 seconds"
|
||||
const IDLE_TIMEOUT = "5 minutes"
|
||||
/** Consecutive exchanges lost to the socket before the Session stays on HTTP. */
|
||||
const MAX_STREAM_FAILURES = 5
|
||||
const events = Metric.counter("opencode_session_websocket_events_total", {
|
||||
description: "Session WebSocket lifecycle events",
|
||||
incremental: true,
|
||||
@@ -51,7 +50,6 @@ interface State {
|
||||
readonly lock: Semaphore.Semaphore
|
||||
closed: boolean
|
||||
httpFallback: boolean
|
||||
streamFailures: number
|
||||
channel?: Channel
|
||||
}
|
||||
|
||||
@@ -128,7 +126,7 @@ export const makeLayer = (connector: WebSocketConnector) =>
|
||||
const state = (sessionID: SessionSchema.ID) => {
|
||||
const current = states.get(sessionID)
|
||||
if (current) return current
|
||||
const created = { lock: Semaphore.makeUnsafe(1), closed: false, httpFallback: false, streamFailures: 0 }
|
||||
const created = { lock: Semaphore.makeUnsafe(1), closed: false, httpFallback: false }
|
||||
states.set(sessionID, created)
|
||||
return created
|
||||
}
|
||||
@@ -170,27 +168,15 @@ export const makeLayer = (connector: WebSocketConnector) =>
|
||||
code: error.reason._tag === "Transport" ? error.reason.code : error.reason._tag,
|
||||
active: channel.active !== undefined,
|
||||
})
|
||||
if (channel.active) {
|
||||
Queue.failCauseUnsafe(channel.active.queue, Cause.fail(error))
|
||||
yield* streamFailure(owner)
|
||||
}
|
||||
yield* metric("protocol_failure")
|
||||
if (channel.active) Queue.failCauseUnsafe(channel.active.queue, Cause.fail(error))
|
||||
yield* metric(
|
||||
error.reason._tag === "Transport" && error.reason.code === "queue-overflow"
|
||||
? "queue_overflow"
|
||||
: "protocol_failure",
|
||||
)
|
||||
yield* channel.connection.close
|
||||
})
|
||||
|
||||
// A socket that keeps dying mid-exchange costs a retry every step; after enough consecutive
|
||||
// losses the Session stays on HTTP.
|
||||
const streamFailure = Effect.fn("SessionModelTransport.streamFailure")(function* (owner: State) {
|
||||
owner.streamFailures++
|
||||
if (owner.streamFailures < MAX_STREAM_FAILURES) return
|
||||
owner.httpFallback = true
|
||||
yield* Effect.logWarning("session websocket failed repeatedly; using http", {
|
||||
sessionTransport: "websocket",
|
||||
failures: owner.streamFailures,
|
||||
})
|
||||
yield* metric("fallback", { reason: "stream_failures" })
|
||||
})
|
||||
|
||||
const open = Effect.fn("SessionModelTransport.open")(function* (
|
||||
owner: State,
|
||||
exchange: WebSocketChannelExchange,
|
||||
@@ -249,7 +235,14 @@ export const makeLayer = (connector: WebSocketConnector) =>
|
||||
code: "message",
|
||||
phase: "receive",
|
||||
})
|
||||
Queue.offerUnsafe(active.queue, message)
|
||||
if (Queue.offerUnsafe(active.queue, message)) return undefined
|
||||
return yield* transportError("Session WebSocket inbound queue overflow", {
|
||||
url: exchange.connect.url,
|
||||
operation: "read",
|
||||
code: "queue-overflow",
|
||||
phase: "receive",
|
||||
delivery: "accepted",
|
||||
})
|
||||
}),
|
||||
),
|
||||
Effect.catch((error) =>
|
||||
@@ -262,7 +255,9 @@ export const makeLayer = (connector: WebSocketConnector) =>
|
||||
phase:
|
||||
error.reason._tag === "Transport" && error.reason.phase === "close" ? "close" : "receive",
|
||||
delivery:
|
||||
channel.active?.delivery === "provider-observed" || channel.active?.delivery === "terminal"
|
||||
channel.active?.delivery === "provider-observed" ||
|
||||
channel.active?.delivery === "terminal" ||
|
||||
(error.reason._tag === "Transport" && error.reason.code === "queue-overflow")
|
||||
? "accepted"
|
||||
: error.reason._tag === "Transport" && error.reason.code === "1009"
|
||||
? "rejected"
|
||||
@@ -375,7 +370,7 @@ export const makeLayer = (connector: WebSocketConnector) =>
|
||||
mode: create.mode,
|
||||
})
|
||||
const active: Active = {
|
||||
queue: yield* Queue.unbounded<string, AIError>(),
|
||||
queue: yield* Queue.bounded<string, AIError>(INBOUND_CAPACITY),
|
||||
delivery: "send-attempted",
|
||||
}
|
||||
channel.active = active
|
||||
@@ -400,7 +395,6 @@ export const makeLayer = (connector: WebSocketConnector) =>
|
||||
return fallback(exchange)
|
||||
}
|
||||
yield* metric("ambiguous_delivery")
|
||||
yield* streamFailure(owner)
|
||||
return yield* annotate(failure, { phase: "send", delivery: "ambiguous" })
|
||||
}
|
||||
yield* metric("send")
|
||||
@@ -441,7 +435,6 @@ export const makeLayer = (connector: WebSocketConnector) =>
|
||||
const pending = yield* Queue.size(active.queue)
|
||||
yield* Queue.shutdown(active.queue)
|
||||
if (terminal && pending === 0) {
|
||||
owner.streamFailures = 0
|
||||
yield* metric("terminal", { type: terminal.type })
|
||||
if (terminal.type === "rejected") yield* metric("rejection", { recovery: terminal.recovery })
|
||||
// The Codex backend stops serving a connection after any error frame: the next request is
|
||||
|
||||
@@ -573,6 +573,14 @@ const layer = Layer.effectDiscard(
|
||||
.run()
|
||||
.pipe(Effect.orDie),
|
||||
)
|
||||
yield* bus.project(SessionEvent.MetadataUpdated, (event) =>
|
||||
db
|
||||
.update(SessionTable)
|
||||
.set({ metadata: event.data.metadata, time_updated: event.created })
|
||||
.where(eq(SessionTable.id, event.data.sessionID))
|
||||
.run()
|
||||
.pipe(Effect.orDie),
|
||||
)
|
||||
yield* bus.project(SessionEvent.Permissions, (event) =>
|
||||
db
|
||||
.update(SessionTable)
|
||||
|
||||
@@ -73,6 +73,13 @@ export const make = Effect.fn("Session.make")(function* () {
|
||||
yield* get(sessionID)
|
||||
yield* bus.publish(SessionEvent.Renamed, { sessionID, title: input.title })
|
||||
})
|
||||
const setMetadata = Effect.fn("Session.setMetadata")(function* (
|
||||
sessionID: SessionSchema.ID,
|
||||
input: { metadata: SessionSchema.Metadata },
|
||||
) {
|
||||
yield* get(sessionID)
|
||||
yield* bus.publish(SessionEvent.MetadataUpdated, { sessionID, metadata: input.metadata })
|
||||
})
|
||||
const setPermissions = Effect.fn("Session.setPermissions")(function* (
|
||||
sessionID: SessionSchema.ID,
|
||||
input: { permissions: Permission.Ruleset },
|
||||
@@ -342,6 +349,7 @@ export const make = Effect.fn("Session.make")(function* () {
|
||||
message,
|
||||
view,
|
||||
rename,
|
||||
setMetadata,
|
||||
setPermissions,
|
||||
switchAgent,
|
||||
switchModel,
|
||||
@@ -365,6 +373,7 @@ export const make = Effect.fn("Session.make")(function* () {
|
||||
const message = operations.message.bind(undefined, sessionID)
|
||||
const view = operations.view.bind(undefined, sessionID)
|
||||
const rename = operations.rename.bind(undefined, sessionID)
|
||||
const setMetadata = operations.setMetadata.bind(undefined, sessionID)
|
||||
const setPermissions = operations.setPermissions.bind(undefined, sessionID)
|
||||
const switchAgent = operations.switchAgent.bind(undefined, sessionID)
|
||||
const switchModel = operations.switchModel.bind(undefined, sessionID)
|
||||
@@ -391,6 +400,7 @@ export const make = Effect.fn("Session.make")(function* () {
|
||||
message,
|
||||
view,
|
||||
rename,
|
||||
setMetadata,
|
||||
setPermissions,
|
||||
switchAgent,
|
||||
switchModel,
|
||||
|
||||
@@ -170,7 +170,7 @@ const standardJsonSchema = (schema: StandardSchemaV1<any, any>, io: "input" | "o
|
||||
}
|
||||
|
||||
const toJsonSchema = (schema: Schema.Top): JsonSchema.JsonSchema => {
|
||||
const document = Schema.toJsonSchemaDocument(schema, { onExcessProperty: "error" })
|
||||
const document = Schema.toJsonSchemaDocument(schema)
|
||||
// Effect emits valid JSON Schema that some inference providers handle poorly. Simplify it
|
||||
// without changing validation: `{ type: "integer", allOf: [{ minimum: 0 }] }` becomes
|
||||
// `{ type: "integer", minimum: 0 }` only when no keyword would be overwritten. Named schemas
|
||||
|
||||
@@ -13,7 +13,7 @@ import { ConfigProviderOptionsV1 } from "./provider-options.js"
|
||||
import { Provider } from "../../provider.js"
|
||||
import { Model } from "../../model.js"
|
||||
|
||||
const decodeOptions = { errors: "all", onExcessProperty: "ignore" } as const
|
||||
const decodeOptions = { errors: "all", onExcessProperty: "ignore", propertyOrder: "original" } as const
|
||||
const decodeInfo = Schema.decodeUnknownSync(Schema.fromJsonString(Info), decodeOptions)
|
||||
const encodeInfo = Schema.encodeSync(Info)
|
||||
const decodeAgent = Schema.decodeUnknownSync(Schema.fromJsonString(ConfigAgent.Info), decodeOptions)
|
||||
|
||||
@@ -11,8 +11,9 @@ export type Object = Schema.Schema.Type<typeof Object>
|
||||
export const Rule = Schema.Union([Action, Object]).annotate({ identifier: "PermissionRuleConfig" })
|
||||
export type Rule = Schema.Schema.Type<typeof Rule>
|
||||
|
||||
// Known permission keys get explicit types in the Effect schema for generated docs/types.
|
||||
// StructWithRest also accepts custom permission names.
|
||||
// Known permission keys get explicit types in the Effect schema for generated
|
||||
// docs/types. Runtime config parsing uses Effect's `propertyOrder: "original"`
|
||||
// parse option so user key order is preserved for permission precedence.
|
||||
const InputObject = Schema.StructWithRest(
|
||||
Schema.Struct({
|
||||
read: Schema.optional(Rule),
|
||||
|
||||
@@ -27,7 +27,6 @@ export function resolve(model: Model.Info, supports: readonly Support[] = [{ typ
|
||||
const EFFORTS = ["low", "medium", "high"]
|
||||
const ENCRYPTED_REASONING = ["reasoning.encrypted_content"]
|
||||
const ADAPTIVE_THINKING = { type: "adaptive", display: "summarized" }
|
||||
const ANTHROPIC_OUTPUT_TOKEN_MAX = 32_000
|
||||
|
||||
const variant = (id: string, overlay: Overlay): Variants[number] => ({ id: Model.VariantID.make(id), ...overlay })
|
||||
|
||||
@@ -40,9 +39,8 @@ function budgets(
|
||||
model: Model.Info,
|
||||
support: Extract<Support, { type: "budget_tokens" }>,
|
||||
spell: (tokens: number) => Overlay,
|
||||
ceiling = model.limit.output,
|
||||
): Variants {
|
||||
const maximum = Math.min(support.max ?? ceiling - 1, model.limit.output - 1, ceiling - 1)
|
||||
const maximum = Math.min(support.max ?? model.limit.output - 1, model.limit.output - 1)
|
||||
if (maximum <= 0) return []
|
||||
const high = Math.min(Math.max(support.min ?? 0, Math.floor((maximum + 1) / 2)), maximum)
|
||||
return [variant("high", spell(high)), variant("max", spell(maximum))]
|
||||
@@ -275,12 +273,9 @@ const anthropicMessages: Protocol = (model, support) => {
|
||||
return toggle({ settings: { thinking: { type: "disabled" } } }, thinking)
|
||||
}
|
||||
case "budget_tokens":
|
||||
return budgets(
|
||||
model,
|
||||
support,
|
||||
(tokens) => ({ settings: { thinking: { type: "enabled", budgetTokens: tokens } } }),
|
||||
ANTHROPIC_OUTPUT_TOKEN_MAX,
|
||||
)
|
||||
return budgets(model, support, (tokens) => ({
|
||||
settings: { thinking: { type: "enabled", budgetTokens: tokens } },
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -386,14 +381,10 @@ const bedrockConverse: Protocol = (model, support) => {
|
||||
? toggle(fields({ thinking: { type: "disabled" } }), fields({ thinking: ADAPTIVE_THINKING }))
|
||||
: toggle(fields({ reasoningConfig: { type: "disabled" } }), fields({ reasoningConfig: { type: "enabled" } }))
|
||||
case "budget_tokens":
|
||||
return budgets(
|
||||
model,
|
||||
support,
|
||||
(tokens) =>
|
||||
claude
|
||||
? fields({ thinking: { type: "enabled", budget_tokens: tokens } })
|
||||
: fields({ reasoningConfig: { type: "enabled", budgetTokens: tokens } }),
|
||||
claude ? ANTHROPIC_OUTPUT_TOKEN_MAX : model.limit.output,
|
||||
return budgets(model, support, (tokens) =>
|
||||
claude
|
||||
? fields({ thinking: { type: "enabled", budget_tokens: tokens } })
|
||||
: fields({ reasoningConfig: { type: "enabled", budgetTokens: tokens } }),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -445,12 +436,9 @@ const bedrockAISDK: Protocol = (model, support) => {
|
||||
{ settings: { additionalModelRequestFields: { reasoningConfig: { type: "enabled" } } } },
|
||||
)
|
||||
case "budget_tokens":
|
||||
return budgets(
|
||||
model,
|
||||
support,
|
||||
(tokens) => ({ settings: { reasoningConfig: { type: "enabled", budgetTokens: tokens } } }),
|
||||
claude ? ANTHROPIC_OUTPUT_TOKEN_MAX : model.limit.output,
|
||||
)
|
||||
return budgets(model, support, (tokens) => ({
|
||||
settings: { reasoningConfig: { type: "enabled", budgetTokens: tokens } },
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -501,11 +489,8 @@ const sapAICore: Protocol = (model, support) => {
|
||||
return []
|
||||
case "budget_tokens":
|
||||
if (id.includes("anthropic"))
|
||||
return budgets(
|
||||
model,
|
||||
support,
|
||||
(tokens) => sap({ additionalModelRequestFields: { thinking: { type: "enabled", budget_tokens: tokens } } }),
|
||||
ANTHROPIC_OUTPUT_TOKEN_MAX,
|
||||
return budgets(model, support, (tokens) =>
|
||||
sap({ additionalModelRequestFields: { thinking: { type: "enabled", budget_tokens: tokens } } }),
|
||||
)
|
||||
if (id.includes("gemini"))
|
||||
return budgets(model, support, (tokens) =>
|
||||
|
||||
@@ -2,7 +2,7 @@ import path from "path"
|
||||
import fs from "fs/promises"
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { Effect, Fiber, Layer, Logger, Schema, Stream } from "effect"
|
||||
import { Arbitrary } from "effect/unstable/arbitrary"
|
||||
import { FastCheck } from "effect/testing"
|
||||
import { Config } from "@opencode/core/config"
|
||||
import { Directory, Document, Event, Info } from "@opencode/schema/config"
|
||||
import { ConfigModel } from "@opencode/schema/config/model"
|
||||
@@ -632,22 +632,17 @@ describe("Config", () => {
|
||||
})
|
||||
|
||||
test("migrates arbitrary v1 configuration into valid v2 configuration", () => {
|
||||
const result = Effect.runSync(
|
||||
Arbitrary.checkEffect(
|
||||
Arbitrary.schema(ConfigV1.Info),
|
||||
(info) => {
|
||||
const parsed = Schema.decodeUnknownSync(ConfigV1.Info)(
|
||||
Schema.decodeUnknownSync(Schema.fromJsonString(Schema.Unknown))(
|
||||
Schema.encodeUnknownSync(Schema.fromJsonString(Schema.Unknown))(info),
|
||||
),
|
||||
)
|
||||
Schema.decodeUnknownSync(Info)(ConfigMigrateV1.migrate(parsed), { errors: "all" })
|
||||
return true
|
||||
},
|
||||
{ runs: 100 },
|
||||
),
|
||||
FastCheck.assert(
|
||||
FastCheck.property(Schema.toArbitrary(ConfigV1.Info)(FastCheck), (info) => {
|
||||
const parsed = Schema.decodeUnknownSync(ConfigV1.Info)(
|
||||
Schema.decodeUnknownSync(Schema.fromJsonString(Schema.Unknown))(
|
||||
Schema.encodeUnknownSync(Schema.fromJsonString(Schema.Unknown))(info),
|
||||
),
|
||||
)
|
||||
Schema.decodeUnknownSync(Info)(ConfigMigrateV1.migrate(parsed), { errors: "all" })
|
||||
}),
|
||||
{ numRuns: 100 },
|
||||
)
|
||||
expect(Arbitrary.formatCheckFailure(result)).toBeUndefined()
|
||||
}, 30_000)
|
||||
|
||||
test("migrates the v1 experimental subagent depth", () => {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { Duration, Effect, Schema } from "effect"
|
||||
import { Arbitrary } from "effect/unstable/arbitrary"
|
||||
import { Duration, Schema } from "effect"
|
||||
import { FastCheck } from "effect/testing"
|
||||
import { ConfigNormalize } from "@opencode/core/config/normalize"
|
||||
import { Info } from "@opencode/schema/config"
|
||||
|
||||
const options = { errors: "all", onExcessProperty: "ignore" } as const
|
||||
const options = { errors: "all", onExcessProperty: "ignore", propertyOrder: "original" } as const
|
||||
|
||||
function normalized(input: unknown) {
|
||||
const result = ConfigNormalize.normalize(input)
|
||||
@@ -80,21 +80,16 @@ describe("ConfigNormalize", () => {
|
||||
})
|
||||
|
||||
test("preserves arbitrary JSON-round-tripped native configuration", () => {
|
||||
const result = Effect.runSync(
|
||||
Arbitrary.checkEffect(
|
||||
Arbitrary.schema(Info),
|
||||
(info) => {
|
||||
const source = JSON.parse(JSON.stringify(Schema.encodeSync(Info)(info)))
|
||||
const result = normalized(source)
|
||||
expect(Schema.decodeUnknownSync(Info)(result.encoded)).toEqual(
|
||||
Schema.decodeUnknownSync(Info)(withoutEmptyCompatibilityContainers(source)),
|
||||
)
|
||||
return true
|
||||
},
|
||||
{ runs: 100 },
|
||||
),
|
||||
FastCheck.assert(
|
||||
FastCheck.property(Schema.toArbitrary(Info)(FastCheck), (info) => {
|
||||
const source = JSON.parse(JSON.stringify(Schema.encodeSync(Info)(info)))
|
||||
const result = normalized(source)
|
||||
expect(Schema.decodeUnknownSync(Info)(result.encoded)).toEqual(
|
||||
Schema.decodeUnknownSync(Info)(withoutEmptyCompatibilityContainers(source)),
|
||||
)
|
||||
}),
|
||||
{ numRuns: 100 },
|
||||
)
|
||||
expect(Arbitrary.formatCheckFailure(result)).toBeUndefined()
|
||||
})
|
||||
|
||||
test("merges named maps by entry and gives valid native entries precedence", () => {
|
||||
|
||||
@@ -290,37 +290,6 @@ describe("ModelsDevPlugin", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
isolated.effect("adopts a refresh that completes between its initial read and its subscription", () =>
|
||||
Effect.gen(function* () {
|
||||
const bundled = richSnapshot("Acme Bundled")
|
||||
const fresh = richSnapshot("Acme Fresh")
|
||||
const current = { snapshot: bundled.snapshot }
|
||||
const location = yield* owner
|
||||
// Cold cache: the first read serves the bundled snapshot, and the boot-time
|
||||
// ModelsDev.refresh() lands right after it, before the plugin subscribes.
|
||||
const source = ModelsDev.Service.of({
|
||||
get: () =>
|
||||
Effect.gen(function* () {
|
||||
const data = current.snapshot
|
||||
if (data !== bundled.snapshot) return data
|
||||
current.snapshot = fresh.snapshot
|
||||
yield* location.bus.publish(ModelsDev.Event.Refreshed, {})
|
||||
return data
|
||||
}),
|
||||
refresh: () => Effect.void,
|
||||
})
|
||||
yield* ModelsDevPlugin.effect(location.host).pipe(
|
||||
Effect.provideService(ModelsDev.Service, source),
|
||||
Effect.provideContext(location.context),
|
||||
)
|
||||
yield* TestClock.adjust("500 millis")
|
||||
yield* TestClock.adjust("500 millis")
|
||||
yield* TestClock.adjust("500 millis")
|
||||
|
||||
expect(required(yield* location.providers.get(bundled.providerID)).name).toBe("Acme Fresh")
|
||||
}),
|
||||
)
|
||||
|
||||
real.effect("keeps the retained definition unchanged across model replay", () =>
|
||||
Effect.gen(function* () {
|
||||
const providers = yield* Provider.Service
|
||||
@@ -1018,11 +987,11 @@ describe("ModelsDevPlugin", () => {
|
||||
const budgetModel = yield* modelState.get(Provider.ID.anthropic, Model.ID.make("claude-budget"))
|
||||
expect(budgetModel?.variants).toContainEqual({
|
||||
id: Model.VariantID.make("high"),
|
||||
settings: { thinking: { type: "enabled", budgetTokens: 16000 } },
|
||||
settings: { thinking: { type: "enabled", budgetTokens: 32000 } },
|
||||
})
|
||||
expect(budgetModel?.variants).toContainEqual({
|
||||
id: Model.VariantID.make("max"),
|
||||
settings: { thinking: { type: "enabled", budgetTokens: 31999 } },
|
||||
settings: { thinking: { type: "enabled", budgetTokens: 63999 } },
|
||||
})
|
||||
|
||||
const anthropicEffortModel = yield* modelState.get(Provider.ID.anthropic, Model.ID.make("claude-opus-4.7"))
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
import { expect } from "bun:test"
|
||||
import { Agent } from "@opencode/core/agent"
|
||||
import { Plugin } from "@opencode/core/plugin"
|
||||
import { PluginHost } from "@opencode/core/plugin/host"
|
||||
import { PluginPromise } from "@opencode/core/plugin/promise"
|
||||
import { Tool } from "@opencode/core/tool"
|
||||
import { Session } from "@opencode/schema/session"
|
||||
import { SessionMessage } from "@opencode/schema/session-message"
|
||||
import { Cause, Deferred, Effect, Exit, Fiber } from "effect"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { PluginTestLayer } from "./fixture"
|
||||
|
||||
const it = testEffect(PluginTestLayer)
|
||||
|
||||
it.live("Promise tool executors receive interruption through their AbortSignal", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugins = yield* Plugin.Service
|
||||
const tools = yield* Tool.Service
|
||||
const started = yield* Deferred.make<AbortSignal>()
|
||||
yield* PluginPromise.fromPromise({
|
||||
id: "cancel-tool",
|
||||
async setup(context) {
|
||||
await context.tool.transform((editor) =>
|
||||
editor.add({
|
||||
name: "wait",
|
||||
description: "Wait until cancelled",
|
||||
input: { type: "object", properties: {}, additionalProperties: false },
|
||||
options: { codemode: false },
|
||||
execute: (_input, context) =>
|
||||
new Promise<never>((_resolve, reject) => {
|
||||
context.signal.addEventListener("abort", () => reject(new Error("cancelled")), { once: true })
|
||||
Effect.runSync(Deferred.succeed(started, context.signal))
|
||||
}),
|
||||
}),
|
||||
)
|
||||
},
|
||||
}).effect(yield* PluginHost.make(plugins))
|
||||
|
||||
const snapshot = yield* tools.snapshot()
|
||||
const fiber = yield* snapshot
|
||||
.execute({
|
||||
sessionID: Session.ID.make("ses_promise_tool_cancel"),
|
||||
agent: Agent.ID.make("build"),
|
||||
messageID: SessionMessage.ID.make("msg_promise_tool_cancel"),
|
||||
call: { type: "tool-call", id: "call_promise_tool_cancel", name: "wait", input: {} },
|
||||
})
|
||||
.pipe(Effect.forkScoped)
|
||||
const signal = yield* Deferred.await(started)
|
||||
expect(signal.aborted).toBe(false)
|
||||
yield* Fiber.interrupt(fiber)
|
||||
const exit = yield* Fiber.await(fiber)
|
||||
expect(signal.aborted).toBe(true)
|
||||
expect(Exit.isFailure(exit) && Cause.hasInterruptsOnly(exit.cause)).toBe(true)
|
||||
}),
|
||||
)
|
||||
@@ -8,7 +8,7 @@ import { Money } from "@opencode/schema/money"
|
||||
import { Shell } from "@opencode/schema/shell"
|
||||
import { Skill } from "@opencode/schema/skill"
|
||||
import { Agent } from "@opencode/core/agent"
|
||||
import { asc, eq } from "drizzle-orm"
|
||||
import { and, asc, eq } from "drizzle-orm"
|
||||
import { Database } from "@opencode/core/database/database"
|
||||
import { AppNodeBuilder } from "@opencode/core/effect/app-node-builder"
|
||||
import { LayerNode } from "@opencode/util/effect/layer-node"
|
||||
@@ -385,6 +385,26 @@ describe("Session.create", () => {
|
||||
|
||||
// Absent stays absent: no empty-object normalization.
|
||||
expect((yield* session.create({ location })).metadata).toBeUndefined()
|
||||
|
||||
const replacement = { thread: "updated", owner: "host" }
|
||||
yield* session.setMetadata({ sessionID: created.id, metadata: replacement })
|
||||
expect((yield* session.get(created.id)).metadata).toEqual(replacement)
|
||||
expect(
|
||||
yield* db
|
||||
.select({ data: EventTable.data })
|
||||
.from(EventTable)
|
||||
.where(
|
||||
and(
|
||||
eq(EventTable.aggregate_id, created.id),
|
||||
eq(EventTable.type, Bus.versionedType(SessionEvent.MetadataUpdated.type, 1)),
|
||||
),
|
||||
)
|
||||
.get()
|
||||
.pipe(Effect.orDie),
|
||||
).toMatchObject({ data: { metadata: replacement } })
|
||||
expect(
|
||||
yield* session.setMetadata({ sessionID: Session.ID.create(), metadata: replacement }).pipe(Effect.flip),
|
||||
).toBeInstanceOf(Session.NotFoundError)
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@@ -593,7 +593,7 @@ describe("SessionModelTransport", () => {
|
||||
Effect.forkChild({ startImmediately: true }),
|
||||
)
|
||||
yield* Effect.yieldNow
|
||||
yield* TestClock.adjust("15 seconds")
|
||||
yield* TestClock.adjust("10 seconds")
|
||||
expect(yield* Fiber.join(running)).toEqual(["fallback:slow"])
|
||||
}),
|
||||
)
|
||||
@@ -731,40 +731,6 @@ describe("SessionModelTransport", () => {
|
||||
)
|
||||
})
|
||||
|
||||
test("keeps the Session on HTTP after repeated mid-stream socket losses", async () => {
|
||||
let opens = 0
|
||||
const connector: WebSocketConnector = {
|
||||
open: () =>
|
||||
Effect.gen(function* () {
|
||||
opens++
|
||||
const messages = yield* Queue.unbounded<string | Uint8Array, AIError>()
|
||||
return {
|
||||
sendText: () =>
|
||||
Effect.sync(() => {
|
||||
Queue.failCauseUnsafe(messages, Cause.fail(error("socket dropped")))
|
||||
}),
|
||||
messages: Stream.fromQueue(messages),
|
||||
close: Queue.shutdown(messages).pipe(Effect.asVoid),
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
await run(
|
||||
connector,
|
||||
Effect.gen(function* () {
|
||||
const transport = yield* SessionModelTransport.Service
|
||||
const executor = transport.bind(session)
|
||||
for (let attempt = 0; attempt < 5; attempt++) {
|
||||
const result = yield* Effect.result(collect(executor, exchange(`attempt-${attempt}`)))
|
||||
expect(result._tag).toBe("Failure")
|
||||
}
|
||||
expect(opens).toBe(5)
|
||||
expect(yield* collect(executor, exchange("sixth"))).toEqual(["fallback:sixth"])
|
||||
expect(opens).toBe(5)
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
test("falls back to HTTP after close code 1009 and keeps the Session on HTTP", async () => {
|
||||
const messages = queue<string | Uint8Array, AIError>()
|
||||
let opened = 0
|
||||
@@ -949,22 +915,24 @@ describe("SessionModelTransport", () => {
|
||||
)
|
||||
})
|
||||
|
||||
test("buffers a synchronous burst of frames larger than any fixed capacity", async () => {
|
||||
// Bun dispatches every frame in a read buffer in one tick; a large tool call streams thousands
|
||||
// of small argument deltas, so the exchange must absorb the whole burst before it can consume.
|
||||
const burst = 1500
|
||||
test("poisons instead of dropping data when the inbound queue overflows", async () => {
|
||||
const messages = queue<string | Uint8Array, AIError>()
|
||||
const poisoned = Deferred.makeUnsafe<void>()
|
||||
let closed = 0
|
||||
const connector: WebSocketConnector = {
|
||||
open: () =>
|
||||
Effect.succeed({
|
||||
sendText: () =>
|
||||
// Hold consumption at the send boundary until the reader fills and poisons the inbound queue.
|
||||
Effect.sync(() => {
|
||||
for (let index = 0; index < burst; index++) Queue.offerUnsafe(messages, `frame:${index}`)
|
||||
Queue.offerUnsafe(messages, "completed")
|
||||
}),
|
||||
messages: Stream.fromQueue(messages),
|
||||
close: Effect.sync(() => closed++).pipe(Effect.andThen(Queue.shutdown(messages)), Effect.asVoid),
|
||||
for (let index = 0; index <= 129; index++) Queue.offerUnsafe(messages, `frame:${index}`)
|
||||
}).pipe(Effect.andThen(Deferred.await(poisoned))),
|
||||
messages: Stream.fromQueue(messages).pipe(Stream.tap(() => Effect.yieldNow)),
|
||||
close: Effect.sync(() => closed++).pipe(
|
||||
Effect.andThen(Deferred.succeed(poisoned, undefined)),
|
||||
Effect.andThen(Queue.shutdown(messages)),
|
||||
Effect.asVoid,
|
||||
),
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -973,21 +941,20 @@ describe("SessionModelTransport", () => {
|
||||
Effect.gen(function* () {
|
||||
const transport = yield* SessionModelTransport.Service
|
||||
const item = exchange("first")
|
||||
const frames = yield* collect(transport.bind(session), {
|
||||
...item,
|
||||
driver: {
|
||||
create: item.driver.create,
|
||||
observe: (_create, frame) =>
|
||||
Effect.succeed(
|
||||
frame === "completed" ? { type: "completed" as const, frame } : { type: "frame" as const, frame },
|
||||
),
|
||||
},
|
||||
const result = yield* Effect.result(
|
||||
collect(transport.bind(session), {
|
||||
...item,
|
||||
driver: {
|
||||
create: item.driver.create,
|
||||
observe: (_create, frame) => Effect.succeed({ type: "frame" as const, frame }),
|
||||
},
|
||||
}),
|
||||
)
|
||||
expect(result).toMatchObject({
|
||||
_tag: "Failure",
|
||||
failure: { reason: { _tag: "Transport", code: "queue-overflow", delivery: "accepted" } },
|
||||
})
|
||||
expect(frames).toHaveLength(burst + 1)
|
||||
expect(frames.slice(0, 3)).toEqual(["frame:0", "frame:1", "frame:2"])
|
||||
expect(frames.at(-2)).toBe(`frame:${burst - 1}`)
|
||||
expect(frames.at(-1)).toBe("completed")
|
||||
expect(closed).toBe(0)
|
||||
expect(closed).toBe(1)
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { State } from "@opencode/core/state"
|
||||
import { Effect } from "effect"
|
||||
import FastCheck from "fast-check"
|
||||
import { FastCheck } from "effect/testing"
|
||||
|
||||
type Operation = { multiply: number; add: number }
|
||||
type Value = { value: number; order: number[] }
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user