Compare commits

..

19 Commits

Author SHA1 Message Date
opencode-agent[bot] 7afe537c69 fix(ai): align chat assistant content (#41210)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2026-08-07 23:59:46 -05:00
opencode-agent[bot] e566c082f1 chore: generate 2026-08-08 04:47:57 +00:00
opencode-agent[bot] bc93da4a46 fix(core): reload changed MCP config (#41204)
Co-authored-by: Aiden Cline <rekram1-node@users.noreply.github.com>
2026-08-07 23:46:48 -05:00
opencode-agent[bot] 4194d55522 chore: generate 2026-08-08 04:21:03 +00:00
Aiden Cline f0d1029719 Add v2 branch to workflow triggers (#41207) 2026-08-07 23:20:00 -05:00
opencode-agent[bot] b274db88b0 chore(core): format package (#41205)
Co-authored-by: Aiden Cline <rekram1-node@users.noreply.github.com>
2026-08-07 23:18:03 -05:00
Kit Langton 9b7b402737 feat(server): run modal sandboxes on the vm runtime (#41177) 2026-08-07 23:21:05 -04:00
James Long 5e6370363b fix(tui): refine provider failure presentation (#41179) 2026-08-07 23:11:32 -04:00
Kit Langton dd6020656e fix(merman): tighten flowchart spacing (#41191) 2026-08-07 23:00:44 -04:00
Kit Langton 8c758e443b fix(core): reuse shared patch diff (#41186) 2026-08-07 22:50:59 -04:00
Kit Langton 0df6aed6ca fix(merman): derive neutral diagram palette (#41181) 2026-08-07 22:43:58 -04:00
Kit Langton aa05fd23b3 refactor(core): remove legacy account runtime schemas (#41173) 2026-08-07 22:40:53 -04:00
Kit Langton fd09760903 fix(tui): open sessions by exact ID (#41180) 2026-08-07 22:29:09 -04:00
Kit Langton 3a4772abb4 fix(merman): separate sibling subgraphs (#41178) 2026-08-07 22:28:12 -04:00
Kit Langton 2cf65c025a fix(merman): support undirected edges and place multiline state labels (#41171) 2026-08-07 21:49:04 -04:00
Kit Langton f3f1204802 feat(tui): render Mermaid diagrams (#41113) 2026-08-07 20:59:50 -04:00
opencode-agent[bot] b3aea1c39d feat(console): add workspace unblock endpoint (#41170)
Co-authored-by: Frank Wang <83515+fwang@users.noreply.github.com>
2026-08-07 20:59:40 -04:00
opencode-agent[bot] 79a493e7f5 fix(tui): show external worktree session labels (#41147)
Co-authored-by: James Long <17031+jlongster@users.noreply.github.com>
2026-08-07 20:52:44 -04:00
Kit Langton 952d4d446f feat(server): add modal environment driver (#41118) 2026-08-07 19:36:55 -04:00
344 changed files with 25339 additions and 24112 deletions
+1
View File
@@ -4,6 +4,7 @@ on:
push: push:
branches: branches:
- dev - dev
- v2
jobs: jobs:
generate: generate:
-1
View File
@@ -251,4 +251,3 @@ opencode-drive stop --name demo
```bash ```bash
opencode-drive dir --name demo opencode-drive dir --name demo
``` ```
+56
View File
@@ -571,6 +571,20 @@
"@typescript/native-preview": "catalog:", "@typescript/native-preview": "catalog:",
}, },
}, },
"packages/merman": {
"name": "@opencode-ai/merman",
"version": "0.0.0",
"dependencies": {
"@opencode-ai/plugin": "workspace:*",
"@opentui/core": "catalog:",
"string-width": "catalog:",
},
"devDependencies": {
"@tsconfig/bun": "catalog:",
"@types/bun": "catalog:",
"@typescript/native-preview": "catalog:",
},
},
"packages/plugin": { "packages/plugin": {
"name": "@opencode-ai/plugin", "name": "@opencode-ai/plugin",
"version": "1.18.8", "version": "1.18.8",
@@ -679,6 +693,7 @@
"@opencode-ai/util": "workspace:*", "@opencode-ai/util": "workspace:*",
"drizzle-orm": "catalog:", "drizzle-orm": "catalog:",
"effect": "catalog:", "effect": "catalog:",
"modal": "0.9.0",
}, },
"devDependencies": { "devDependencies": {
"@tsconfig/bun": "catalog:", "@tsconfig/bun": "catalog:",
@@ -881,6 +896,7 @@
"dependencies": { "dependencies": {
"@opencode-ai/client": "workspace:*", "@opencode-ai/client": "workspace:*",
"@opencode-ai/core": "workspace:*", "@opencode-ai/core": "workspace:*",
"@opencode-ai/merman": "workspace:*",
"@opencode-ai/plugin": "workspace:*", "@opencode-ai/plugin": "workspace:*",
"@opencode-ai/schema": "workspace:*", "@opencode-ai/schema": "workspace:*",
"@opencode-ai/simulation": "workspace:*", "@opencode-ai/simulation": "workspace:*",
@@ -1512,6 +1528,18 @@
"@capsizecss/unpack": ["@capsizecss/unpack@2.4.0", "", { "dependencies": { "blob-to-buffer": "^1.2.8", "cross-fetch": "^3.0.4", "fontkit": "^2.0.2" } }, "sha512-GrSU71meACqcmIUxPYOJvGKF0yryjN/L1aCuE9DViCTJI7bfkjgYDPD1zbNDcINJwSSP6UaBZY9GAbYDO7re0Q=="], "@capsizecss/unpack": ["@capsizecss/unpack@2.4.0", "", { "dependencies": { "blob-to-buffer": "^1.2.8", "cross-fetch": "^3.0.4", "fontkit": "^2.0.2" } }, "sha512-GrSU71meACqcmIUxPYOJvGKF0yryjN/L1aCuE9DViCTJI7bfkjgYDPD1zbNDcINJwSSP6UaBZY9GAbYDO7re0Q=="],
"@cbor-extract/cbor-extract-darwin-arm64": ["@cbor-extract/cbor-extract-darwin-arm64@2.2.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ZKZ/F8US7JR92J4DMct6cLW/Y66o2K576+zjlEN/MevH70bFIsB10wkZEQPLzl2oNh2SMGy55xpJ9JoBRl5DOA=="],
"@cbor-extract/cbor-extract-darwin-x64": ["@cbor-extract/cbor-extract-darwin-x64@2.2.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-32b1mgc+P61Js+KW9VZv/c+xRw5EfmOcPx990JbCBSkYJFY0l25VinvyyWfl+3KjibQmAcYwmyzKF9J4DyKP/Q=="],
"@cbor-extract/cbor-extract-linux-arm": ["@cbor-extract/cbor-extract-linux-arm@2.2.2", "", { "os": "linux", "cpu": "arm" }, "sha512-tNg0za41TpQfkhWjptD+0gSD2fggMiDCSacuIeELyb2xZhr7PrhPe5h66Jc67B/5dmpIhI2QOUtv4SBsricyYQ=="],
"@cbor-extract/cbor-extract-linux-arm64": ["@cbor-extract/cbor-extract-linux-arm64@2.2.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-wfqgzqCAy/Vn8i6WVIh7qZd0DdBFaWBjPdB6ma+Wihcjv0gHqD/mw3ouVv7kbbUNrab6dKEx/w3xQZEdeXIlzg=="],
"@cbor-extract/cbor-extract-linux-x64": ["@cbor-extract/cbor-extract-linux-x64@2.2.2", "", { "os": "linux", "cpu": "x64" }, "sha512-rpiLnVEsqtPJ+mXTdx1rfz4RtUGYIUg2rUAZgd1KjiC1SehYUSkJN7Yh+aVfSjvCGtVP0/bfkQkXpPXKbmSUaA=="],
"@cbor-extract/cbor-extract-win32-x64": ["@cbor-extract/cbor-extract-win32-x64@2.2.2", "", { "os": "win32", "cpu": "x64" }, "sha512-dI+9P7cfWxkTQ+oE+7Aa6onEn92PHgfWXZivjNheCRmTBDBf2fx6RyTi0cmgpYLnD1KLZK9ZYrMxaPZ4oiXhGA=="],
"@chevrotain/types": ["@chevrotain/types@11.1.2", "", {}, "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw=="], "@chevrotain/types": ["@chevrotain/types@11.1.2", "", {}, "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw=="],
"@clack/core": ["@clack/core@1.0.0-alpha.1", "", { "dependencies": { "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-rFbCU83JnN7l3W1nfgCqqme4ZZvTTgsiKQ6FM0l+r0P+o2eJpExcocBUWUIwnDzL76Aca9VhUdWmB2MbUv+Qyg=="], "@clack/core": ["@clack/core@1.0.0-alpha.1", "", { "dependencies": { "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-rFbCU83JnN7l3W1nfgCqqme4ZZvTTgsiKQ6FM0l+r0P+o2eJpExcocBUWUIwnDzL76Aca9VhUdWmB2MbUv+Qyg=="],
@@ -1720,6 +1748,10 @@
"@graphql-typed-document-node/core": ["@graphql-typed-document-node/core@3.2.0", "", { "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ=="], "@graphql-typed-document-node/core": ["@graphql-typed-document-node/core@3.2.0", "", { "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ=="],
"@grpc/grpc-js": ["@grpc/grpc-js@1.14.4", "", { "dependencies": { "@grpc/proto-loader": "^0.8.0", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-k9Dj3DV/itK9D06Y8f190Qgop7/Ui+D0njFV3LHMPwPT75DpXLQohE9Wmz0QElrJnzsjB7KPWiKJbOl7IPDArQ=="],
"@grpc/proto-loader": ["@grpc/proto-loader@0.8.1", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.5.5", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg=="],
"@happy-dom/global-registrator": ["@happy-dom/global-registrator@20.0.11", "", { "dependencies": { "@types/node": "^20.0.0", "happy-dom": "^20.0.11" } }, "sha512-GqNqiShBT/lzkHTMC/slKBrvN0DsD4Di8ssBk4aDaVgEn+2WMzE6DXxq701ndSXj7/0cJ8mNT71pM7Bnrr6JRw=="], "@happy-dom/global-registrator": ["@happy-dom/global-registrator@20.0.11", "", { "dependencies": { "@types/node": "^20.0.0", "happy-dom": "^20.0.11" } }, "sha512-GqNqiShBT/lzkHTMC/slKBrvN0DsD4Di8ssBk4aDaVgEn+2WMzE6DXxq701ndSXj7/0cJ8mNT71pM7Bnrr6JRw=="],
"@hono/node-server": ["@hono/node-server@1.19.15", "", { "peerDependencies": { "hono": "^4" } }, "sha512-Za2ai6TLdKjUvnur+eenO6nuYYipVAEhyCAdaV8IRvmU9kK8crOZUSYvIXn72E4f8fJqyAbpcJuTsYYmZp9Deg=="], "@hono/node-server": ["@hono/node-server@1.19.15", "", { "peerDependencies": { "hono": "^4" } }, "sha512-Za2ai6TLdKjUvnur+eenO6nuYYipVAEhyCAdaV8IRvmU9kK8crOZUSYvIXn72E4f8fJqyAbpcJuTsYYmZp9Deg=="],
@@ -1814,6 +1846,8 @@
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
"@js-sdsl/ordered-map": ["@js-sdsl/ordered-map@4.4.2", "", {}, "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw=="],
"@js-temporal/polyfill": ["@js-temporal/polyfill@0.5.1", "", { "dependencies": { "jsbi": "^4.3.0" } }, "sha512-hloP58zRVCRSpgDxmqCWJNlizAlUgJFqG2ypq79DCvyv9tHjRYMDOcPFjzfl/A1/YxDvRCZz8wvZvmapQnKwFQ=="], "@js-temporal/polyfill": ["@js-temporal/polyfill@0.5.1", "", { "dependencies": { "jsbi": "^4.3.0" } }, "sha512-hloP58zRVCRSpgDxmqCWJNlizAlUgJFqG2ypq79DCvyv9tHjRYMDOcPFjzfl/A1/YxDvRCZz8wvZvmapQnKwFQ=="],
"@jsx-email/all": ["@jsx-email/all@2.2.3", "", { "dependencies": { "@jsx-email/body": "1.0.2", "@jsx-email/button": "1.0.4", "@jsx-email/column": "1.0.3", "@jsx-email/container": "1.0.2", "@jsx-email/font": "1.0.3", "@jsx-email/head": "1.0.2", "@jsx-email/heading": "1.0.2", "@jsx-email/hr": "1.0.2", "@jsx-email/html": "1.0.2", "@jsx-email/img": "1.0.2", "@jsx-email/link": "1.0.2", "@jsx-email/markdown": "2.0.4", "@jsx-email/preview": "1.0.2", "@jsx-email/render": "1.1.1", "@jsx-email/row": "1.0.2", "@jsx-email/section": "1.0.2", "@jsx-email/tailwind": "2.4.4", "@jsx-email/text": "1.0.2" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-OBvLe/hVSQc0LlMSTJnkjFoqs3bmxcC4zpy/5pT5agPCSKMvAKQjzmsc2xJ2wO73jSpRV1K/g38GmvdCfrhSoQ=="], "@jsx-email/all": ["@jsx-email/all@2.2.3", "", { "dependencies": { "@jsx-email/body": "1.0.2", "@jsx-email/button": "1.0.4", "@jsx-email/column": "1.0.3", "@jsx-email/container": "1.0.2", "@jsx-email/font": "1.0.3", "@jsx-email/head": "1.0.2", "@jsx-email/heading": "1.0.2", "@jsx-email/hr": "1.0.2", "@jsx-email/html": "1.0.2", "@jsx-email/img": "1.0.2", "@jsx-email/link": "1.0.2", "@jsx-email/markdown": "2.0.4", "@jsx-email/preview": "1.0.2", "@jsx-email/render": "1.1.1", "@jsx-email/row": "1.0.2", "@jsx-email/section": "1.0.2", "@jsx-email/tailwind": "2.4.4", "@jsx-email/text": "1.0.2" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-OBvLe/hVSQc0LlMSTJnkjFoqs3bmxcC4zpy/5pT5agPCSKMvAKQjzmsc2xJ2wO73jSpRV1K/g38GmvdCfrhSoQ=="],
@@ -2064,6 +2098,8 @@
"@opencode-ai/httpapi-codegen": ["@opencode-ai/httpapi-codegen@workspace:packages/httpapi-codegen"], "@opencode-ai/httpapi-codegen": ["@opencode-ai/httpapi-codegen@workspace:packages/httpapi-codegen"],
"@opencode-ai/merman": ["@opencode-ai/merman@workspace:packages/merman"],
"@opencode-ai/plugin": ["@opencode-ai/plugin@workspace:packages/plugin"], "@opencode-ai/plugin": ["@opencode-ai/plugin@workspace:packages/plugin"],
"@opencode-ai/protocol": ["@opencode-ai/protocol@workspace:packages/protocol"], "@opencode-ai/protocol": ["@opencode-ai/protocol@workspace:packages/protocol"],
@@ -3298,6 +3334,8 @@
"abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="], "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="],
"abort-controller-x": ["abort-controller-x@0.5.0", "", {}, "sha512-yTt9CI0x+nRfX6BFMenEGP8ooPvErGH6AbFz20C2IeOLIlDsrw/VHpgne3GsCEuTA410IiFiaLVFKmgM4bKEPQ=="],
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
@@ -3538,6 +3576,10 @@
"caniuse-lite": ["caniuse-lite@1.0.30001806", "", {}, "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw=="], "caniuse-lite": ["caniuse-lite@1.0.30001806", "", {}, "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw=="],
"cbor-extract": ["cbor-extract@2.2.2", "", { "dependencies": { "node-gyp-build-optional-packages": "5.1.1" }, "optionalDependencies": { "@cbor-extract/cbor-extract-darwin-arm64": "2.2.2", "@cbor-extract/cbor-extract-darwin-x64": "2.2.2", "@cbor-extract/cbor-extract-linux-arm": "2.2.2", "@cbor-extract/cbor-extract-linux-arm64": "2.2.2", "@cbor-extract/cbor-extract-linux-x64": "2.2.2", "@cbor-extract/cbor-extract-win32-x64": "2.2.2" }, "bin": { "download-cbor-prebuilds": "bin/download-prebuilds.js" } }, "sha512-hlSxxI9XO2yQfe9g6msd3g4xCfDqK5T5P0fRMLuaLHhxn4ViPrm+a+MUfhrvH2W962RGxcBwEGzLQyjbDG1gng=="],
"cbor-x": ["cbor-x@1.6.5", "", { "optionalDependencies": { "cbor-extract": "^2.2.2" } }, "sha512-yO64CxnSh6kp+pHNRK9IfwnMvCB+c8HvmUjQY/9l9YRF0/cAPka/tUHLwS64QqUpFCq3/OtbKziVJYXH2EaRig=="],
"ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
"chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="], "chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="],
@@ -4570,6 +4612,8 @@
"lodash-es": ["lodash-es@4.18.1", "", {}, "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A=="], "lodash-es": ["lodash-es@4.18.1", "", {}, "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A=="],
"lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="],
"lodash.escaperegexp": ["lodash.escaperegexp@4.1.2", "", {}, "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw=="], "lodash.escaperegexp": ["lodash.escaperegexp@4.1.2", "", {}, "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw=="],
"lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="], "lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="],
@@ -4796,6 +4840,8 @@
"mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="], "mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="],
"modal": ["modal@0.9.0", "", { "dependencies": { "cbor-x": "^1.6.0", "long": "^5.3.1", "nice-grpc": "^2.1.12", "protobufjs": "^7.5.0", "smol-toml": "^1.3.3", "uuid": "^11.1.0" } }, "sha512-kCXcdJkhbJorf/q/6T9Wdlg6in9JmRnCNQnV6rVBMyeqNV/iXI6BYk4IzY4cvZ6dbauNeDMjk/Q08cbxvoIaXg=="],
"morphdom": ["morphdom@2.7.8", "", {}, "sha512-D/fR4xgGUyVRbdMGU6Nejea1RFzYxYtyurG4Fbv2Fi/daKlWKuXGLOdXtl+3eIwL110cI2hz1ZojGICjjFLgTg=="], "morphdom": ["morphdom@2.7.8", "", {}, "sha512-D/fR4xgGUyVRbdMGU6Nejea1RFzYxYtyurG4Fbv2Fi/daKlWKuXGLOdXtl+3eIwL110cI2hz1ZojGICjjFLgTg=="],
"motion": ["motion@12.34.5", "", { "dependencies": { "framer-motion": "^12.34.5", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-N06NLJ9IeBHeielRqIvYvjPfXuRdyTxa+9++BgpGa+hY2D7TcMkI6QzV3jaRuv0aZRXgMa7cPy9YcBUBisPzAQ=="], "motion": ["motion@12.34.5", "", { "dependencies": { "framer-motion": "^12.34.5", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-N06NLJ9IeBHeielRqIvYvjPfXuRdyTxa+9++BgpGa+hY2D7TcMkI6QzV3jaRuv0aZRXgMa7cPy9YcBUBisPzAQ=="],
@@ -4834,6 +4880,10 @@
"nf3": ["nf3@0.1.12", "", {}, "sha512-qbMXT7RTGh74MYWPeqTIED8nDW70NXOULVHpdWcdZ7IVHVnAsMV9fNugSNnvooipDc1FMOzpis7T9nXJEbJhvQ=="], "nf3": ["nf3@0.1.12", "", {}, "sha512-qbMXT7RTGh74MYWPeqTIED8nDW70NXOULVHpdWcdZ7IVHVnAsMV9fNugSNnvooipDc1FMOzpis7T9nXJEbJhvQ=="],
"nice-grpc": ["nice-grpc@2.1.16", "", { "dependencies": { "@grpc/grpc-js": "^1.14.0", "abort-controller-x": "^0.5.0", "nice-grpc-common": "^2.0.3" } }, "sha512-Cl3Pn00212Hl8/U6bpgMxmhZj5lyv3nWoJov4cd3FjWarktrMHP4DNvSjCnDwkMWYx4W1tyscEia4JX6Y4GVCQ=="],
"nice-grpc-common": ["nice-grpc-common@2.0.3", "", { "dependencies": { "ts-error": "^1.0.6" } }, "sha512-MEhnD3JMah0mgyivpb9hpRDbOBuXBxI/TVO+OK1h6rC97WM42HsPMR+zzRNQ0C5BqYJTw1nyWiQRD0DucO+pjQ=="],
"nitro": ["nitro@3.0.1-alpha.1", "", { "dependencies": { "consola": "^3.4.2", "crossws": "^0.4.1", "db0": "^0.3.4", "h3": "2.0.1-rc.5", "jiti": "^2.6.1", "nf3": "^0.1.10", "ofetch": "^2.0.0-alpha.3", "ohash": "^2.0.11", "oxc-minify": "^0.96.0", "oxc-transform": "^0.96.0", "srvx": "^0.9.5", "undici": "^7.16.0", "unenv": "^2.0.0-rc.24", "unstorage": "^2.0.0-alpha.4" }, "peerDependencies": { "rolldown": "*", "rollup": "^4", "vite": "^7", "xml2js": "^0.6.2" }, "optionalPeers": ["rolldown", "rollup", "vite", "xml2js"], "bin": { "nitro": "dist/cli/index.mjs" } }, "sha512-U4AxIsXxdkxzkFrK0XAw0e5Qbojk8jQ50MjjRBtBakC4HurTtQoiZvF+lSe382jhuQZCfAyywGWOFa9QzXLFaw=="], "nitro": ["nitro@3.0.1-alpha.1", "", { "dependencies": { "consola": "^3.4.2", "crossws": "^0.4.1", "db0": "^0.3.4", "h3": "2.0.1-rc.5", "jiti": "^2.6.1", "nf3": "^0.1.10", "ofetch": "^2.0.0-alpha.3", "ohash": "^2.0.11", "oxc-minify": "^0.96.0", "oxc-transform": "^0.96.0", "srvx": "^0.9.5", "undici": "^7.16.0", "unenv": "^2.0.0-rc.24", "unstorage": "^2.0.0-alpha.4" }, "peerDependencies": { "rolldown": "*", "rollup": "^4", "vite": "^7", "xml2js": "^0.6.2" }, "optionalPeers": ["rolldown", "rollup", "vite", "xml2js"], "bin": { "nitro": "dist/cli/index.mjs" } }, "sha512-U4AxIsXxdkxzkFrK0XAw0e5Qbojk8jQ50MjjRBtBakC4HurTtQoiZvF+lSe382jhuQZCfAyywGWOFa9QzXLFaw=="],
"nlcst-to-string": ["nlcst-to-string@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0" } }, "sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA=="], "nlcst-to-string": ["nlcst-to-string@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0" } }, "sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA=="],
@@ -5632,6 +5682,8 @@
"ts-dedent": ["ts-dedent@2.3.0", "", {}, "sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg=="], "ts-dedent": ["ts-dedent@2.3.0", "", {}, "sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg=="],
"ts-error": ["ts-error@1.0.6", "", {}, "sha512-tLJxacIQUM82IR7JO1UUkKlYuUTmoY9HBJAmNWFzheSlDS5SPMcNIepejHJa4BpPQLAcbRhRf3GDJzyj6rbKvA=="],
"ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="], "ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="],
"tsconfck": ["tsconfck@3.1.6", "", { "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"], "bin": { "tsconfck": "bin/tsconfck.js" } }, "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w=="], "tsconfck": ["tsconfck@3.1.6", "", { "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"], "bin": { "tsconfck": "bin/tsconfck.js" } }, "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w=="],
@@ -6590,6 +6642,8 @@
"builder-util/js-yaml": ["js-yaml@4.3.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q=="], "builder-util/js-yaml": ["js-yaml@4.3.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q=="],
"cbor-extract/node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.1.1", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-test": "build-test.js", "node-gyp-build-optional-packages-optional": "optional.js" } }, "sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw=="],
"cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
@@ -6734,6 +6788,8 @@
"minipass-pipeline/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], "minipass-pipeline/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
"modal/uuid": ["uuid@11.1.1", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ=="],
"motion/framer-motion": ["framer-motion@12.42.2", "", { "dependencies": { "motion-dom": "^12.42.2", "motion-utils": "^12.39.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-5XY9luDiu0oHfHBjpDthFMh0ES+122w6p/papSJBweMkO8Sn+PW2QaEgRblQBpWFnuvZS5qvarpt/hO2pjGmnw=="], "motion/framer-motion": ["framer-motion@12.42.2", "", { "dependencies": { "motion-dom": "^12.42.2", "motion-utils": "^12.39.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-5XY9luDiu0oHfHBjpDthFMh0ES+122w6p/papSJBweMkO8Sn+PW2QaEgRblQBpWFnuvZS5qvarpt/hO2pjGmnw=="],
"nitro/h3": ["h3@2.0.1-rc.5", "", { "dependencies": { "rou3": "^0.7.9", "srvx": "^0.9.1" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"] }, "sha512-qkohAzCab0nLzXNm78tBjZDvtKMTmtygS8BJLT3VPczAQofdqlFXDPkXdLMJN4r05+xqneG8snZJ0HgkERCZTg=="], "nitro/h3": ["h3@2.0.1-rc.5", "", { "dependencies": { "rou3": "^0.7.9", "srvx": "^0.9.1" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"] }, "sha512-qkohAzCab0nLzXNm78tBjZDvtKMTmtygS8BJLT3VPczAQofdqlFXDPkXdLMJN4r05+xqneG8snZJ0HgkERCZTg=="],
+2 -1
View File
@@ -133,7 +133,8 @@ const countHints = (request: LLMRequest) =>
export const applyCachePolicy = (request: LLMRequest): LLMRequest => { export const applyCachePolicy = (request: LLMRequest): LLMRequest => {
if (!RESPECTS_INLINE_HINTS.has(request.model.route.id)) return request if (!RESPECTS_INLINE_HINTS.has(request.model.route.id)) return request
if (request.model.route.id === "openrouter" && (request.cache === undefined || request.cache === "auto")) return request if (request.model.route.id === "openrouter" && (request.cache === undefined || request.cache === "auto"))
return request
const policy = resolve(request.cache) const policy = resolve(request.cache)
if (!policy.tools && !policy.system && !policy.messages) return request if (!policy.tools && !policy.system && !policy.messages) return request
+18 -17
View File
@@ -371,12 +371,13 @@ const lowerAssistantMessage = Effect.fn("OpenAIChat.lowerAssistantMessage")(func
return text return text
})() })()
const cached = message.content.findLast((part) => "cache" in part && part.cache !== undefined) const cached = message.content.findLast((part) => "cache" in part && part.cache !== undefined)
const cacheControl = options.cacheControl?.(cached && "cache" in cached ? cached.cache : undefined)
const result = { const result = {
role: "assistant" as const, role: "assistant" as const,
content: content.length === 0 ? null : ProviderShared.joinText(content), content: content.length > 0 ? content.map((part) => part.text).join("") : toolCalls.length > 0 ? null : "",
tool_calls: toolCalls.length === 0 ? undefined : toolCalls, ...(toolCalls.length > 0 ? { tool_calls: toolCalls } : {}),
reasoning_details: details, ...(details !== undefined ? { reasoning_details: details } : {}),
cache_control: options.cacheControl?.(cached && "cache" in cached ? cached.cache : undefined), ...(cacheControl !== undefined ? { cache_control: cacheControl } : {}),
} }
if (field === undefined || reasoningText === undefined) return result if (field === undefined || reasoningText === undefined) return result
return { ...result, [field]: reasoningText } return { ...result, [field]: reasoningText }
@@ -709,23 +710,20 @@ const step = (state: ParserState, event: OpenAIChatEvent) =>
Boolean(delta?.content) || Boolean(delta?.content) ||
reasoning !== undefined || reasoning !== undefined ||
(Array.isArray(delta?.reasoning_details) && delta.reasoning_details.length > 0) || (Array.isArray(delta?.reasoning_details) && delta.reasoning_details.length > 0) ||
toolDeltas.some( toolDeltas.some((tool) => Boolean(tool.id) || Boolean(tool.function?.name) || Boolean(tool.function?.arguments))
(tool) => Boolean(tool.id) || Boolean(tool.function?.name) || Boolean(tool.function?.arguments),
)
if (state.finishReason !== undefined) { if (state.finishReason !== undefined) {
if (hasLateContent) if (hasLateContent)
return yield* ProviderShared.eventError(ADAPTER, "OpenAI Chat received content after the finish reason") return yield* ProviderShared.eventError(ADAPTER, "OpenAI Chat received content after the finish reason")
return [{ ...state, usage }, events] as const return [{ ...state, usage }, events] as const
} }
const reasoningField = state.reasoningField ?? (!state.lifecycle.text.has("text-0") ? reasoning?.field : undefined) const reasoningField = state.reasoningField ?? reasoning?.field
const detailDelta = Array.isArray(delta?.reasoning_details) ? delta.reasoning_details : undefined const detailDelta = Array.isArray(delta?.reasoning_details) ? delta.reasoning_details : undefined
if (detailDelta !== undefined) appendReasoningDetails(state.reasoningDetails, detailDelta) if (detailDelta !== undefined) appendReasoningDetails(state.reasoningDetails, detailDelta)
const reasoningDetailsObserved = state.reasoningDetailsObserved || detailDelta !== undefined const reasoningDetailsObserved = state.reasoningDetailsObserved || detailDelta !== undefined
const deltaMetadata = reasoningMetadata(reasoningField) const deltaMetadata = reasoningMetadata(reasoningField)
const text = detailDelta?.length ? (detailText(detailDelta) ?? reasoning?.text) : reasoning?.text const text = detailDelta?.length ? (detailText(detailDelta) ?? reasoning?.text) : reasoning?.text
if (!state.lifecycle.text.has("text-0") && text !== undefined) if (text !== undefined) lifecycle = Lifecycle.reasoningDelta(lifecycle, events, "reasoning-0", text, deltaMetadata)
lifecycle = Lifecycle.reasoningDelta(lifecycle, events, "reasoning-0", text, deltaMetadata)
else if ( else if (
reasoningDetailsObserved && reasoningDetailsObserved &&
!lifecycle.reasoning.has("reasoning-0") && !lifecycle.reasoning.has("reasoning-0") &&
@@ -751,8 +749,7 @@ const step = (state: ParserState, event: OpenAIChatEvent) =>
const fallback = toolDeltas.length > 1 ? position : (latestToolIndex ?? position) const fallback = toolDeltas.length > 1 ? position : (latestToolIndex ?? position)
const fallbackTool = tools[fallback] ?? pendingTools[fallback] const fallbackTool = tools[fallback] ?? pendingTools[fallback]
const index = const index =
tool.index ?? matched ?? tool.index ?? matched ?? (tool.id && fallbackTool?.id && fallbackTool.id !== tool.id ? nextToolIndex : fallback)
(tool.id && fallbackTool?.id && fallbackTool.id !== tool.id ? nextToolIndex : fallback)
const current = tools[index] const current = tools[index]
const pending = pendingTools[index] const pending = pendingTools[index]
const id = current?.id ?? pending?.id ?? (tool.id || undefined) const id = current?.id ?? pending?.id ?? (tool.id || undefined)
@@ -815,14 +812,18 @@ const step = (state: ParserState, event: OpenAIChatEvent) =>
const finishEvents = (state: ParserState): ReadonlyArray<LLMEvent> => { const finishEvents = (state: ParserState): ReadonlyArray<LLMEvent> => {
const events: LLMEvent[] = [] const events: LLMEvent[] = []
const hasToolCalls = state.toolCallEvents.length > 0 const toolCallEvents =
state.finishReason === undefined && Object.keys(state.tools).length > 0
? Effect.runSync(ToolStream.finishAll(ADAPTER, state.tools)).events
: state.toolCallEvents
const hasToolCalls = toolCallEvents.length > 0
const reason = state.finishReason const reason = state.finishReason
? { ? {
...state.finishReason, ...state.finishReason,
normalized: normalized:
state.finishReason.normalized === "stop" && hasToolCalls ? "tool-calls" : state.finishReason.normalized, state.finishReason.normalized === "stop" && hasToolCalls ? "tool-calls" : state.finishReason.normalized,
} }
: undefined : { normalized: hasToolCalls ? ("tool-calls" as const) : ("unknown" as const) }
const metadata = reasoningMetadata( const metadata = reasoningMetadata(
state.reasoningField, state.reasoningField,
state.reasoningDetailsObserved ? state.reasoningDetails : undefined, state.reasoningDetailsObserved ? state.reasoningDetails : undefined,
@@ -832,9 +833,9 @@ const finishEvents = (state: ParserState): ReadonlyArray<LLMEvent> => {
? Lifecycle.reasoningStart(state.lifecycle, events, "reasoning-0", reasoningMetadata(state.reasoningField)) ? Lifecycle.reasoningStart(state.lifecycle, events, "reasoning-0", reasoningMetadata(state.reasoningField))
: state.lifecycle : state.lifecycle
const ended = Lifecycle.reasoningEnd(started, events, "reasoning-0", metadata) const ended = Lifecycle.reasoningEnd(started, events, "reasoning-0", metadata)
const lifecycle = state.toolCallEvents.length ? Lifecycle.stepStart(ended, events) : ended const lifecycle = toolCallEvents.length ? Lifecycle.stepStart(ended, events) : ended
events.push(...state.toolCallEvents) events.push(...toolCallEvents)
if (reason) Lifecycle.finish(lifecycle, events, { reason, usage: state.usage }) Lifecycle.finish(lifecycle, events, { reason, usage: state.usage })
return events return events
} }
+1 -7
View File
@@ -255,13 +255,7 @@ const requireTerminalEvent = (route: string) => (events: Stream.Stream<LLMEvent,
if (LLMEvent.is.finish(event) || LLMEvent.is.providerError(event)) terminal = true if (LLMEvent.is.finish(event) || LLMEvent.is.providerError(event)) terminal = true
return Effect.succeed(event) return Effect.succeed(event)
}), }),
Stream.onEnd( Stream.onEnd(Effect.suspend(() => (terminal ? Effect.void : Effect.fail(incompleteStreamError(route))))),
Effect.suspend(() =>
terminal
? Effect.void
: Effect.fail(incompleteStreamError(route)),
),
),
) )
}) })
+14 -2
View File
@@ -1,4 +1,4 @@
import { Cause, Context, Effect, Layer } from "effect" import { Cause, Context, Effect, Layer, Option, Schema } from "effect"
import { import {
FetchHttpClient, FetchHttpClient,
Headers, Headers,
@@ -198,8 +198,20 @@ const responseBody = (body: string | void, request: HttpClientRequest.HttpClient
return { body: redacted.slice(0, BODY_LIMIT), bodyTruncated: true } return { body: redacted.slice(0, BODY_LIMIT), bodyTruncated: true }
} }
const decodeProviderBody = Schema.decodeUnknownOption(
Schema.fromJsonString(
Schema.Struct({
message: Schema.optionalKey(Schema.String),
error: Schema.optionalKey(Schema.Struct({ message: Schema.optionalKey(Schema.String) })),
}),
),
)
const providerMessage = (status: number, body: { readonly body?: string }) => { const providerMessage = (status: number, body: { readonly body?: string }) => {
if (body.body && body.body.length <= 500) return `Provider request failed with HTTP ${status}: ${body.body}` if (body.body && body.body.length <= 500) {
const decoded = Option.getOrUndefined(decodeProviderBody(body.body))
return `Provider request failed with HTTP ${status}: ${decoded?.error?.message ?? decoded?.message ?? body.body}`
}
return `Provider request failed with HTTP ${status}` return `Provider request failed with HTTP ${status}`
} }
+2 -7
View File
@@ -1,10 +1,6 @@
import { Effect, JsonSchema, Schema } from "effect" import { Effect, JsonSchema, Schema } from "effect"
import { Tool } from "@opencode-ai/schema/tool" import { Tool } from "@opencode-ai/schema/tool"
import type { import type { ToolCallPart, ToolDefinition as ToolDefinitionClass, ToolOutput as ToolOutputType } from "./schema"
ToolCallPart,
ToolDefinition as ToolDefinitionClass,
ToolOutput as ToolOutputType,
} from "./schema"
import { ToolDefinition, ToolFailure, ToolOutput } from "./schema" import { ToolDefinition, ToolFailure, ToolOutput } from "./schema"
/** /**
@@ -244,8 +240,7 @@ const project = (
): ToolOutputType => ): ToolOutputType =>
ToolOutput.make( ToolOutput.make(
toStructuredOutput?.(output) ?? output, toStructuredOutput?.(output) ?? output,
toModelOutput?.({ id, parameters, output }) ?? toModelOutput?.({ id, parameters, output }) ?? (typeof output === "string" ? [{ type: "text", text: output }] : []),
(typeof output === "string" ? [{ type: "text", text: output }] : []),
) )
export { ToolFailure } export { ToolFailure }
@@ -1,13 +1,7 @@
{ {
"version": 1, "version": 1,
"metadata": { "metadata": {
"tags": [ "tags": ["prefix:anthropic-messages-cache", "provider:anthropic", "protocol:anthropic-messages", "cache", "tool"],
"prefix:anthropic-messages-cache",
"provider:anthropic",
"protocol:anthropic-messages",
"cache",
"tool"
],
"name": "anthropic-messages-cache/keeps-a-long-tool-turn-inside-the-cache-lookback", "name": "anthropic-messages-cache/keeps-a-long-tool-turn-inside-the-cache-lookback",
"recordedAt": "2026-07-24T16:22:29.494Z" "recordedAt": "2026-07-24T16:22:29.494Z"
}, },
@@ -1,12 +1,7 @@
{ {
"version": 1, "version": 1,
"metadata": { "metadata": {
"tags": [ "tags": ["prefix:bedrock-converse-cache", "provider:amazon-bedrock", "protocol:bedrock-converse", "cache"],
"prefix:bedrock-converse-cache",
"provider:amazon-bedrock",
"protocol:bedrock-converse",
"cache"
],
"name": "bedrock-converse-cache/writes-then-reads-cachepoint-on-identical-second-call", "name": "bedrock-converse-cache/writes-then-reads-cachepoint-on-identical-second-call",
"recordedAt": "2026-07-23T02:29:10.955Z" "recordedAt": "2026-07-23T02:29:10.955Z"
}, },
@@ -1,11 +1,7 @@
{ {
"version": 1, "version": 1,
"metadata": { "metadata": {
"tags": [ "tags": ["prefix:google-images", "provider:google", "protocol:google-images"],
"prefix:google-images",
"provider:google",
"protocol:google-images"
],
"name": "google-images/generates-an-image", "name": "google-images/generates-an-image",
"recordedAt": "2026-07-19T16:05:51.868Z" "recordedAt": "2026-07-19T16:05:51.868Z"
}, },
@@ -1,11 +1,7 @@
{ {
"version": 1, "version": 1,
"metadata": { "metadata": {
"tags": [ "tags": ["prefix:openai-images", "provider:openai", "protocol:openai-images"],
"prefix:openai-images",
"provider:openai",
"protocol:openai-images"
],
"name": "openai-images/generates-an-image", "name": "openai-images/generates-an-image",
"recordedAt": "2026-07-19T14:41:43.188Z" "recordedAt": "2026-07-19T14:41:43.188Z"
}, },
@@ -1,11 +1,7 @@
{ {
"version": 1, "version": 1,
"metadata": { "metadata": {
"tags": [ "tags": ["prefix:openai-responses-images", "provider:openai", "protocol:openai-responses"],
"prefix:openai-responses-images",
"provider:openai",
"protocol:openai-responses"
],
"name": "openai-responses-images/generates-and-edits-an-image-with-the-hosted-tool", "name": "openai-responses-images/generates-and-edits-an-image-with-the-hosted-tool",
"recordedAt": "2026-07-19T14:57:16.284Z" "recordedAt": "2026-07-19T14:57:16.284Z"
}, },
@@ -2,12 +2,7 @@
"version": 1, "version": 1,
"metadata": { "metadata": {
"model": "anthropic/claude-sonnet-4.6", "model": "anthropic/claude-sonnet-4.6",
"tags": [ "tags": ["prefix:openai-compatible-chat", "provider:openrouter", "protocol:openai-chat", "reasoning"],
"prefix:openai-compatible-chat",
"provider:openrouter",
"protocol:openai-chat",
"reasoning"
],
"name": "openrouter-reasoning", "name": "openrouter-reasoning",
"recordedAt": "2026-07-18T11:28:39.267Z" "recordedAt": "2026-07-18T11:28:39.267Z"
}, },
@@ -1,14 +1,7 @@
{ {
"version": 1, "version": 1,
"metadata": { "metadata": {
"tags": [ "tags": ["prefix:pdf", "pdf", "provider:anthropic", "protocol:anthropic-messages", "tool", "tool-result"],
"prefix:pdf",
"pdf",
"provider:anthropic",
"protocol:anthropic-messages",
"tool",
"tool-result"
],
"name": "pdf/anthropic-tool-result", "name": "pdf/anthropic-tool-result",
"recordedAt": "2026-07-22T18:15:39.002Z" "recordedAt": "2026-07-22T18:15:39.002Z"
}, },
@@ -1,13 +1,7 @@
{ {
"version": 1, "version": 1,
"metadata": { "metadata": {
"tags": [ "tags": ["prefix:pdf", "pdf", "provider:anthropic", "protocol:anthropic-messages", "user-input"],
"prefix:pdf",
"pdf",
"provider:anthropic",
"protocol:anthropic-messages",
"user-input"
],
"name": "pdf/anthropic-user-input", "name": "pdf/anthropic-user-input",
"recordedAt": "2026-07-22T18:15:37.979Z" "recordedAt": "2026-07-22T18:15:37.979Z"
}, },
@@ -1,14 +1,7 @@
{ {
"version": 1, "version": 1,
"metadata": { "metadata": {
"tags": [ "tags": ["prefix:pdf", "pdf", "provider:amazon-bedrock", "protocol:bedrock-converse", "tool", "tool-result"],
"prefix:pdf",
"pdf",
"provider:amazon-bedrock",
"protocol:bedrock-converse",
"tool",
"tool-result"
],
"name": "pdf/bedrock-tool-result", "name": "pdf/bedrock-tool-result",
"recordedAt": "2026-07-22T18:15:52.400Z" "recordedAt": "2026-07-22T18:15:52.400Z"
}, },
@@ -1,13 +1,7 @@
{ {
"version": 1, "version": 1,
"metadata": { "metadata": {
"tags": [ "tags": ["prefix:pdf", "pdf", "provider:amazon-bedrock", "protocol:bedrock-converse", "user-input"],
"prefix:pdf",
"pdf",
"provider:amazon-bedrock",
"protocol:bedrock-converse",
"user-input"
],
"name": "pdf/bedrock-user-input", "name": "pdf/bedrock-user-input",
"recordedAt": "2026-07-22T18:15:48.408Z" "recordedAt": "2026-07-22T18:15:48.408Z"
}, },
@@ -1,14 +1,7 @@
{ {
"version": 1, "version": 1,
"metadata": { "metadata": {
"tags": [ "tags": ["prefix:pdf", "pdf", "provider:google", "protocol:gemini", "tool", "tool-result"],
"prefix:pdf",
"pdf",
"provider:google",
"protocol:gemini",
"tool",
"tool-result"
],
"name": "pdf/gemini-tool-result", "name": "pdf/gemini-tool-result",
"recordedAt": "2026-07-22T18:21:59.606Z" "recordedAt": "2026-07-22T18:21:59.606Z"
}, },
@@ -1,13 +1,7 @@
{ {
"version": 1, "version": 1,
"metadata": { "metadata": {
"tags": [ "tags": ["prefix:pdf", "pdf", "provider:google", "protocol:gemini", "user-input"],
"prefix:pdf",
"pdf",
"provider:google",
"protocol:gemini",
"user-input"
],
"name": "pdf/gemini-user-input", "name": "pdf/gemini-user-input",
"recordedAt": "2026-07-22T18:20:55.140Z" "recordedAt": "2026-07-22T18:20:55.140Z"
}, },
@@ -1,14 +1,7 @@
{ {
"version": 1, "version": 1,
"metadata": { "metadata": {
"tags": [ "tags": ["prefix:pdf", "pdf", "provider:openai", "protocol:openai-responses", "tool", "tool-result"],
"prefix:pdf",
"pdf",
"provider:openai",
"protocol:openai-responses",
"tool",
"tool-result"
],
"name": "pdf/openai-tool-result", "name": "pdf/openai-tool-result",
"recordedAt": "2026-07-22T18:15:36.438Z" "recordedAt": "2026-07-22T18:15:36.438Z"
}, },
@@ -1,13 +1,7 @@
{ {
"version": 1, "version": 1,
"metadata": { "metadata": {
"tags": [ "tags": ["prefix:pdf", "pdf", "provider:openai", "protocol:openai-responses", "user-input"],
"prefix:pdf",
"pdf",
"provider:openai",
"protocol:openai-responses",
"user-input"
],
"name": "pdf/openai-user-input", "name": "pdf/openai-user-input",
"recordedAt": "2026-07-22T18:15:34.867Z" "recordedAt": "2026-07-22T18:15:34.867Z"
}, },
@@ -1,14 +1,7 @@
{ {
"version": 1, "version": 1,
"metadata": { "metadata": {
"tags": [ "tags": ["prefix:pdf", "pdf", "provider:xai", "protocol:openai-responses", "tool", "tool-result"],
"prefix:pdf",
"pdf",
"provider:xai",
"protocol:openai-responses",
"tool",
"tool-result"
],
"name": "pdf/xai-tool-result", "name": "pdf/xai-tool-result",
"recordedAt": "2026-07-22T18:15:43.608Z" "recordedAt": "2026-07-22T18:15:43.608Z"
}, },
@@ -1,13 +1,7 @@
{ {
"version": 1, "version": 1,
"metadata": { "metadata": {
"tags": [ "tags": ["prefix:pdf", "pdf", "provider:xai", "protocol:openai-responses", "user-input"],
"prefix:pdf",
"pdf",
"provider:xai",
"protocol:openai-responses",
"user-input"
],
"name": "pdf/xai-user-input", "name": "pdf/xai-user-input",
"recordedAt": "2026-07-22T18:15:42.429Z" "recordedAt": "2026-07-22T18:15:42.429Z"
}, },
@@ -2,12 +2,7 @@
"version": 1, "version": 1,
"metadata": { "metadata": {
"model": "anthropic/claude-sonnet-4.6", "model": "anthropic/claude-sonnet-4.6",
"tags": [ "tags": ["prefix:openai-compatible-chat", "provider:vercel-ai-gateway", "protocol:openai-chat", "reasoning"],
"prefix:openai-compatible-chat",
"provider:vercel-ai-gateway",
"protocol:openai-chat",
"reasoning"
],
"name": "vercel-ai-gateway-reasoning", "name": "vercel-ai-gateway-reasoning",
"recordedAt": "2026-07-18T11:28:42.077Z" "recordedAt": "2026-07-18T11:28:42.077Z"
}, },
@@ -1,11 +1,7 @@
{ {
"version": 1, "version": 1,
"metadata": { "metadata": {
"tags": [ "tags": ["prefix:xai-images", "provider:xai", "protocol:xai-images"],
"prefix:xai-images",
"provider:xai",
"protocol:xai-images"
],
"name": "xai-images/generates-an-image", "name": "xai-images/generates-an-image",
"recordedAt": "2026-07-19T15:56:20.098Z" "recordedAt": "2026-07-19T15:56:20.098Z"
}, },
@@ -519,14 +519,8 @@ describe("Bedrock Converse route", () => {
fixedBytes( fixedBytes(
eventStreamBody( eventStreamBody(
["messageStart", { role: "assistant" }], ["messageStart", { role: "assistant" }],
[ ["contentBlockDelta", { contentBlockIndex: 0, delta: { reasoningContent: { text: "Let me think." } } }],
"contentBlockDelta", ["contentBlockDelta", { contentBlockIndex: 0, delta: { reasoningContent: { signature: "sig_1" } } }],
{ contentBlockIndex: 0, delta: { reasoningContent: { text: "Let me think." } } },
],
[
"contentBlockDelta",
{ contentBlockIndex: 0, delta: { reasoningContent: { signature: "sig_1" } } },
],
["messageStop", { stopReason: "end_turn" }], ["messageStop", { stopReason: "end_turn" }],
), ),
), ),
@@ -561,10 +555,7 @@ describe("Bedrock Converse route", () => {
Effect.gen(function* () { Effect.gen(function* () {
const body = eventStreamBody( const body = eventStreamBody(
["messageStart", { role: "assistant" }], ["messageStart", { role: "assistant" }],
[ ["contentBlockDelta", { contentBlockIndex: 0, delta: { reasoningContent: { signature: "sig_1" } } }],
"contentBlockDelta",
{ contentBlockIndex: 0, delta: { reasoningContent: { signature: "sig_1" } } },
],
["contentBlockStop", { contentBlockIndex: 0 }], ["contentBlockStop", { contentBlockIndex: 0 }],
["messageStop", { stopReason: "end_turn" }], ["messageStop", { stopReason: "end_turn" }],
) )
+48 -26
View File
@@ -102,6 +102,24 @@ describe("OpenAI Chat route", () => {
}), }),
) )
it.effect("concatenates assistant text parts without adding separators", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({
model,
messages: [
Message.assistant([
{ type: "text", text: "Hello" },
{ type: "text", text: " world" },
]),
],
}),
)
expect(prepared.body.messages).toEqual([{ role: "assistant", content: "Hello world" }])
}),
)
it.effect("writes reasoning to a configured custom field on every assistant message", () => it.effect("writes reasoning to a configured custom field on every assistant message", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* compileRequest(
@@ -578,7 +596,7 @@ describe("OpenAI Chat route", () => {
}), }),
) )
expect(prepared.body.messages).toEqual([{ role: "assistant", content: null, reasoning_content: "hidden" }]) expect(prepared.body.messages).toEqual([{ role: "assistant", content: "", reasoning_content: "hidden" }])
}), }),
) )
@@ -827,7 +845,7 @@ describe("OpenAI Chat route", () => {
}), }),
) )
it.effect("ignores scalar reasoning after content starts", () => it.effect("preserves scalar reasoning after content starts", () =>
Effect.gen(function* () { Effect.gen(function* () {
const details = [{ type: "reasoning.text", text: "detail", format: "unknown", index: 0 }] const details = [{ type: "reasoning.text", text: "detail", format: "unknown", index: 0 }]
const response = yield* LLMClient.generate(request).pipe( const response = yield* LLMClient.generate(request).pipe(
@@ -843,11 +861,11 @@ describe("OpenAI Chat route", () => {
), ),
) )
expect(response.reasoning).toBe("detail") expect(response.reasoning).toBe("detailscalar")
expect(response.events.filter(LLMEvent.is.reasoningStart)).toHaveLength(1) expect(response.events.filter(LLMEvent.is.reasoningStart)).toHaveLength(2)
expect(response.events.filter(LLMEvent.is.reasoningEnd)).toHaveLength(1) expect(response.events.filter(LLMEvent.is.reasoningEnd)).toHaveLength(2)
expect(response.message.content.find((part) => part.type === "reasoning")?.providerMetadata).toEqual({ expect(response.message.content.find((part) => part.type === "reasoning")?.providerMetadata).toEqual({
openai: { reasoningDetails: details }, openai: { reasoningField: "reasoning", reasoningDetails: details },
}) })
}), }),
) )
@@ -947,7 +965,7 @@ describe("OpenAI Chat route", () => {
expect(response.events.filter(LLMEvent.is.reasoningEnd)).toHaveLength(1) expect(response.events.filter(LLMEvent.is.reasoningEnd)).toHaveLength(1)
const replay = yield* compileRequest(LLM.request({ model, messages: [response.message] })) const replay = yield* compileRequest(LLM.request({ model, messages: [response.message] }))
expect(replay.body.messages).toEqual([{ role: "assistant", content: null, reasoning_details: details }]) expect(replay.body.messages).toEqual([{ role: "assistant", content: "", reasoning_details: details }])
}), }),
) )
@@ -997,7 +1015,7 @@ describe("OpenAI Chat route", () => {
) )
expect(replay.body.messages).toEqual([ expect(replay.body.messages).toEqual([
{ role: "assistant", content: null, reasoning: "firstsecond", reasoning_details: [first, second] }, { role: "assistant", content: "", reasoning: "firstsecond", reasoning_details: [first, second] },
]) ])
}), }),
) )
@@ -1022,7 +1040,7 @@ describe("OpenAI Chat route", () => {
) )
expect(replay.body.messages).toEqual([ expect(replay.body.messages).toEqual([
{ role: "assistant", content: null, reasoning_content: "AB", reasoning_details: [detail] }, { role: "assistant", content: "", reasoning_content: "AB", reasoning_details: [detail] },
]) ])
}), }),
) )
@@ -1044,7 +1062,7 @@ describe("OpenAI Chat route", () => {
) )
expect(replay.body.messages).toEqual([ expect(replay.body.messages).toEqual([
{ role: "assistant", content: null, reasoning_content: "thinking", reasoning_details: details }, { role: "assistant", content: "", reasoning_content: "thinking", reasoning_details: details },
]) ])
}), }),
) )
@@ -1152,7 +1170,7 @@ describe("OpenAI Chat route", () => {
}), }),
) )
it.effect("fails a streamed tool call when the provider ends without a finish reason", () => it.effect("finalizes a streamed tool call when the provider ends without a finish reason", () =>
Effect.gen(function* () { Effect.gen(function* () {
const body = sseEvents( const body = sseEvents(
deltaChunk({ deltaChunk({
@@ -1164,27 +1182,31 @@ describe("OpenAI Chat route", () => {
const input = LLMRequest.update(request, { const input = LLMRequest.update(request, {
tools: [ToolDefinition.make({ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } })], tools: [ToolDefinition.make({ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } })],
}) })
const events: LLMEvent[] = [] const response = yield* LLMClient.generate(input).pipe(Effect.provide(fixedResponse(body)))
const streamError = yield* LLMClient.stream(input).pipe(
Stream.runForEach((event) => Effect.sync(() => events.push(event))),
Effect.flip,
Effect.provide(fixedResponse(body)),
)
const error = yield* LLMClient.generate(input).pipe(Effect.provide(fixedResponse(body)), Effect.flip)
expect(events).toEqual([ expect(response.events).toEqual([
{ type: "step-start", index: 0 }, { type: "step-start", index: 0 },
{ type: "tool-input-start", id: "call_1", name: "lookup", providerMetadata: undefined }, { type: "tool-input-start", id: "call_1", name: "lookup", providerMetadata: undefined },
{ type: "tool-input-delta", id: "call_1", name: "lookup", text: '{"query"' }, { type: "tool-input-delta", id: "call_1", name: "lookup", text: '{"query"' },
{ type: "tool-input-delta", id: "call_1", name: "lookup", text: ':"weather"}' }, { type: "tool-input-delta", id: "call_1", name: "lookup", text: ':"weather"}' },
{ type: "tool-input-end", id: "call_1", name: "lookup", providerMetadata: undefined },
{
type: "tool-call",
id: "call_1",
name: "lookup",
input: { query: "weather" },
providerExecuted: undefined,
providerMetadata: undefined,
},
{
type: "step-finish",
index: 0,
reason: { normalized: "tool-calls" },
usage: undefined,
providerMetadata: undefined,
},
{ type: "finish", reason: { normalized: "tool-calls" }, usage: undefined },
]) ])
expect(events.filter(LLMEvent.is.toolCall)).toEqual([])
expect(streamError.reason).toMatchObject({
_tag: "InvalidProviderOutput",
classification: "incomplete-stream",
})
expect(streamError.message).toContain("The provider response ended unexpectedly.")
expect(error.message).toContain("The provider response ended unexpectedly.")
}), }),
) )
@@ -300,7 +300,9 @@ describe("OpenAI-compatible Chat route", () => {
Effect.gen(function* () { Effect.gen(function* () {
const response = yield* LLMClient.generate( const response = yield* LLMClient.generate(
LLMRequest.update(request, { LLMRequest.update(request, {
tools: [ToolDefinition.make({ name: "weather", description: "Get weather", inputSchema: { type: "object" } })], tools: [
ToolDefinition.make({ name: "weather", description: "Get weather", inputSchema: { type: "object" } }),
],
}), }),
).pipe( ).pipe(
Effect.provide( Effect.provide(
@@ -177,8 +177,8 @@ test("shows all and expands historical diff summary without overlap", async ({ p
const firstUser = userMessage(undefined, { const firstUser = userMessage(undefined, {
summary: { summary: {
diffs: Array.from({ length: 12 }, (_, index) => ({ diffs: Array.from({ length: 12 }, (_, index) => ({
file: `src/diff-${index}.ts`, file: `src/diff-${index}.ts`,
status: "modified", status: "modified",
additions: 1, additions: 1,
deletions: 1, deletions: 1,
patch: `@@ -1 +1 @@\n-export const value = ${index}\n+export const value = ${index + 1}`, patch: `@@ -1 +1 @@\n-export const value = ${index}\n+export const value = ${index + 1}`,
@@ -85,8 +85,7 @@ async function mockServers(page: Page, requests: string[]) {
const current = url.origin === serverA ? sessionA : sessionB const current = url.origin === serverA ? sessionA : sessionB
const directory = url.searchParams.get("directory") const directory = url.searchParams.get("directory")
if (directory && directory !== current.directory) return json(route, { name: "InvalidDirectory" }, 500) if (directory && directory !== current.directory) return json(route, { name: "InvalidDirectory" }, 500)
if (url.pathname === "/api/event") if (url.pathname === "/api/event") return sse(route)
return sse(route)
if (url.pathname === "/api/health") return json(route, { pid: 1 }) if (url.pathname === "/api/health") return json(route, { pid: 1 })
if (url.pathname === "/api/session") return json(route, { data: [currentSession(current)], cursor: {} }) if (url.pathname === "/api/session") return json(route, { data: [currentSession(current)], cursor: {} })
if (url.pathname === "/api/session/active") return json(route, { data: {} }) if (url.pathname === "/api/session/active") return json(route, { data: {} })
@@ -181,12 +181,17 @@ async function mockServers(page: Page, permissionRequests: string[], permissionR
return json(route, true) return json(route, true)
} }
if (requestDirectory && requestDirectory !== directory) return json(route, { name: "InvalidDirectory" }, 500) if (requestDirectory && requestDirectory !== directory) return json(route, { name: "InvalidDirectory" }, 500)
if (url.pathname === "/api/event") if (url.pathname === "/api/event") return sse(route)
return sse(route)
if (url.pathname === "/api/provider") if (url.pathname === "/api/provider")
return json(route, { return json(route, {
location: { directory }, location: { directory },
data: [{ id: remote ? "server-b" : "server-a", name: remote ? "Server B Provider" : "Server A Provider", package: "test" }], data: [
{
id: remote ? "server-b" : "server-a",
name: remote ? "Server B Provider" : "Server A Provider",
package: "test",
},
],
}) })
if (url.pathname === "/api/model") return json(route, { location: { directory }, data: [model(remote)] }) if (url.pathname === "/api/model") return json(route, { location: { directory }, data: [model(remote)] })
if (url.pathname === "/api/model/default") return json(route, { location: { directory }, data: model(remote) }) if (url.pathname === "/api/model/default") return json(route, { location: { directory }, data: model(remote) })
@@ -58,8 +58,7 @@ async function mockServers(page: Page) {
const current = url.origin === serverA ? sessionA : sessionB const current = url.origin === serverA ? sessionA : sessionB
const directory = url.searchParams.get("directory") const directory = url.searchParams.get("directory")
if (directory && directory !== current.directory) return json(route, { name: "InvalidDirectory" }, 500) if (directory && directory !== current.directory) return json(route, { name: "InvalidDirectory" }, 500)
if (url.pathname === "/api/event") if (url.pathname === "/api/event") return sse(route, url.pathname === "/api/event")
return sse(route, url.pathname === "/api/event")
if (url.pathname === "/api/health") return json(route, { pid: 1 }) if (url.pathname === "/api/health") return json(route, { pid: 1 })
if (url.pathname === "/api/session/active") if (url.pathname === "/api/session/active")
return json(route, { data: url.origin === serverB ? { [sessionB.id]: { type: "running" } } : {} }) return json(route, { data: url.origin === serverB ? { [sessionB.id]: { type: "running" } } : {} })
@@ -171,8 +171,8 @@ test("keeps the review tree and terminal sized when both panels are open", async
expect(bottomGap).toBeLessThanOrEqual(16) expect(bottomGap).toBeLessThanOrEqual(16)
const lazyDiff = page.waitForRequest((request) => { const lazyDiff = page.waitForRequest((request) => {
const url = new URL(request.url()) const url = new URL(request.url())
return ( return (
url.pathname === "/api/vcs/diff" && url.pathname === "/api/vcs/diff" &&
url.searchParams.get("location[directory]")?.replaceAll("\\", "/").endsWith("/src/branch/d00027") === true url.searchParams.get("location[directory]")?.replaceAll("\\", "/").endsWith("/src/branch/d00027") === true
) )
}) })
@@ -86,15 +86,10 @@ test.describe("session timeline projection", () => {
], ],
{ summary: { diffs: Array.from({ length: 11 }, (_, index) => summaryDiff(index)) } }, { summary: { diffs: Array.from({ length: 11 }, (_, index) => summaryDiff(index)) } },
) )
const aborted = assistantMessage( const aborted = assistantMessage([{ id: "prt_before_abort", type: "text", text: "Before interruption" }], {
[ id: "msg_1001_assistant_aborted",
{ id: "prt_before_abort", type: "text", text: "Before interruption" }, error: { name: "MessageAbortedError", data: { message: "Stopped" } },
], })
{
id: "msg_1001_assistant_aborted",
error: { name: "MessageAbortedError", data: { message: "Stopped" } },
},
)
const failed = assistantMessage([{ id: "prt_after_abort", type: "text", text: "After interruption" }], { const failed = assistantMessage([{ id: "prt_after_abort", type: "text", text: "After interruption" }], {
id: "msg_1002_assistant_failed", id: "msg_1002_assistant_failed",
error: { error: {
@@ -89,8 +89,7 @@ async function mockServer(page: Page) {
if (url.origin !== server) return route.fallback() if (url.origin !== server) return route.fallback()
if ([`/api/session/${unresolvedSessionID}`, `/session/${unresolvedSessionID}`].includes(url.pathname)) if ([`/api/session/${unresolvedSessionID}`, `/session/${unresolvedSessionID}`].includes(url.pathname))
return new Promise(() => {}) return new Promise(() => {})
if (url.pathname === "/api/event") if (url.pathname === "/api/event") return sse(route)
return sse(route)
if (url.pathname === "/api/session") return json(route, { data: sessions.map(currentSession), cursor: {} }) if (url.pathname === "/api/session") return json(route, { data: sessions.map(currentSession), cursor: {} })
if (url.pathname === "/api/session/active") return json(route, { data: {} }) if (url.pathname === "/api/session/active") return json(route, { data: {} })
const currentSessionInfo = sessions.find((item) => url.pathname === `/api/session/${item.id}`) const currentSessionInfo = sessions.find((item) => url.pathname === `/api/session/${item.id}`)
@@ -300,9 +300,8 @@ export const fixture = {
.filter((message) => message.info.role === "user") .filter((message) => message.info.role === "user")
.map((message) => message.info.id), .map((message) => message.info.id),
targetPartIDs: targetMessages.flatMap(currentPartIDs), targetPartIDs: targetMessages.flatMap(currentPartIDs),
expandedShellPartID: targetMessages expandedShellPartID: targetMessages.flatMap((message) => message.parts).find((part) => part.tool === "bash")!
.flatMap((message) => message.parts) .callID,
.find((part) => part.tool === "bash")!.callID,
}, },
} }
+25 -23
View File
@@ -144,7 +144,8 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
location: location(config), location: location(config),
data: currentProviders(providerConfig(config)), data: currentProviders(providerConfig(config)),
}) })
if (path === "/api/model") return json(route, { location: location(config), data: currentModels(providerConfig(config)) }) if (path === "/api/model")
return json(route, { location: location(config), data: currentModels(providerConfig(config)) })
if (path === "/api/model/default") if (path === "/api/model/default")
return json(route, { location: location(config), data: currentDefaultModel(providerConfig(config)) }) return json(route, { location: location(config), data: currentDefaultModel(providerConfig(config)) })
if (path === "/api/integration") return json(route, { location: location(config), data: [] }) if (path === "/api/integration") return json(route, { location: location(config), data: [] })
@@ -206,7 +207,8 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
const fileRead = path.match(/^\/api\/fs\/read\/(.+)$/)?.[1] const fileRead = path.match(/^\/api\/fs\/read\/(.+)$/)?.[1]
if (fileRead && config.fileContent) { if (fileRead && config.fileContent) {
const value = await config.fileContent(decodeURIComponent(fileRead)) const value = await config.fileContent(decodeURIComponent(fileRead))
const content = value && typeof value === "object" && "content" in value ? String(value.content) : String(value ?? "") const content =
value && typeof value === "object" && "content" in value ? String(value.content) : String(value ?? "")
return route.fulfill({ status: 200, body: content, headers: { "content-type": "application/octet-stream" } }) return route.fulfill({ status: 200, body: content, headers: { "content-type": "application/octet-stream" } })
} }
if (path === "/api/fs/find" && config.findFiles) { if (path === "/api/fs/find" && config.findFiles) {
@@ -281,7 +283,8 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
if (/^\/api\/session\/[^/]+\/permission\/[^/]+\/reply$/.test(path) && route.request().method() === "POST") { if (/^\/api\/session\/[^/]+\/permission\/[^/]+\/reply$/.test(path) && route.request().method() === "POST") {
return route.fulfill({ status: 204, headers: { "access-control-allow-origin": "*" } }) return route.fulfill({ status: 204, headers: { "access-control-allow-origin": "*" } })
} }
if (/^\/question\/[^/]+\/(reply|reject)$/.test(path) && route.request().method() === "POST") return json(route, true) if (/^\/question\/[^/]+\/(reply|reject)$/.test(path) && route.request().method() === "POST")
return json(route, true)
if (/^\/session\/[^/]+\/permissions\/[^/]+$/.test(path) && route.request().method() === "POST") if (/^\/session\/[^/]+\/permissions\/[^/]+$/.test(path) && route.request().method() === "POST")
return json(route, true) return json(route, true)
if ( if (
@@ -386,11 +389,13 @@ function providerConfig(config: MockServerConfig) {
function currentProviders(value: unknown) { function currentProviders(value: unknown) {
if (!record(value) || !Array.isArray(value.all)) return Array.isArray(value) ? value : [] if (!record(value) || !Array.isArray(value.all)) return Array.isArray(value) ? value : []
return value.all.filter(record).flatMap((provider) => return value.all
typeof provider.id === "string" && typeof provider.name === "string" .filter(record)
? [{ id: provider.id, name: provider.name, package: provider.id }] .flatMap((provider) =>
: [], typeof provider.id === "string" && typeof provider.name === "string"
) ? [{ id: provider.id, name: provider.name, package: provider.id }]
: [],
)
} }
function currentModels(value: unknown) { function currentModels(value: unknown) {
@@ -440,9 +445,7 @@ function currentDefaultModel(value: unknown) {
if (!record(value) || !record(value.default)) return null if (!record(value) || !record(value.default)) return null
const selected = value.default const selected = value.default
const models = currentModels(value) const models = currentModels(value)
return models.find( return models.find((model) => model.providerID === selected.providerID && model.id === selected.modelID) ?? null
(model) => model.providerID === selected.providerID && model.id === selected.modelID,
) ?? null
} }
function currentPermission(value: unknown) { function currentPermission(value: unknown) {
@@ -562,22 +565,16 @@ function legacyAgent(part: Record<string, unknown>): PromptAgentAttachment[] {
} }
function mentionFrom(value: Record<string, unknown> | undefined) { function mentionFrom(value: Record<string, unknown> | undefined) {
if ( if (!value || typeof value.value !== "string" || typeof value.start !== "number" || typeof value.end !== "number")
!value ||
typeof value.value !== "string" ||
typeof value.start !== "number" ||
typeof value.end !== "number"
)
return return
return { text: value.value, start: value.start, end: value.end } return { text: value.value, start: value.start, end: value.end }
} }
function legacyAssistantContent( function legacyAssistantContent(part: Record<string, unknown>, created: number): SessionMessageAssistant["content"] {
part: Record<string, unknown>,
created: number,
): SessionMessageAssistant["content"] {
if (part.type === "text" && typeof part.text === "string") if (part.type === "text" && typeof part.text === "string")
return [{ type: "text", text: part.text, ...(jsonRecord(part.metadata) ? { state: jsonRecord(part.metadata) } : {}) }] return [
{ type: "text", text: part.text, ...(jsonRecord(part.metadata) ? { state: jsonRecord(part.metadata) } : {}) },
]
if (part.type === "reasoning" && typeof part.text === "string") { if (part.type === "reasoning" && typeof part.text === "string") {
const time = record(part.time) ? part.time : undefined const time = record(part.time) ? part.time : undefined
return [ return [
@@ -618,7 +615,12 @@ function legacyAssistantContent(
...(jsonRecord(part.providerResultState) ? { providerResultState: jsonRecord(part.providerResultState) } : {}), ...(jsonRecord(part.providerResultState) ? { providerResultState: jsonRecord(part.providerResultState) } : {}),
} }
if (state.status === "pending") if (state.status === "pending")
return [{ ...base, state: { status: "streaming", input: typeof state.raw === "string" ? state.raw : JSON.stringify(input) } }] return [
{
...base,
state: { status: "streaming", input: typeof state.raw === "string" ? state.raw : JSON.stringify(input) },
},
]
if (state.status === "completed") if (state.status === "completed")
return [ return [
{ {
+1 -2
View File
@@ -174,8 +174,7 @@ export async function installSseTransport<T>(
const fetch = (input: RequestInfo | URL, init?: RequestInit) => { const fetch = (input: RequestInfo | URL, init?: RequestInit) => {
const request = new Request(input, init) const request = new Request(input, init)
const url = new URL(request.url) const url = new URL(request.url)
if (url.origin !== server || url.pathname !== "/api/event") if (url.origin !== server || url.pathname !== "/api/event") return originalFetch(request)
return originalFetch(request)
const id = ++nextConnectionID const id = ++nextConnectionID
const record = { const record = {
+4 -1
View File
@@ -20,7 +20,10 @@
<meta property="twitter:image" content="/social-share.png" /> <meta property="twitter:image" content="/social-share.png" />
<script id="oc-theme-preload-script" src="/oc-theme-preload.js"></script> <script id="oc-theme-preload-script" src="/oc-theme-preload.js"></script>
</head> </head>
<body data-new-layout class="antialiased overscroll-none font-(family-name:--font-family-text) text-[13px] font-[440] overflow-hidden bg-v2-background-bg-deep"> <body
data-new-layout
class="antialiased overscroll-none font-(family-name:--font-family-text) text-[13px] font-[440] overflow-hidden bg-v2-background-bg-deep"
>
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root" class="flex flex-col h-dvh bg-v2-background-bg-deep p-px"></div> <div id="root" class="flex flex-col h-dvh bg-v2-background-bg-deep p-px"></div>
<script src="/src/entry.tsx" type="module"></script> <script src="/src/entry.tsx" type="module"></script>
+1 -8
View File
@@ -9,14 +9,7 @@ import { Font } from "@opencode-ai/ui/font"
import { Splash } from "@opencode-ai/ui/logo" import { Splash } from "@opencode-ai/ui/logo"
import { ThemeProvider } from "@opencode-ai/ui/theme/context" import { ThemeProvider } from "@opencode-ai/ui/theme/context"
import { MetaProvider } from "@solidjs/meta" import { MetaProvider } from "@solidjs/meta"
import { import { type BaseRouterProps, Navigate, Route, Router, useParams, useSearchParams } from "@solidjs/router"
type BaseRouterProps,
Navigate,
Route,
Router,
useParams,
useSearchParams,
} from "@solidjs/router"
import { QueryClient, QueryClientProvider } from "@tanstack/solid-query" import { QueryClient, QueryClientProvider } from "@tanstack/solid-query"
import { Effect } from "effect" import { Effect } from "effect"
import { import {
@@ -123,9 +123,7 @@ export const SettingsProvidersV2: Component<{
const credentials = integration.data?.connections.filter((item) => item.type === "credential") ?? [] const credentials = integration.data?.connections.filter((item) => item.type === "credential") ?? []
if (credentials.length === 0) throw new Error(`No removable credentials found for ${name}`) if (credentials.length === 0) throw new Error(`No removable credentials found for ${name}`)
await Promise.all( await Promise.all(
credentials.map((credential) => credentials.map((credential) => serverSdk().api.credential.remove({ credentialID: credential.id, location })),
serverSdk().api.credential.remove({ credentialID: credential.id, location }),
),
) )
showToast({ showToast({
variant: "success", variant: "success",
@@ -5,7 +5,16 @@ import { Switch } from "@opencode-ai/ui/switch"
import { Tabs } from "@opencode-ai/ui/tabs" import { Tabs } from "@opencode-ai/ui/tabs"
import { showToast } from "@/utils/toast" import { showToast } from "@/utils/toast"
import { useNavigate } from "@solidjs/router" import { useNavigate } from "@solidjs/router"
import { type Accessor, createEffect, createMemo, createResource, For, type JSXElement, onCleanup, Show } from "solid-js" import {
type Accessor,
createEffect,
createMemo,
createResource,
For,
type JSXElement,
onCleanup,
Show,
} from "solid-js"
import { createStore } from "solid-js/store" import { createStore } from "solid-js/store"
import { ServerHealthIndicator, ServerRow } from "@/components/server/server-row" import { ServerHealthIndicator, ServerRow } from "@/components/server/server-row"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
@@ -291,7 +300,10 @@ export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
const lspCount = createMemo(() => lspItems().length) const lspCount = createMemo(() => lspItems().length)
const [pluginList] = createResource( const [pluginList] = createResource(
() => (props.shown() ? sdk().directory : undefined), () => (props.shown() ? sdk().directory : undefined),
(directory) => sdk().api.plugin.list({ location: { directory } }).then((result) => result.data), (directory) =>
sdk()
.api.plugin.list({ location: { directory } })
.then((result) => result.data),
) )
const plugins = createMemo(() => (pluginList.latest ?? []).map((item) => item.id)) const plugins = createMemo(() => (pluginList.latest ?? []).map((item) => item.id))
const pluginCount = createMemo(() => plugins().length) const pluginCount = createMemo(() => plugins().length)
+1 -4
View File
@@ -190,10 +190,7 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl
.find((item) => ServerConnection.key(item) === (route.server ?? server.key)) .find((item) => ServerConnection.key(item) === (route.server ?? server.key))
return conn ? { route, sdk: global.ensureServerCtx(conn).sdk } : undefined return conn ? { route, sdk: global.ensureServerCtx(conn).sdk } : undefined
}, },
({ route, sdk }) => ({ route, sdk }) => sdk.api.session.get({ sessionID: route.sessionId }).catch(() => {}),
sdk.api.session
.get({ sessionID: route.sessionId })
.catch(() => {}),
) )
const matchRoute = (route: LayoutRoute) => { const matchRoute = (route: LayoutRoute) => {
+1 -3
View File
@@ -124,9 +124,7 @@ export const createDirSyncContext = (
const [store, setStore] = current() const [store, setStore] = current()
setStore("limit", (value) => value + count) setStore("limit", (value) => value + count)
const response = await serverSDK.api.session.list({ directory, limit: store.limit, order: "desc" }) const response = await serverSDK.api.session.list({ directory, limit: store.limit, order: "desc" })
const sessions = response.data const sessions = response.data.sort((a, b) => cmp(a.id, b.id)).slice(0, store.limit)
.sort((a, b) => cmp(a.id, b.id))
.slice(0, store.limit)
sessions.forEach(serverSync.session.remember) sessions.forEach(serverSync.session.remember)
setStore("session", reconcile(sessions, { key: "id" })) setStore("session", reconcile(sessions, { key: "id" }))
}, },
+76 -124
View File
@@ -1,9 +1,4 @@
import type { import type { Config, Path, Project, ProviderAuthResponse } from "@/types"
Config,
Path,
Project,
ProviderAuthResponse,
} from "@/types"
import type { import type {
AgentListInput, AgentListInput,
AgentListOutput, AgentListOutput,
@@ -31,12 +26,7 @@ import { batch } from "solid-js"
import { produce, reconcile, type SetStoreFunction, type Store } from "solid-js/store" import { produce, reconcile, type SetStoreFunction, type Store } from "solid-js/store"
import type { State, VcsCache } from "./types" import type { State, VcsCache } from "./types"
import type { ServerSession } from "../server-session" import type { ServerSession } from "../server-session"
import { import { cmp, normalizeAgentList, normalizeProjectInfo, normalizeProviderList } from "./utils"
cmp,
normalizeAgentList,
normalizeProjectInfo,
normalizeProviderList,
} from "./utils"
import { formatServerError } from "@/utils/server-errors" import { formatServerError } from "@/utils/server-errors"
import { QueryClient, queryOptions } from "@tanstack/solid-query" import { QueryClient, queryOptions } from "@tanstack/solid-query"
import { loadMcpQuery, loadMcpResourcesQuery } from "../server-sync" import { loadMcpQuery, loadMcpResourcesQuery } from "../server-sync"
@@ -148,10 +138,7 @@ export async function bootstrapGlobal(input: {
}) { }) {
const slow = [ const slow = [
() => input.queryClient.fetchQuery(loadGlobalConfigQuery(input.scope)), () => input.queryClient.fetchQuery(loadGlobalConfigQuery(input.scope)),
() => () => input.queryClient.fetchQuery(loadProvidersQuery(input.scope, null, input.serverAPI)),
input.queryClient.fetchQuery(
loadProvidersQuery(input.scope, null, input.serverAPI),
),
() => input.queryClient.fetchQuery(loadPathQuery(input.scope, null, input.serverAPI.location)), () => input.queryClient.fetchQuery(loadPathQuery(input.scope, null, input.serverAPI.location)),
() => () =>
input.queryClient input.queryClient
@@ -211,11 +198,7 @@ function warmSessions(input: {
).then(() => undefined) ).then(() => undefined)
} }
export const loadProvidersQuery = ( export const loadProvidersQuery = (scope: ServerScope, directory: string | null, sdk: CatalogApi) =>
scope: ServerScope,
directory: string | null,
sdk: CatalogApi,
) =>
queryOptions({ queryOptions({
queryKey: [scope, directory, "providers"], queryKey: [scope, directory, "providers"],
queryFn: () => queryFn: () =>
@@ -242,28 +225,16 @@ type ReferenceListApi = {
readonly list: (input?: ReferenceListInput) => Promise<ReferenceListOutput> readonly list: (input?: ReferenceListInput) => Promise<ReferenceListOutput>
} }
export const loadAgentsQuery = ( export const loadAgentsQuery = (scope: ServerScope, directory: string, sdk: AgentListApi) =>
scope: ServerScope,
directory: string,
sdk: AgentListApi,
) =>
queryOptions({ queryOptions({
queryKey: [scope, directory, "agents"], queryKey: [scope, directory, "agents"],
queryFn: () => queryFn: () => retry(() => sdk.list({ location: { directory } }).then((result) => normalizeAgentList(result.data))),
retry(() => sdk.list({ location: { directory } }).then((result) => normalizeAgentList(result.data))),
}) })
export const loadCommands = ( export const loadCommands = (directory: string, api: CommandListApi): Promise<CommandInfo[]> =>
directory: string,
api: CommandListApi,
): Promise<CommandInfo[]> =>
retry(() => api.list({ location: { directory } }).then((result) => result.data)) retry(() => api.list({ location: { directory } }).then((result) => result.data))
export const loadPathQuery = ( export const loadPathQuery = (scope: ServerScope, directory: string | null, api: LocationApi) =>
scope: ServerScope,
directory: string | null,
api: LocationApi,
) =>
queryOptions<Path>({ queryOptions<Path>({
queryKey: [scope, directory, "path"], queryKey: [scope, directory, "path"],
queryFn: () => queryFn: () =>
@@ -276,15 +247,10 @@ export const loadPathQuery = (
})), })),
}) })
export const loadReferencesQuery = ( export const loadReferencesQuery = (scope: ServerScope, directory: string, api: ReferenceListApi) =>
scope: ServerScope,
directory: string,
api: ReferenceListApi,
) =>
queryOptions<ReferenceInfo[]>({ queryOptions<ReferenceInfo[]>({
queryKey: [scope, directory, "references"] as const, queryKey: [scope, directory, "references"] as const,
queryFn: () => queryFn: () => retry(() => api.list({ location: { directory } }).then((result) => result.data)).catch(() => []),
retry(() => api.list({ location: { directory } }).then((result) => result.data)).catch(() => []),
placeholderData: [], placeholderData: [],
}) })
@@ -353,46 +319,41 @@ export async function bootstrapDirectory(input: {
})), })),
input.mcp && input.mcp &&
(() => (() =>
loadCommands(input.directory, input.api.command).then((commands) => loadCommands(input.directory, input.api.command).then((commands) => input.setStore("command", commands))),
input.setStore("command", commands), () => input.queryClient.fetchQuery(loadReferencesQuery(input.scope, input.directory, input.api.reference)),
)),
() =>
input.queryClient.fetchQuery(
loadReferencesQuery(input.scope, input.directory, input.api.reference),
),
() => () =>
retry(() => retry(() =>
input.api.permission.request input.api.permission.request
.list({ location: { directory: input.directory } }) .list({ location: { directory: input.directory } })
.then((result) => result.data) .then((result) => result.data)
.then((permissions) => { .then((permissions) => {
const ids = permissions.map((permission) => permission.sessionID) const ids = permissions.map((permission) => permission.sessionID)
const grouped = groupBySession( const grouped = groupBySession(
permissions.filter((permission) => !!permission.id && !!permission.sessionID), permissions.filter((permission) => !!permission.id && !!permission.sessionID),
) )
const warm = input.session const warm = input.session
? Promise.all(ids.map((sessionID) => input.session!.resolve(sessionID))).then(() => undefined) ? Promise.all(ids.map((sessionID) => input.session!.resolve(sessionID))).then(() => undefined)
: warmSessions({ ids, store: input.store, setStore: input.setStore, api: input.api.session }) : warmSessions({ ids, store: input.store, setStore: input.setStore, api: input.api.session })
return warm.then(() => return warm.then(() =>
batch(() => { batch(() => {
const current = input.session?.data.permission ?? input.store.permission const current = input.session?.data.permission ?? input.store.permission
for (const sessionID of Object.keys(current)) { for (const sessionID of Object.keys(current)) {
if (grouped[sessionID]) continue if (grouped[sessionID]) continue
if (input.session?.get(sessionID)?.location.directory !== input.directory) continue if (input.session?.get(sessionID)?.location.directory !== input.directory) continue
if (input.session) input.session.set("permission", sessionID, []) if (input.session) input.session.set("permission", sessionID, [])
if (!input.session) input.setStore("permission", sessionID, []) if (!input.session) input.setStore("permission", sessionID, [])
} }
for (const [sessionID, permissions] of Object.entries(grouped)) { for (const [sessionID, permissions] of Object.entries(grouped)) {
const value = reconcile( const value = reconcile(
permissions.filter((p) => !!p?.id).sort((a, b) => cmp(a.id, b.id)), permissions.filter((p) => !!p?.id).sort((a, b) => cmp(a.id, b.id)),
{ key: "id" }, { key: "id" },
) )
if (input.session) input.session.set("permission", sessionID, value) if (input.session) input.session.set("permission", sessionID, value)
if (!input.session) input.setStore("permission", sessionID, value) if (!input.session) input.setStore("permission", sessionID, value)
} }
}), }),
) )
}), }),
), ),
() => () =>
retry(() => retry(() =>
@@ -400,56 +361,47 @@ export async function bootstrapDirectory(input: {
.list({ location: { directory: input.directory } }) .list({ location: { directory: input.directory } })
.then((result) => result.data) .then((result) => result.data)
.then((questions) => { .then((questions) => {
const ids = questions.map((question) => question.sessionID) const ids = questions.map((question) => question.sessionID)
const grouped = groupBySession( const grouped = groupBySession(
questions.filter((question) => !!question.id && !!question.sessionID) as QuestionRequest[], questions.filter((question) => !!question.id && !!question.sessionID) as QuestionRequest[],
) )
const warm = input.session const warm = input.session
? Promise.all(ids.map((sessionID) => input.session!.resolve(sessionID))).then(() => undefined) ? Promise.all(ids.map((sessionID) => input.session!.resolve(sessionID))).then(() => undefined)
: warmSessions({ ids, store: input.store, setStore: input.setStore, api: input.api.session }) : warmSessions({ ids, store: input.store, setStore: input.setStore, api: input.api.session })
return warm.then(() => return warm.then(() =>
batch(() => { batch(() => {
const current = input.session?.data.question ?? input.store.question const current = input.session?.data.question ?? input.store.question
for (const sessionID of Object.keys(current)) { for (const sessionID of Object.keys(current)) {
if (grouped[sessionID]) continue if (grouped[sessionID]) continue
if (input.session?.get(sessionID)?.location.directory !== input.directory) continue if (input.session?.get(sessionID)?.location.directory !== input.directory) continue
if (input.session) input.session.set("question", sessionID, []) if (input.session) input.session.set("question", sessionID, [])
if (!input.session) input.setStore("question", sessionID, []) if (!input.session) input.setStore("question", sessionID, [])
} }
for (const [sessionID, questions] of Object.entries(grouped)) { for (const [sessionID, questions] of Object.entries(grouped)) {
const value = reconcile( const value = reconcile(
questions.filter((q) => !!q?.id).sort((a, b) => cmp(a.id, b.id)), questions.filter((q) => !!q?.id).sort((a, b) => cmp(a.id, b.id)),
{ key: "id" }, { key: "id" },
) )
if (input.session) input.session.set("question", sessionID, value) if (input.session) input.session.set("question", sessionID, value)
if (!input.session) input.setStore("question", sessionID, value) if (!input.session) input.setStore("question", sessionID, value)
} }
}), }),
) )
}), }),
), ),
() => Promise.resolve(input.loadSessions(input.directory)), () => Promise.resolve(input.loadSessions(input.directory)),
input.mcp && (() => input.queryClient.fetchQuery(loadMcpQuery(input.scope, input.directory, input.api.mcp))),
input.mcp && input.mcp &&
(() => (() => input.queryClient.fetchQuery(loadMcpResourcesQuery(input.scope, input.directory, input.api.mcp))),
input.queryClient.fetchQuery(
loadMcpQuery(input.scope, input.directory, input.api.mcp),
)),
input.mcp &&
(() =>
input.queryClient.fetchQuery(
loadMcpResourcesQuery(input.scope, input.directory, input.api.mcp),
)),
() => () =>
input.queryClient input.queryClient.fetchQuery(loadProvidersQuery(input.scope, input.directory, input.api)).catch((err) => {
.fetchQuery(loadProvidersQuery(input.scope, input.directory, input.api)) const project = getFilename(input.directory)
.catch((err) => { showToast({
const project = getFilename(input.directory) variant: "error",
showToast({ title: input.translate("toast.project.reloadFailed.title", { project }),
variant: "error", description: formatServerError(err, input.translate),
title: input.translate("toast.project.reloadFailed.title", { project }), })
description: formatServerError(err, input.translate), }),
})
}),
].filter(Boolean) as (() => Promise<any>)[] ].filter(Boolean) as (() => Promise<any>)[]
await waitForPaint() await waitForPaint()
@@ -1,12 +1,13 @@
import { Binary } from "@opencode-ai/core/util/binary" import { Binary } from "@opencode-ai/core/util/binary"
import { produce, reconcile, type SetStoreFunction, type Store } from "solid-js/store" import { produce, reconcile, type SetStoreFunction, type Store } from "solid-js/store"
import type { Message, Part, Project, Todo } from "@/types"
import type { import type {
Message, FileDiffInfo,
Part, PermissionRequest,
Project, QuestionRequest,
Todo, SessionInfo,
} from "@/types" SessionStatus,
import type { FileDiffInfo, PermissionRequest, QuestionRequest, SessionInfo, SessionStatus } from "@opencode-ai/client/promise" } from "@opencode-ai/client/promise"
import type { State, VcsCache } from "./types" import type { State, VcsCache } from "./types"
import { trimSessions } from "./session-trim" import { trimSessions } from "./session-trim"
import { dropSessionCaches } from "./session-cache" import { dropSessionCaches } from "./session-cache"
@@ -81,9 +81,9 @@ describe("Home V2 session index", () => {
]) ])
expect(result).toEqual([ expect(result).toEqual([
expect.objectContaining({ expect.objectContaining({
id: "root", id: "root",
location: { directory: "/project" }, location: { directory: "/project" },
projectID: "project", projectID: "project",
title: "root", title: "root",
time: { created: 1, updated: 30 }, time: { created: 1, updated: 30 },
+1 -10
View File
@@ -1,13 +1,4 @@
import type { import type { Agent, Config, LspStatus, Message, Part, Path, Todo, VcsInfo } from "@/types"
Agent,
Config,
LspStatus,
Message,
Part,
Path,
Todo,
VcsInfo,
} from "@/types"
import type { import type {
FileDiffInfo, FileDiffInfo,
PermissionRequest, PermissionRequest,
+279 -280
View File
@@ -67,64 +67,74 @@ function currentMessages(data: MessageResponse["data"]): SessionMessageInfo[] {
}, },
] ]
} }
return [{ return [
id: item.info.id, {
type: "assistant", id: item.info.id,
agent: item.info.agent, type: "assistant",
model: { id: item.info.modelID, providerID: item.info.providerID, variant: item.info.variant }, agent: item.info.agent,
content: item.parts.flatMap((part): SessionMessageAssistant["content"] => { model: { id: item.info.modelID, providerID: item.info.providerID, variant: item.info.variant },
if (part.type === "text") return [{ type: "text", text: part.text }] content: item.parts.flatMap((part): SessionMessageAssistant["content"] => {
if (part.type === "reasoning") if (part.type === "text") return [{ type: "text", text: part.text }]
if (part.type === "reasoning")
return [
{
type: "reasoning",
text: part.text,
time: part.time ? { created: part.time.start, completed: part.time.end } : undefined,
},
]
if (part.type !== "tool") return []
const state: SessionMessageAssistantTool["state"] = (() => {
if (part.state.status === "pending")
return { status: "streaming" as const, input: JSON.stringify(part.state.input) }
if (part.state.status === "running")
return {
status: "running" as const,
input: part.state.input as CurrentToolObject,
metadata: (part.state.metadata ?? {}) as CurrentToolObject,
}
if (part.state.status === "error")
return {
status: "error" as const,
input: part.state.input as CurrentToolObject,
error: { type: "tool_error", message: part.state.error },
metadata: part.state.metadata as CurrentToolObject | undefined,
}
return {
status: "completed" as const,
input: part.state.input as CurrentToolObject,
content: [{ type: "text" as const, text: part.state.output }],
metadata: part.state.metadata as CurrentToolObject,
}
})()
return [ return [
{ {
type: "reasoning", id: part.id,
text: part.text, type: "tool" as const,
time: part.time ? { created: part.time.start, completed: part.time.end } : undefined, name: part.tool,
state,
time: {
created: part.state.status === "pending" ? item.info.time.created : part.state.time.start,
ran: part.state.status === "pending" ? undefined : part.state.time.start,
completed:
part.state.status === "completed" || part.state.status === "error" ? part.state.time.end : undefined,
},
}, },
] ]
if (part.type !== "tool") return [] }),
const state: SessionMessageAssistantTool["state"] = (() => { time: item.info.time,
if (part.state.status === "pending") return { status: "streaming" as const, input: JSON.stringify(part.state.input) } cost: item.info.cost,
if (part.state.status === "running") tokens: item.info.tokens,
return { finish: item.info.finish as
status: "running" as const, | "stop"
input: part.state.input as CurrentToolObject, | "length"
metadata: (part.state.metadata ?? {}) as CurrentToolObject, | "tool-calls"
} | "content-filter"
if (part.state.status === "error") | "error"
return { | "unknown"
status: "error" as const, | undefined,
input: part.state.input as CurrentToolObject, },
error: { type: "tool_error", message: part.state.error }, ]
metadata: part.state.metadata as CurrentToolObject | undefined,
}
return {
status: "completed" as const,
input: part.state.input as CurrentToolObject,
content: [{ type: "text" as const, text: part.state.output }],
metadata: part.state.metadata as CurrentToolObject,
}
})()
return [
{
id: part.id,
type: "tool" as const,
name: part.tool,
state,
time: {
created: part.state.status === "pending" ? item.info.time.created : part.state.time.start,
ran: part.state.status === "pending" ? undefined : part.state.time.start,
completed:
part.state.status === "completed" || part.state.status === "error" ? part.state.time.end : undefined,
},
},
]
}),
time: item.info.time,
cost: item.info.cost,
tokens: item.info.tokens,
finish: item.info.finish as "stop" | "length" | "tool-calls" | "content-filter" | "error" | "unknown" | undefined,
}]
}) })
} }
@@ -428,270 +438,259 @@ describe("server session", () => {
// V2 messages are ordered projections and do not expose V1 assistant parent IDs. // V2 messages are ordered projections and do not expose V1 assistant parent IDs.
describe.skip("V1 assistant parent projections", () => { describe.skip("V1 assistant parent projections", () => {
test("backfills an assistant-only initial page through its user root", async () => { test("backfills an assistant-only initial page through its user root", async () => {
const user = userMessage("message-1") const user = userMessage("message-1")
const assistants = [assistantMessage("message-2", user.id), assistantMessage("message-3", user.id)] const assistants = [assistantMessage("message-2", user.id), assistantMessage("message-3", user.id)]
const client = rootMessageClient( const client = rootMessageClient(
[ [response(assistants.map((info) => ({ info, parts: [] })))],
response(assistants.map((info) => ({ info, parts: [] }))), [singleResponse(user)],
], )
[singleResponse(user)], const store = createServerSession(client)
)
const store = createServerSession(client)
await store.sync("child") await store.sync("child")
expect(client.requests).toEqual([{ sessionID: "child", limit: 20, order: "desc" }]) expect(client.requests).toEqual([{ sessionID: "child", limit: 20, order: "desc" }])
expect(client.rootRequests).toEqual([{ sessionID: "child", messageID: user.id }]) expect(client.rootRequests).toEqual([{ sessionID: "child", messageID: user.id }])
expect(store.data.message.child).toEqual([user, ...assistants]) expect(store.data.message.child).toEqual([user, ...assistants])
expect(store.history.more("child")).toBe(false) expect(store.history.more("child")).toBe(false)
}) })
test("keeps assistant history when its deleted parent cannot be backfilled", async () => { test("keeps assistant history when its deleted parent cannot be backfilled", async () => {
const missing = Promise.withResolvers<SingleMessageResponse>() const missing = Promise.withResolvers<SingleMessageResponse>()
const assistant = assistantMessage("message-2", "message-missing") const assistant = assistantMessage("message-2", "message-missing")
const client = rootMessageClient([response([{ info: assistant, parts: [] }])], [missing.promise]) const client = rootMessageClient([response([{ info: assistant, parts: [] }])], [missing.promise])
const store = createServerSession(client) const store = createServerSession(client)
const loading = store.sync("child") const loading = store.sync("child")
await client.rootRequested(1) await client.rootRequested(1)
missing.reject(new Error("Message not found: message-missing", { cause: { status: 404 } })) missing.reject(new Error("Message not found: message-missing", { cause: { status: 404 } }))
await loading await loading
expect(client.rootRequests).toEqual([{ sessionID: "child", messageID: "message-missing" }]) expect(client.rootRequests).toEqual([{ sessionID: "child", messageID: "message-missing" }])
expect(store.data.message.child).toEqual([assistant]) expect(store.data.message.child).toEqual([assistant])
expect(store.history.more("child")).toBe(false) expect(store.history.more("child")).toBe(false)
}) })
test("drops a cached parent when a forced refresh confirms it was deleted", async () => { test("drops a cached parent when a forced refresh confirms it was deleted", async () => {
const missing = Promise.withResolvers<SingleMessageResponse>() const missing = Promise.withResolvers<SingleMessageResponse>()
const parent = userMessage("message-1") const parent = userMessage("message-1")
const part = textPart(parent.id) const part = textPart(parent.id)
const assistant = assistantMessage("message-2", parent.id) const assistant = assistantMessage("message-2", parent.id)
const client = rootMessageClient( const client = rootMessageClient(
[ [
response([ response([
{ info: parent, parts: [part] }, { info: parent, parts: [part] },
{ info: assistant, parts: [] }, { info: assistant, parts: [] },
]), ]),
response([{ info: assistant, parts: [] }]), response([{ info: assistant, parts: [] }]),
], ],
[missing.promise], [missing.promise],
) )
const store = createServerSession(client) const store = createServerSession(client)
await store.sync("child") await store.sync("child")
const loading = store.sync("child", { force: true }) const loading = store.sync("child", { force: true })
await client.rootRequested(1) await client.rootRequested(1)
missing.reject(new Error(`Message not found: ${parent.id}`, { cause: { status: 404 } })) missing.reject(new Error(`Message not found: ${parent.id}`, { cause: { status: 404 } }))
await loading await loading
expect(store.data.message.child).toEqual([assistant]) expect(store.data.message.child).toEqual([assistant])
expect(store.data.part[parent.id]).toBeUndefined() expect(store.data.part[parent.id]).toBeUndefined()
}) })
test("does not let an optimistic user suppress initial root backfill", async () => { test("does not let an optimistic user suppress initial root backfill", async () => {
const user = userMessage("message-1") const user = userMessage("message-1")
const part = textPart(user.id) const part = textPart(user.id)
const assistants = [assistantMessage("message-2", user.id), assistantMessage("message-3", user.id)] const assistants = [assistantMessage("message-2", user.id), assistantMessage("message-3", user.id)]
const client = rootMessageClient( const client = rootMessageClient(
[ [response(assistants.map((info) => ({ info, parts: [] })))],
response(assistants.map((info) => ({ info, parts: [] }))), [singleResponse(user)],
], )
[singleResponse(user)], const store = createServerSession(client)
) store.optimistic.add({ sessionID: "child", message: user, parts: [part] })
const store = createServerSession(client)
store.optimistic.add({ sessionID: "child", message: user, parts: [part] })
await store.sync("child") await store.sync("child")
store.optimistic.remove({ sessionID: "child", messageID: user.id }) store.optimistic.remove({ sessionID: "child", messageID: user.id })
expect(client.requests).toHaveLength(1) expect(client.requests).toHaveLength(1)
expect(client.rootRequests).toHaveLength(1) expect(client.rootRequests).toHaveLength(1)
expect(store.data.message.child).toEqual([user, ...assistants]) expect(store.data.message.child).toEqual([user, ...assistants])
}) })
test("backfills the parent of fetched assistants when another user is cached", async () => { test("backfills the parent of fetched assistants when another user is cached", async () => {
const unrelated = userMessage("message-0", { time: { created: 0 } }) const unrelated = userMessage("message-0", { time: { created: 0 } })
const user = userMessage("message-1") const user = userMessage("message-1")
const assistants = [assistantMessage("message-2", user.id), assistantMessage("message-3", user.id)] const assistants = [assistantMessage("message-2", user.id), assistantMessage("message-3", user.id)]
const client = rootMessageClient( const client = rootMessageClient(
[ [response([{ info: unrelated, parts: [] }]), response(assistants.map((info) => ({ info, parts: [] })))],
response([{ info: unrelated, parts: [] }]), [singleResponse(user)],
response(assistants.map((info) => ({ info, parts: [] }))), )
], const store = createServerSession(client)
[singleResponse(user)], await store.sync("child")
)
const store = createServerSession(client)
await store.sync("child")
await store.sync("child", { force: true }) await store.sync("child", { force: true })
expect(client.requests).toHaveLength(2) expect(client.requests).toHaveLength(2)
expect(client.rootRequests).toHaveLength(1) expect(client.rootRequests).toHaveLength(1)
expect(store.data.message.child).toEqual([unrelated, user, ...assistants]) expect(store.data.message.child).toEqual([unrelated, user, ...assistants])
}) })
test("preserves cached history between an injected parent and the page boundary", async () => { test("preserves cached history between an injected parent and the page boundary", async () => {
const user = userMessage("message-1") const user = userMessage("message-1")
const cached = userMessage("message-3", { time: { created: 3 } }) const cached = userMessage("message-3", { time: { created: 3 } })
const assistant = assistantMessage("message-4", user.id) const assistant = assistantMessage("message-4", user.id)
const client = rootMessageClient( const client = rootMessageClient(
[response([{ info: cached, parts: [] }]), response([{ info: assistant, parts: [] }])], [response([{ info: cached, parts: [] }]), response([{ info: assistant, parts: [] }])],
[singleResponse(user)], [singleResponse(user)],
) )
const store = createServerSession(client) const store = createServerSession(client)
await store.sync("child") await store.sync("child")
await store.sync("child", { force: true }) await store.sync("child", { force: true })
expect(store.data.message.child).toEqual([user, cached, assistant]) expect(store.data.message.child).toEqual([user, cached, assistant])
}) })
test("refreshes a cached parent omitted by an assistant-only replacement page", async () => { test("refreshes a cached parent omitted by an assistant-only replacement page", async () => {
const stale = userMessage("message-1", { summary: { title: "stale", diffs: [] } }) const stale = userMessage("message-1", { summary: { title: "stale", diffs: [] } })
const fresh = { ...stale, summary: { title: "fresh", diffs: [] } } const fresh = { ...stale, summary: { title: "fresh", diffs: [] } }
const stalePart = textPart(stale.id, { text: "stale" }) const stalePart = textPart(stale.id, { text: "stale" })
const freshPart = { ...stalePart, text: "fresh" } const freshPart = { ...stalePart, text: "fresh" }
const assistant = assistantMessage("message-2", stale.id) const assistant = assistantMessage("message-2", stale.id)
const client = rootMessageClient( const client = rootMessageClient(
[response([{ info: stale, parts: [stalePart] }]), response([{ info: assistant, parts: [] }])], [response([{ info: stale, parts: [stalePart] }]), response([{ info: assistant, parts: [] }])],
[singleResponse(fresh, [freshPart])], [singleResponse(fresh, [freshPart])],
) )
const store = createServerSession(client) const store = createServerSession(client)
await store.sync("child") await store.sync("child")
await store.sync("child", { force: true }) await store.sync("child", { force: true })
expect(client.rootRequests).toEqual([{ sessionID: "child", messageID: stale.id }]) expect(client.rootRequests).toEqual([{ sessionID: "child", messageID: stale.id }])
expect(store.data.message.child).toEqual([fresh, assistant]) expect(store.data.message.child).toEqual([fresh, assistant])
expect(store.data.part[stale.id]).toEqual([freshPart]) expect(store.data.part[stale.id]).toEqual([freshPart])
}) })
test("refreshes a confirmed optimistic parent while preserving pending parts", async () => { test("refreshes a confirmed optimistic parent while preserving pending parts", async () => {
const stale = userMessage("message-1", { summary: { title: "stale", diffs: [] } }) const stale = userMessage("message-1", { summary: { title: "stale", diffs: [] } })
const fresh = { ...stale, summary: { title: "fresh", diffs: [] } } const fresh = { ...stale, summary: { title: "fresh", diffs: [] } }
const confirmed = textPart(stale.id, { id: "confirmed", text: "stale" }) const confirmed = textPart(stale.id, { id: "confirmed", text: "stale" })
const refreshed = { ...confirmed, text: "fresh" } const refreshed = { ...confirmed, text: "fresh" }
const pending = textPart(stale.id, { id: "pending", text: "pending" }) const pending = textPart(stale.id, { id: "pending", text: "pending" })
const assistant = assistantMessage("message-2", stale.id) const assistant = assistantMessage("message-2", stale.id)
const client = rootMessageClient( const client = rootMessageClient(
[response([{ info: stale, parts: [confirmed] }]), response([{ info: assistant, parts: [] }])], [response([{ info: stale, parts: [confirmed] }]), response([{ info: assistant, parts: [] }])],
[singleResponse(fresh, [refreshed])], [singleResponse(fresh, [refreshed])],
) )
const store = createServerSession(client) const store = createServerSession(client)
store.optimistic.add({ sessionID: "child", message: stale, parts: [confirmed, pending] }) store.optimistic.add({ sessionID: "child", message: stale, parts: [confirmed, pending] })
await store.sync("child") await store.sync("child")
await store.sync("child", { force: true }) await store.sync("child", { force: true })
expect(client.rootRequests).toEqual([{ sessionID: "child", messageID: stale.id }]) expect(client.rootRequests).toEqual([{ sessionID: "child", messageID: stale.id }])
expect(store.data.message.child).toEqual([fresh, assistant]) expect(store.data.message.child).toEqual([fresh, assistant])
expect(store.data.part[stale.id]).toEqual([refreshed, pending]) expect(store.data.part[stale.id]).toEqual([refreshed, pending])
}) })
test("uses a parent received by SSE during the replacement load", async () => { test("uses a parent received by SSE during the replacement load", async () => {
const pending = deferredResponse() const pending = deferredResponse()
const user = userMessage("message-1") const user = userMessage("message-1")
const assistant = assistantMessage("message-2", user.id) const assistant = assistantMessage("message-2", user.id)
const client = rootMessageClient([pending.promise], []) const client = rootMessageClient([pending.promise], [])
const store = createServerSession(client) const store = createServerSession(client)
const loading = store.sync("child") const loading = store.sync("child")
store.apply({ type: "message.updated", properties: { info: user } }) store.apply({ type: "message.updated", properties: { info: user } })
pending.resolve(response([{ info: assistant, parts: [] }])) pending.resolve(response([{ info: assistant, parts: [] }]))
await loading await loading
expect(client.rootRequests).toEqual([]) expect(client.rootRequests).toEqual([])
expect(store.data.message.child).toEqual([user, assistant]) expect(store.data.message.child).toEqual([user, assistant])
}) })
test("uses a successful retry over events received by a failed backfill attempt", async () => { test("uses a successful retry over events received by a failed backfill attempt", async () => {
const failed = deferredResponse() const failed = deferredResponse()
const user = userMessage("message-1") const user = userMessage("message-1")
const live = { ...user, agent: "stale" } const live = { ...user, agent: "stale" }
const assistants = [assistantMessage("message-2", user.id), assistantMessage("message-3", user.id)] const assistants = [assistantMessage("message-2", user.id), assistantMessage("message-3", user.id)]
const client = rootMessageClient( const client = rootMessageClient(
[ [response(assistants.map((info) => ({ info, parts: [] })))],
response( [failed.promise.then((result) => ({ data: result.data[0]! })), singleResponse(user)],
assistants.map((info) => ({ info, parts: [] })), )
), const store = createServerSession(client, { retry: retryImmediately })
], const loading = store.sync("child")
[failed.promise.then((result) => ({ data: result.data[0]! })), singleResponse(user)], await client.rootRequested(1)
)
const store = createServerSession(client, { retry: retryImmediately })
const loading = store.sync("child")
await client.rootRequested(1)
store.apply({ type: "message.updated", properties: { info: live } }) store.apply({ type: "message.updated", properties: { info: live } })
failed.reject(new Error("retry")) failed.reject(new Error("retry"))
await loading await loading
expect(client.requests).toHaveLength(1) expect(client.requests).toHaveLength(1)
expect(client.rootRequests).toHaveLength(2) expect(client.rootRequests).toHaveLength(2)
expect(store.data.message.child).toEqual([user, ...assistants]) expect(store.data.message.child).toEqual([user, ...assistants])
}) })
test("preserves newer-page events across a failed parent retry", async () => { test("preserves newer-page events across a failed parent retry", async () => {
const failed = deferredResponse() const failed = deferredResponse()
const user = userMessage("message-1") const user = userMessage("message-1")
const assistant = assistantMessage("message-2", user.id) const assistant = assistantMessage("message-2", user.id)
const live = { ...assistant, cost: 1 } const live = { ...assistant, cost: 1 }
const client = rootMessageClient( const client = rootMessageClient(
[response([{ info: assistant, parts: [] }])], [response([{ info: assistant, parts: [] }])],
[failed.promise.then((result) => ({ data: result.data[0]! })), singleResponse(user)], [failed.promise.then((result) => ({ data: result.data[0]! })), singleResponse(user)],
) )
const store = createServerSession(client, { retry: retryImmediately }) const store = createServerSession(client, { retry: retryImmediately })
const loading = store.sync("child") const loading = store.sync("child")
await client.rootRequested(1) await client.rootRequested(1)
store.apply({ type: "message.updated", properties: { info: live } }) store.apply({ type: "message.updated", properties: { info: live } })
failed.reject(new Error("retry")) failed.reject(new Error("retry"))
await loading await loading
expect(store.data.message.child).toEqual([user, live]) expect(store.data.message.child).toEqual([user, live])
}) })
test("preserves unrelated message events across a failed parent retry", async () => { test("preserves unrelated message events across a failed parent retry", async () => {
const failed = deferredResponse() const failed = deferredResponse()
const user = userMessage("message-1") const user = userMessage("message-1")
const assistant = assistantMessage("message-2", user.id) const assistant = assistantMessage("message-2", user.id)
const live = userMessage("message-4", { time: { created: 4 } }) const live = userMessage("message-4", { time: { created: 4 } })
const client = rootMessageClient( const client = rootMessageClient(
[response([{ info: assistant, parts: [] }])], [response([{ info: assistant, parts: [] }])],
[failed.promise.then((result) => ({ data: result.data[0]! })), singleResponse(user)], [failed.promise.then((result) => ({ data: result.data[0]! })), singleResponse(user)],
) )
const store = createServerSession(client, { retry: retryImmediately }) const store = createServerSession(client, { retry: retryImmediately })
const loading = store.sync("child") const loading = store.sync("child")
await client.rootRequested(1) await client.rootRequested(1)
store.apply({ type: "message.updated", properties: { info: live } }) store.apply({ type: "message.updated", properties: { info: live } })
failed.reject(new Error("retry")) failed.reject(new Error("retry"))
await loading await loading
expect(store.data.message.child).toEqual([user, assistant, live]) expect(store.data.message.child).toEqual([user, assistant, live])
}) })
test("preserves newer-page part events across a failed parent retry", async () => { test("preserves newer-page part events across a failed parent retry", async () => {
const failed = deferredResponse() const failed = deferredResponse()
const user = userMessage("message-1") const user = userMessage("message-1")
const assistant = assistantMessage("message-2", user.id) const assistant = assistantMessage("message-2", user.id)
const stale = textPart(assistant.id, { text: "stale" }) const stale = textPart(assistant.id, { text: "stale" })
const live = { ...stale, text: "live" } const live = { ...stale, text: "live" }
const client = rootMessageClient( const client = rootMessageClient(
[response([{ info: assistant, parts: [stale] }])], [response([{ info: assistant, parts: [stale] }])],
[failed.promise.then((result) => ({ data: result.data[0]! })), singleResponse(user)], [failed.promise.then((result) => ({ data: result.data[0]! })), singleResponse(user)],
) )
const store = createServerSession(client, { retry: retryImmediately }) const store = createServerSession(client, { retry: retryImmediately })
const loading = store.sync("child") const loading = store.sync("child")
await client.rootRequested(1) await client.rootRequested(1)
store.apply({ type: "message.part.updated", properties: { sessionID: "child", part: live, time: 2 } }) store.apply({ type: "message.part.updated", properties: { sessionID: "child", part: live, time: 2 } })
failed.reject(new Error("retry")) failed.reject(new Error("retry"))
await loading await loading
expect(store.data.part[assistant.id]).toEqual([live]) expect(store.data.part[assistant.id]).toEqual([live])
}) })
}) })
test("merges live events into the initial page", async () => { test("merges live events into the initial page", async () => {
+12 -18
View File
@@ -1,11 +1,7 @@
import { Binary } from "@opencode-ai/core/util/binary" import { Binary } from "@opencode-ai/core/util/binary"
import { retry } from "@opencode-ai/core/util/retry" import { retry } from "@opencode-ai/core/util/retry"
import type { OpenCodeEvent, SessionApi, SessionInfo, SessionMessageInfo } from "@opencode-ai/client/promise" import type { OpenCodeEvent, SessionApi, SessionInfo, SessionMessageInfo } from "@opencode-ai/client/promise"
import type { import type { Message, Part, Todo } from "@/types"
Message,
Part,
Todo,
} from "@/types"
import type { FileDiffInfo, PermissionRequest, QuestionRequest, SessionStatus } from "@opencode-ai/client/promise" import type { FileDiffInfo, PermissionRequest, QuestionRequest, SessionStatus } from "@opencode-ai/client/promise"
import { batch } from "solid-js" import { batch } from "solid-js"
import { createStore, produce, reconcile } from "solid-js/store" import { createStore, produce, reconcile } from "solid-js/store"
@@ -38,14 +34,16 @@ function projectMessageSource(message: Message): SessionMessageInfo[] {
{ id: message.id, type: "user", text: "", time: message.time }, { id: message.id, type: "user", text: "", time: message.time },
] ]
} }
return [{ return [
id: message.id, {
type: "assistant", id: message.id,
agent: message.agent ?? message.mode, type: "assistant",
model: { id: message.modelID, providerID: message.providerID, variant: message.variant }, agent: message.agent ?? message.mode,
content: [], model: { id: message.modelID, providerID: message.providerID, variant: message.variant },
time: message.time, content: [],
}] time: message.time,
},
]
} }
function needsOlderTurnRoot(source: readonly SessionMessageInfo[]) { function needsOlderTurnRoot(source: readonly SessionMessageInfo[]) {
@@ -243,11 +241,7 @@ export function createServerSession(
const indexProjectedMessage = (message: Message) => { const indexProjectedMessage = (message: Message) => {
const current = data.session_message[message.sessionID] ?? [] const current = data.session_message[message.sessionID] ?? []
if (current.some((item) => item.id === message.id)) return if (current.some((item) => item.id === message.id)) return
setData( setData("session_message", message.sessionID, reconcile([...current, ...projectMessageSource(message)]))
"session_message",
message.sessionID,
reconcile([...current, ...projectMessageSource(message)]),
)
} }
const remember = (session: SessionInfo) => { const remember = (session: SessionInfo) => {
+3 -12
View File
@@ -1,9 +1,4 @@
import type { import type { Config, Path, Project, ProviderAuthResponse } from "@/types"
Config,
Path,
Project,
ProviderAuthResponse,
} from "@/types"
import { showToast } from "@/utils/toast" import { showToast } from "@/utils/toast"
import { getFilename } from "@opencode-ai/core/util/path" import { getFilename } from "@opencode-ai/core/util/path"
import { type Accessor, batch, createMemo, getOwner, onCleanup, onMount, untrack } from "solid-js" import { type Accessor, batch, createMemo, getOwner, onCleanup, onMount, untrack } from "solid-js"
@@ -164,15 +159,11 @@ export function seedActiveSessionStatuses(
} }
} }
function makeQueryOptionsApi( function makeQueryOptionsApi(scope: ServerScope, serverAPI: ServerApi) {
scope: ServerScope,
serverAPI: ServerApi,
) {
return { return {
globalConfig: () => loadGlobalConfigQuery(scope), globalConfig: () => loadGlobalConfigQuery(scope),
projects: () => loadProjectsQuery(scope, serverAPI.project), projects: () => loadProjectsQuery(scope, serverAPI.project),
providers: (directory: PathKey | null) => providers: (directory: PathKey | null) => loadProvidersQuery(scope, directory, serverAPI),
loadProvidersQuery(scope, directory, serverAPI),
path: (directory: PathKey | null) => loadPathQuery(scope, directory, serverAPI.location), path: (directory: PathKey | null) => loadPathQuery(scope, directory, serverAPI.location),
agents: (directory: PathKey) => loadAgentsQuery(scope, directory, serverAPI.agent), agents: (directory: PathKey) => loadAgentsQuery(scope, directory, serverAPI.agent),
references: (directory: PathKey) => loadReferencesQuery(scope, directory, serverAPI.reference), references: (directory: PathKey) => loadReferencesQuery(scope, directory, serverAPI.reference),
+7 -4
View File
@@ -268,10 +268,13 @@ function createWorkspaceTerminalSession(
const index = store.all.findIndex((x) => x.id === id) const index = store.all.findIndex((x) => x.id === id)
const pty = store.all[index] const pty = store.all[index]
if (!pty) return if (!pty) return
const data = await sdk.api.pty.create({ location, title: pty.title }).then((result) => result.data).catch((error: unknown) => { const data = await sdk.api.pty
console.error("Failed to clone terminal", error) .create({ location, title: pty.title })
return undefined .then((result) => result.data)
}) .catch((error: unknown) => {
console.error("Failed to clone terminal", error)
return undefined
})
if (!data?.id) return if (!data?.id) return
const active = store.active === pty.id const active = store.active === pty.id
@@ -45,13 +45,7 @@ import { StickyAccordionHeader } from "@opencode-ai/ui/sticky-accordion-header"
import { TextField } from "@opencode-ai/ui/text-field" import { TextField } from "@opencode-ai/ui/text-field"
import { TextReveal } from "@opencode-ai/ui/text-reveal" import { TextReveal } from "@opencode-ai/ui/text-reveal"
import { TextShimmer } from "@opencode-ai/ui/text-shimmer" import { TextShimmer } from "@opencode-ai/ui/text-shimmer"
import type { import type { AssistantMessage, Message as MessageType, Part as PartType, ToolPart, UserMessage } from "@/types"
AssistantMessage,
Message as MessageType,
Part as PartType,
ToolPart,
UserMessage,
} from "@/types"
import { showToast } from "@/utils/toast" import { showToast } from "@/utils/toast"
import { getDirectory, getFilename } from "@opencode-ai/core/util/path" import { getDirectory, getFilename } from "@opencode-ai/core/util/path"
import { Popover as KobaltePopover } from "@kobalte/core/popover" import { Popover as KobaltePopover } from "@kobalte/core/popover"
@@ -54,9 +54,9 @@ describe("reviewDiffNeedsLoad", () => {
patch: "@@ -0,0 +1 @@\n+value", patch: "@@ -0,0 +1 @@\n+value",
}), }),
).toBe(false) ).toBe(false)
expect( expect(reviewDiffNeedsLoad({ file: "empty.txt", patch: "", additions: 0, deletions: 0, status: "modified" })).toBe(
reviewDiffNeedsLoad({ file: "empty.txt", patch: "", additions: 0, deletions: 0, status: "modified" }), false,
).toBe(false) )
}) })
}) })
+1 -5
View File
@@ -1,8 +1,4 @@
import type { import type { EventSubscribeOutput, FileDiffInfo, ProjectListOutput } from "@opencode-ai/client/promise"
EventSubscribeOutput,
FileDiffInfo,
ProjectListOutput,
} from "@opencode-ai/client/promise"
import type { NormalizedProviderListResponse } from "@opencode-ai/session-ui/context" import type { NormalizedProviderListResponse } from "@opencode-ai/session-ui/context"
export type Project = Omit<ProjectListOutput[number], "canonical"> & { worktree: string } export type Project = Omit<ProjectListOutput[number], "canonical"> & { worktree: string }
+12 -2
View File
@@ -119,7 +119,15 @@ function installPackage(name) {
try { try {
const result = childProcess.spawnSync( const result = childProcess.spawnSync(
"npm", "npm",
["install", "--ignore-scripts", "--no-save", "--loglevel=error", "--prefix", temp, `${name}@${dependencies[name]}`], [
"install",
"--ignore-scripts",
"--no-save",
"--loglevel=error",
"--prefix",
temp,
`${name}@${dependencies[name]}`,
],
{ stdio: "inherit", windowsHide: true }, { stdio: "inherit", windowsHide: true },
) )
if (result.status !== 0) return false if (result.status !== 0) return false
@@ -150,7 +158,9 @@ function main() {
} }
} }
throw new Error(`Failed to install OpenCode. Try manually installing ${names.map((name) => JSON.stringify(name)).join(" or ")}.`) throw new Error(
`Failed to install OpenCode. Try manually installing ${names.map((name) => JSON.stringify(name)).join(" or ")}.`,
)
} }
try { try {
+11 -13
View File
@@ -40,15 +40,13 @@ try {
const token = encodeURIComponent(credential) const token = encodeURIComponent(credential)
const health = await waitForReady(info.url, headers) const health = await waitForReady(info.url, headers)
if (health.pid !== info.pid) throw new Error("Health process does not match registration") if (health.pid !== info.pid) throw new Error("Health process does not match registration")
const tokenHealth = await fetch( const tokenHealth = await fetch(new URL(`/api/health?auth_token=${token}`, info.url), {
new URL(`/api/health?auth_token=${token}`, info.url), signal: AbortSignal.timeout(5_000),
{ signal: AbortSignal.timeout(5_000) }, })
)
if (tokenHealth.status !== 200) throw new Error("Compiled service rejected query authentication") if (tokenHealth.status !== 200) throw new Error("Compiled service rejected query authentication")
const tokenOpenApi = await fetch( const tokenOpenApi = await fetch(new URL(`/openapi.json?auth_token=${token}`, info.url), {
new URL(`/openapi.json?auth_token=${token}`, info.url), signal: AbortSignal.timeout(5_000),
{ signal: AbortSignal.timeout(5_000) }, })
)
if (tokenOpenApi.status !== 200) throw new Error("Compiled application rejected query authentication") if (tokenOpenApi.status !== 200) throw new Error("Compiled application rejected query authentication")
if ((await pluginIDs(info.url, headers)).includes("smoke")) throw new Error("Smoke plugin existed before creation") if ((await pluginIDs(info.url, headers)).includes("smoke")) throw new Error("Smoke plugin existed before creation")
const plugin = path.join(root, ".opencode", "plugins", "smoke.ts") const plugin = path.join(root, ".opencode", "plugins", "smoke.ts")
@@ -63,7 +61,8 @@ try {
const unauthorizedOpenApi = await fetch(new URL("/openapi.json", info.url), { const unauthorizedOpenApi = await fetch(new URL("/openapi.json", info.url), {
signal: AbortSignal.timeout(5_000), signal: AbortSignal.timeout(5_000),
}) })
if (unauthorizedOpenApi.status !== 401) throw new Error("Compiled service exposed application routes without authentication") if (unauthorizedOpenApi.status !== 401)
throw new Error("Compiled service exposed application routes without authentication")
const unauthorizedStop = await fetch(new URL("/api/service/stop", info.url), { const unauthorizedStop = await fetch(new URL("/api/service/stop", info.url), {
method: "POST", method: "POST",
headers: { "content-type": "application/json" }, headers: { "content-type": "application/json" },
@@ -94,7 +93,8 @@ try {
} finally { } finally {
processes.forEach((process) => process.kill()) processes.forEach((process) => process.kill())
await Promise.all(processes.map((process) => process.exited)) await Promise.all(processes.map((process) => process.exited))
if (failure) errors.push(fs.readFile(path.join(root, "data", "opencode", "log", "opencode.log"), "utf8").catch(() => "")) if (failure)
errors.push(fs.readFile(path.join(root, "data", "opencode", "log", "opencode.log"), "utf8").catch(() => ""))
} }
const output = await Promise.all(errors) const output = await Promise.all(errors)
@@ -154,9 +154,7 @@ async function pluginIDs(url: string, headers: HeadersInit) {
throw new Error("Compiled service returned an invalid plugin list") throw new Error("Compiled service returned an invalid plugin list")
} }
return body.data.flatMap((plugin) => return body.data.flatMap((plugin) =>
typeof plugin === "object" && plugin !== null && "id" in plugin && typeof plugin.id === "string" typeof plugin === "object" && plugin !== null && "id" in plugin && typeof plugin.id === "string" ? [plugin.id] : [],
? [plugin.id]
: [],
) )
} }
+5 -4
View File
@@ -20,7 +20,8 @@ export default Runtime.handler(
return response.text() return response.text()
}) })
: Bun.file(input.file).text(), : Bun.file(input.file).text(),
catch: (cause) => new Error(`Failed to read session data: ${cause instanceof Error ? cause.message : String(cause)}`), catch: (cause) =>
new Error(`Failed to read session data: ${cause instanceof Error ? cause.message : String(cause)}`),
}) })
const data = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(SessionTransfer.Data))(text) const data = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(SessionTransfer.Data))(text)
const encoded = Schema.encodeSync(SessionTransfer.Data)(data) const encoded = Schema.encodeSync(SessionTransfer.Data)(data)
@@ -52,9 +53,9 @@ export default Runtime.handler(
return return
} }
if (!response.ok) yield* Effect.fail(new Error(`Failed to import session: ${response.statusText}`)) if (!response.ok) yield* Effect.fail(new Error(`Failed to import session: ${response.statusText}`))
const imported = yield* Schema.decodeUnknownEffect( const imported = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Struct({ data: Session.Info })))(
Schema.fromJsonString(Schema.Struct({ data: Session.Info })), yield* Effect.promise(() => response.text()),
)(yield* Effect.promise(() => response.text())) )
process.stdout.write(`Imported session: ${imported.data.id}${EOL}`) process.stdout.write(`Imported session: ${imported.data.id}${EOL}`)
}), }),
) )
+1 -3
View File
@@ -36,8 +36,6 @@ export default Runtime.handler(
const hostname = new URL(endpoint.url).hostname const hostname = new URL(endpoint.url).hostname
if (!["localhost", "127.0.0.1", "[::1]"].includes(hostname)) return if (!["localhost", "127.0.0.1", "[::1]"].includes(hostname)) return
process.stderr.write( process.stderr.write(` Run \`opencode service set hostname 0.0.0.0\` to access the service remotely.${EOL}${EOL}`)
` Run \`opencode service set hostname 0.0.0.0\` to access the service remotely.${EOL}${EOL}`,
)
}), }),
) )
+7 -3
View File
@@ -48,12 +48,16 @@ export function migrateV1(legacy: TuiConfigV1.Info | undefined, kv: Record<strin
const attentionSoundPack = kv.attention_sound_pack const attentionSoundPack = kv.attention_sound_pack
const diffView = kv.diff_viewer_view ?? (legacy?.diff_style === "stacked" ? "unified" : undefined) const diffView = kv.diff_viewer_view ?? (legacy?.diff_style === "stacked" ? "unified" : undefined)
const thinking = const thinking =
kv.thinking_mode ?? kv.thinking_mode ?? (kv.thinking_visibility === undefined ? undefined : kv.thinking_visibility ? "show" : "hide")
(kv.thinking_visibility === undefined ? undefined : kv.thinking_visibility ? "show" : "hide")
return { return {
...(themeName !== undefined || themeMode !== undefined ...(themeName !== undefined || themeMode !== undefined
? { theme: { ...(themeName === undefined ? {} : { name: themeName }), ...(themeMode === undefined ? {} : { mode: themeMode }) } } ? {
theme: {
...(themeName === undefined ? {} : { name: themeName }),
...(themeMode === undefined ? {} : { mode: themeMode }),
},
}
: {}), : {}),
...(legacy?.keybinds === undefined ? {} : { keybinds: legacy.keybinds }), ...(legacy?.keybinds === undefined ? {} : { keybinds: legacy.keybinds }),
...(plugins.length ? { plugins } : {}), ...(plugins.length ? { plugins } : {}),
+1 -4
View File
@@ -468,10 +468,7 @@ export async function runNonInteractivePrompt(input: Input) {
continue continue
} }
if (event.type === "session.step.failed") { if (event.type === "session.step.failed") {
if ( if (input.compatibility === "v1" && event.data.error.message === "The provider response ended unexpectedly.") {
input.compatibility === "v1" &&
event.data.error.message === "The provider response ended unexpectedly."
) {
pendingStep = undefined pendingStep = undefined
v1InvalidOutput = true v1InvalidOutput = true
continue continue
@@ -62,10 +62,7 @@ function managedService(options: EnsureOptions) {
} }
} }
const resolveManaged = Effect.fnUntraced(function* ( const resolveManaged = Effect.fnUntraced(function* (options: EnsureOptions, mismatch: NonNullable<Args["mismatch"]>) {
options: EnsureOptions,
mismatch: NonNullable<Args["mismatch"]>,
) {
if (mismatch === "replace") return yield* Service.ensure(options) if (mismatch === "replace") return yield* Service.ensure(options)
if (mismatch === "ignore") return yield* Service.ensure({ ...options, version: undefined }) if (mismatch === "ignore") return yield* Service.ensure({ ...options, version: undefined })
@@ -413,9 +413,7 @@ function UpdateFooter(props: {
const completion = smoothstep(headerFade.progress()) const completion = smoothstep(headerFade.progress())
return Array.from({ length: width }, (_, index) => { return Array.from({ length: width }, (_, index) => {
const color = const color =
index >= filled index >= filled ? colors.muted : shade(railRamp, Math.max(0, 1 - Math.abs(index - center) / glowRadius) ** 2)
? colors.muted
: shade(railRamp, Math.max(0, 1 - Math.abs(index - center) / glowRadius) ** 2)
return { return {
char: success || index < filled ? "━" : "·", char: success || index < filled ? "━" : "·",
color: success ? blend(color, colors.accent, completion) : color, color: success ? blend(color, colors.accent, completion) : color,
+35 -34
View File
@@ -52,9 +52,10 @@ export const layer = Layer.effect(
const readPolicy = Effect.fnUntraced(function* () { const readPolicy = Effect.fnUntraced(function* () {
const values = yield* Effect.forEach(["config.json", "opencode.json", "opencode.jsonc"], (name) => const values = yield* Effect.forEach(["config.json", "opencode.json", "opencode.jsonc"], (name) =>
fs fs.readFileString(path.join(global.config, name)).pipe(
.readFileString(path.join(global.config, name)) Effect.map(decodePolicy),
.pipe(Effect.map(decodePolicy), Effect.catch(() => Effect.succeed(undefined))), Effect.catch(() => Effect.succeed(undefined)),
),
) )
return values.findLast((value) => value !== undefined) ?? true return values.findLast((value) => value !== undefined) ?? true
}) })
@@ -94,10 +95,10 @@ export const layer = Layer.effect(
const latest = Effect.fnUntraced(function* () { const latest = Effect.fnUntraced(function* () {
const response = yield* Effect.tryPromise({ const response = yield* Effect.tryPromise({
try: () => try: () =>
fetch( fetch(`https://update.opencode.ai/api/${encodeURIComponent(channel)}/cli/npm`, {
`https://update.opencode.ai/api/${encodeURIComponent(channel)}/cli/npm`, headers: { "User-Agent": `opencode/${OPENCODE_VERSION}` },
{ headers: { "User-Agent": `opencode/${OPENCODE_VERSION}` }, signal: AbortSignal.timeout(10_000) }, signal: AbortSignal.timeout(10_000),
), }),
catch: (cause) => new Error("Failed to check for updates", { cause }), catch: (cause) => new Error("Failed to check for updates", { cause }),
}) })
if (!response.ok) return yield* Effect.fail(new Error(`Update check failed with status ${response.status}`)) if (!response.ok) return yield* Effect.fail(new Error(`Update check failed with status ${response.status}`))
@@ -118,7 +119,7 @@ export const layer = Layer.effect(
pnpm: ["pnpm", "install", "--global", target], pnpm: ["pnpm", "install", "--global", target],
yarn: ["yarn", "global", "add", target], yarn: ["yarn", "global", "add", target],
} }
const result = yield* (method === "bun" const result = yield* method === "bun"
? Effect.scoped( ? Effect.scoped(
Effect.gen(function* () { Effect.gen(function* () {
// Bun does not prune old versions from its shared package cache. // Bun does not prune old versions from its shared package cache.
@@ -127,38 +128,38 @@ export const layer = Layer.effect(
return yield* run(["bun", "install", "--global", "--cache-dir", cache, target], "5 minutes") return yield* run(["bun", "install", "--global", "--cache-dir", cache, target], "5 minutes")
}), }),
) )
: run(commands[method], "5 minutes")) : run(commands[method], "5 minutes")
if (result.code === 0) return if (result.code === 0) return
return yield* Effect.fail(new Error(result.stderr.trim() || `Failed to update with ${method}`)) return yield* Effect.fail(new Error(result.stderr.trim() || `Failed to update with ${method}`))
}) })
const check = Effect.fn("cli.updater.check")(function* () { const check = Effect.fn("cli.updater.check")(
if ( function* () {
OPENCODE_LOCAL || if (OPENCODE_LOCAL || ["1", "true"].includes(process.env.OPENCODE_DISABLE_AUTOUPDATE?.toLowerCase() ?? ""))
["1", "true"].includes(process.env.OPENCODE_DISABLE_AUTOUPDATE?.toLowerCase() ?? "") return yield* Effect.logInfo("update check skipped", {
) reason: OPENCODE_LOCAL ? "local-install" : "disabled",
return yield* Effect.logInfo("update check skipped", { version: OPENCODE_VERSION,
reason: OPENCODE_LOCAL ? "local-install" : "disabled", channel: OPENCODE_CHANNEL,
version: OPENCODE_VERSION, })
channel: OPENCODE_CHANNEL, const policy = yield* readPolicy()
}) if (policy === false) return yield* Effect.logInfo("update check skipped", { reason: "policy-disabled" })
const policy = yield* readPolicy()
if (policy === false) return yield* Effect.logInfo("update check skipped", { reason: "policy-disabled" })
return yield* Effect.gen(function* () { return yield* Effect.gen(function* () {
const version = yield* latest() const version = yield* latest()
yield* Effect.logInfo("update check", { yield* Effect.logInfo("update check", {
current: OPENCODE_VERSION, current: OPENCODE_VERSION,
latest: version, latest: version,
})
const next = action(OPENCODE_VERSION, version, policy)
if (next === "none") return yield* Effect.logInfo("update check done", { action: "up-to-date" })
const detected = yield* method()
if (!detected) return yield* Effect.logWarning("automatic update skipped: installation method not found")
yield* upgrade(detected, version)
yield* Effect.logInfo("updated OpenCode", { from: OPENCODE_VERSION, to: version, method: detected })
}) })
const next = action(OPENCODE_VERSION, version, policy) },
if (next === "none") return yield* Effect.logInfo("update check done", { action: "up-to-date" }) Effect.catchCause((cause) => Effect.logWarning("automatic update failed", { cause })),
const detected = yield* method() )
if (!detected) return yield* Effect.logWarning("automatic update skipped: installation method not found")
yield* upgrade(detected, version)
yield* Effect.logInfo("updated OpenCode", { from: OPENCODE_VERSION, to: version, method: detected })
})
}, Effect.catchCause((cause) => Effect.logWarning("automatic update failed", { cause })))
return Service.of({ check }) return Service.of({ check })
}), }),
+2 -2
View File
@@ -141,7 +141,7 @@ export async function createTimelineHost(): Promise<TimelineHost> {
if (closeTask) return closeTask if (closeTask) return closeTask
closed = true closed = true
closeTask = (async () => { closeTask = (async () => {
await active?.catch(() => { }) await active?.catch(() => {})
signals.forEach((signal) => process.off(signal, cancel)) signals.forEach((signal) => process.off(signal, cancel))
})() })()
return closeTask return closeTask
@@ -211,7 +211,7 @@ export async function createTimelineHost(): Promise<TimelineHost> {
if (closeTask) return closeTask if (closeTask) return closeTask
closed = true closed = true
closeTask = (async () => { closeTask = (async () => {
await active?.catch(() => { }) await active?.catch(() => {})
try { try {
await shutdown(activeRenderer) await shutdown(activeRenderer)
await bounded(renderTask) await bounded(renderTask)
+2 -4
View File
@@ -102,9 +102,7 @@ test("migrates before the first update and does not remigrate afterward", async
draft.animations = false draft.animations = false
draft.mouse = false draft.mouse = false
}) })
yield* Effect.promise(() => yield* Effect.promise(() => Bun.write(path.join(directory, "tui.json"), JSON.stringify({ theme: "changed" })))
Bun.write(path.join(directory, "tui.json"), JSON.stringify({ theme: "changed" })),
)
return yield* service.get() return yield* service.get()
}), }),
) )
@@ -122,7 +120,7 @@ test("migrates before the first update and does not remigrate afterward", async
test("updates a config draft while preserving JSONC comments", async () => { test("updates a config draft while preserving JSONC comments", async () => {
const directory = await Bun.$`mktemp -d`.text().then((value) => value.trim()) const directory = await Bun.$`mktemp -d`.text().then((value) => value.trim())
await Bun.write(path.join(directory, "cli.json"), "{\n // Keep this comment\n \"animations\": true\n}\n") await Bun.write(path.join(directory, "cli.json"), '{\n // Keep this comment\n "animations": true\n}\n')
try { try {
const config = await run( const config = await run(
@@ -17,7 +17,9 @@ export default defineScript({
const session = `mini-stage2-${process.pid}` const session = `mini-stage2-${process.pid}`
const snapshots = path.join(artifacts, "mini-stage2") const snapshots = path.join(artifacts, "mini-stage2")
const explicitDirectory = path.join(artifacts, "explicit-model") const explicitDirectory = path.join(artifacts, "explicit-model")
yield* Effect.promise(() => Promise.all([snapshots, explicitDirectory].map((dir) => mkdir(dir, { recursive: true })))) yield* Effect.promise(() =>
Promise.all([snapshots, explicitDirectory].map((dir) => mkdir(dir, { recursive: true }))),
)
/** @param {string} directory @param {string | undefined} model */ /** @param {string} directory @param {string | undefined} model */
const mini = (directory, model) => [ const mini = (directory, model) => [
"env", "env",
@@ -169,14 +171,7 @@ export default defineScript({
yield* Effect.promise(() => tmux(["clear-history", "-t", session])) yield* Effect.promise(() => tmux(["clear-history", "-t", session]))
yield* Effect.promise(() => yield* Effect.promise(() =>
tmux([ tmux(["respawn-pane", "-k", "-t", session, "--", ...mini(explicitDirectory, "simulation/gpt-sim-model")]),
"respawn-pane",
"-k",
"-t",
session,
"--",
...mini(explicitDirectory, "simulation/gpt-sim-model"),
]),
) )
const explicitModel = yield* Effect.promise(() => waitForPane(session, "Simulated Model", 15_000)) const explicitModel = yield* Effect.promise(() => waitForPane(session, "Simulated Model", 15_000))
yield* Effect.promise(() => Bun.write(path.join(snapshots, "07-explicit-model.txt"), explicitModel)) yield* Effect.promise(() => Bun.write(path.join(snapshots, "07-explicit-model.txt"), explicitModel))
+1 -8
View File
@@ -159,14 +159,7 @@ test("import validates a file and sends it to the resolved location", async () =
}) })
try { try {
const [stdout, , exitCode] = await run([ const [stdout, , exitCode] = await run(["import", file, "--directory", root, "--server", server.url.toString()])
"import",
file,
"--directory",
root,
"--server",
server.url.toString(),
])
expect(exitCode).toBe(0) expect(exitCode).toBe(0)
expect(stdout).toBe(`Imported session: ${info.id}${os.EOL}`) expect(stdout).toBe(`Imported session: ${info.id}${os.EOL}`)
+1 -7
View File
@@ -3,13 +3,7 @@ import { readFile } from "node:fs/promises"
import { createRequire } from "node:module" import { createRequire } from "node:module"
import { defineConfig, type Plugin, type UserConfig } from "vite" import { defineConfig, type Plugin, type UserConfig } from "vite"
import solid from "vite-plugin-solid" import solid from "vite-plugin-solid"
import { import { nodeExecArgv, nodeTarget, type NodeTarget, photonWasmAsset, shellParserWasmAssets } from "./src/node/target"
nodeExecArgv,
nodeTarget,
type NodeTarget,
photonWasmAsset,
shellParserWasmAssets,
} from "./src/node/target"
const dir = import.meta.dirname const dir = import.meta.dirname
+1 -6
View File
@@ -1,11 +1,6 @@
import { NodeFileSystem } from "@effect/platform-node" import { NodeFileSystem } from "@effect/platform-node"
import { compile, emitEffectImported, emitEffectShape, emitPromise, write } from "@opencode-ai/httpapi-codegen" import { compile, emitEffectImported, emitEffectShape, emitPromise, write } from "@opencode-ai/httpapi-codegen"
import { import { ClientApi, effectOmitEndpoints, groupNames, promiseOmitEndpoints } from "@opencode-ai/protocol/client"
ClientApi,
effectOmitEndpoints,
groupNames,
promiseOmitEndpoints,
} from "@opencode-ai/protocol/client"
import { Agent } from "@opencode-ai/schema/agent" import { Agent } from "@opencode-ai/schema/agent"
import { Command } from "@opencode-ai/schema/command" import { Command } from "@opencode-ai/schema/command"
import { Config } from "@opencode-ai/schema/config" import { Config } from "@opencode-ai/schema/config"
+1 -6
View File
@@ -1,6 +1 @@
export { export { ClientApi, effectOmitEndpoints, groupNames, promiseOmitEndpoints } from "@opencode-ai/protocol/client"
ClientApi,
effectOmitEndpoints,
groupNames,
promiseOmitEndpoints,
} from "@opencode-ai/protocol/client"
@@ -1,3 +1 @@
[ ["api.ts"]
"api.ts"
]
@@ -1,5 +1 @@
[ ["client-error.ts", "client.ts", "index.ts"]
"client-error.ts",
"client.ts",
"index.ts"
]
@@ -1,6 +1 @@
[ ["client-error.ts", "client.ts", "index.ts", "types.ts"]
"client-error.ts",
"client.ts",
"index.ts",
"types.ts"
]
+2 -4
View File
@@ -66,8 +66,7 @@ export async function ensure(options: EnsureOptions = {}): Promise<Endpoint> {
if (registration.timedOut && registration.info !== undefined) { if (registration.timedOut && registration.info !== undefined) {
timeouts = { timeouts = {
info: registration.info, info: registration.info,
count: count: timeouts !== undefined && same(timeouts.info, registration.info) ? timeouts.count + 1 : 1,
timeouts !== undefined && same(timeouts.info, registration.info) ? timeouts.count + 1 : 1,
} }
if (timeouts.count >= 3) { if (timeouts.count >= 3) {
announce("missing") announce("missing")
@@ -188,8 +187,7 @@ async function probeResult(info: Info, allowLegacy = false) {
const body = result.value.body const body = result.value.body
if (body !== undefined && "version" in body && "pid" in body) { if (body !== undefined && "version" in body && "pid" in body) {
if (body.pid !== info.pid) return { service: undefined, timedOut: false } if (body.pid !== info.pid) return { service: undefined, timedOut: false }
if (info.version !== undefined && body.version !== info.version) if (info.version !== undefined && body.version !== info.version) return { service: undefined, timedOut: false }
return { service: undefined, timedOut: false }
return { return {
service: { service: {
info, info,
+2 -8
View File
@@ -9,12 +9,7 @@ if (mode === "record-start") {
} }
if (mode === "signal") process.kill(process.pid, process.platform === "win32" ? "SIGTERM" : "SIGKILL") if (mode === "signal") process.kill(process.pid, process.platform === "win32" ? "SIGTERM" : "SIGKILL")
if ( if (mode === "delayed" || mode === "delayed-failed" || mode === "coordinated" || mode === "coordinated-failed-loser") {
mode === "delayed" ||
mode === "delayed-failed" ||
mode === "coordinated" ||
mode === "coordinated-failed-loser"
) {
await appendFile(registration + ".starts", process.pid + "\n") await appendFile(registration + ".starts", process.pid + "\n")
const owner = await writeFile(registration + ".owner", String(process.pid), { flag: "wx" }) const owner = await writeFile(registration + ".owner", String(process.pid), { flag: "wx" })
.then(() => true) .then(() => true)
@@ -59,8 +54,7 @@ const server = Bun.serve({
if (mode === "legacy") return Response.json({ healthy: true }) if (mode === "legacy") return Response.json({ healthy: true })
if (mode === "starting" && !(await Bun.file(registration + ".release").exists())) if (mode === "starting" && !(await Bun.file(registration + ".release").exists()))
return Response.json({ healthy: true, version, pid: process.pid }, { status: 503 }) return Response.json({ healthy: true, version, pid: process.pid }, { status: 503 })
if (mode === "failed-owner") if (mode === "failed-owner") return Response.json({ healthy: true, version, pid: process.pid }, { status: 500 })
return Response.json({ healthy: true, version, pid: process.pid }, { status: 500 })
if (mode === "starting" || mode === "graceful" || mode === "reject-stop") if (mode === "starting" || mode === "graceful" || mode === "reject-stop")
return Response.json({ healthy: true, version, pid: process.pid }) return Response.json({ healthy: true, version, pid: process.pid })
return Response.json({ healthy: true, version, pid: process.pid }) return Response.json({ healthy: true, version, pid: process.pid })
+5 -20
View File
@@ -93,16 +93,10 @@
}, },
"role": { "role": {
"type": "string", "type": "string",
"enum": [ "enum": ["admin", "member"]
"admin",
"member"
]
} }
}, },
"required": [ "required": ["name", "email"],
"name",
"email"
],
"additionalProperties": false "additionalProperties": false
} }
} }
@@ -143,9 +137,7 @@
"type": "integer" "type": "integer"
} }
}, },
"required": [ "required": ["query"],
"query"
],
"additionalProperties": false "additionalProperties": false
} }
}, },
@@ -216,17 +208,10 @@
}, },
"role": { "role": {
"type": "string", "type": "string",
"enum": [ "enum": ["admin", "member"]
"admin",
"member"
]
} }
}, },
"required": [ "required": ["id", "name", "email"],
"id",
"name",
"email"
],
"additionalProperties": false "additionalProperties": false
} }
}, },
File diff suppressed because it is too large Load Diff
@@ -258,7 +258,8 @@ add("test/built-ins/String/prototype/toUpperCase/supplementary_plane.js", "toUpp
), ),
]) ])
const whitespace = "\u0009\u000A\u000B\u000C\u000D\u0020\u00A0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF" const whitespace =
"\u0009\u000A\u000B\u000C\u000D\u0020\u00A0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF"
const lineTerminators = "\u000A\u000D\u2028\u2029" const lineTerminators = "\u000A\u000D\u2028\u2029"
const trim = (file: string, input: string, expected: string) => const trim = (file: string, input: string, expected: string) =>
add(`test/built-ins/String/prototype/trim/${file}`, "trim", [assertion("upstream assertion", input, expected)]) add(`test/built-ins/String/prototype/trim/${file}`, "trim", [assertion("upstream assertion", input, expected)])
@@ -334,18 +335,35 @@ add("test/built-ins/String/prototype/trim/u180e.js", "trim", [
assertion("leading U+180E", "\u180E_", "\u180E_"), assertion("leading U+180E", "\u180E_", "\u180E_"),
]) ])
const directionalWhitespace = "\x09\x0A\x0B\x0C\x0D\x20\xA0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000\u2028\u2029\uFEFF" const directionalWhitespace =
"\x09\x0A\x0B\x0C\x0D\x20\xA0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000\u2028\u2029\uFEFF"
add("test/built-ins/String/prototype/trimStart/this-value-whitespace.js", "trimStart", [ add("test/built-ins/String/prototype/trimStart/this-value-whitespace.js", "trimStart", [
assertion("all whitespace", directionalWhitespace + "a" + directionalWhitespace + "b" + directionalWhitespace, "a" + directionalWhitespace + "b" + directionalWhitespace), assertion(
"all whitespace",
directionalWhitespace + "a" + directionalWhitespace + "b" + directionalWhitespace,
"a" + directionalWhitespace + "b" + directionalWhitespace,
),
]) ])
add("test/built-ins/String/prototype/trimStart/this-value-line-terminator.js", "trimStart", [ add("test/built-ins/String/prototype/trimStart/this-value-line-terminator.js", "trimStart", [
assertion("all line terminators", lineTerminators + "a" + lineTerminators + "b" + lineTerminators, "a" + lineTerminators + "b" + lineTerminators), assertion(
"all line terminators",
lineTerminators + "a" + lineTerminators + "b" + lineTerminators,
"a" + lineTerminators + "b" + lineTerminators,
),
]) ])
add("test/built-ins/String/prototype/trimEnd/this-value-whitespace.js", "trimEnd", [ add("test/built-ins/String/prototype/trimEnd/this-value-whitespace.js", "trimEnd", [
assertion("all whitespace", directionalWhitespace + "a" + directionalWhitespace + "b" + directionalWhitespace, directionalWhitespace + "a" + directionalWhitespace + "b"), assertion(
"all whitespace",
directionalWhitespace + "a" + directionalWhitespace + "b" + directionalWhitespace,
directionalWhitespace + "a" + directionalWhitespace + "b",
),
]) ])
add("test/built-ins/String/prototype/trimEnd/this-value-line-terminator.js", "trimEnd", [ add("test/built-ins/String/prototype/trimEnd/this-value-line-terminator.js", "trimEnd", [
assertion("all line terminators", lineTerminators + "a" + lineTerminators + "b" + lineTerminators, lineTerminators + "a" + lineTerminators + "b"), assertion(
"all line terminators",
lineTerminators + "a" + lineTerminators + "b" + lineTerminators,
lineTerminators + "a" + lineTerminators + "b",
),
]) ])
add("test/built-ins/String/prototype/repeat/repeat-string-n-times.js", "repeat", [ add("test/built-ins/String/prototype/repeat/repeat-string-n-times.js", "repeat", [
assertion("repeat once", "abc", "abc", [1]), assertion("repeat once", "abc", "abc", [1]),
@@ -363,7 +381,9 @@ add("test/built-ins/String/prototype/repeat/count-is-zero-returns-empty-string.j
add("test/built-ins/String/prototype/repeat/count-coerced-to-zero-returns-empty-string.js", "repeat", [ add("test/built-ins/String/prototype/repeat/count-coerced-to-zero-returns-empty-string.js", "repeat", [
assertion("fraction truncates to zero", "abc", "", [0.9]), assertion("fraction truncates to zero", "abc", "", [0.9]),
]) ])
add("test/built-ins/String/prototype/padStart/fill-string-empty.js", "padStart", [assertion("empty fill", "abc", "abc", [5, ""])]) add("test/built-ins/String/prototype/padStart/fill-string-empty.js", "padStart", [
assertion("empty fill", "abc", "abc", [5, ""]),
])
add("test/built-ins/String/prototype/padStart/normal-operation.js", "padStart", [ add("test/built-ins/String/prototype/padStart/normal-operation.js", "padStart", [
assertion("truncated multi-character fill", "abc", "defdabc", [7, "def"]), assertion("truncated multi-character fill", "abc", "defdabc", [7, "def"]),
assertion("single-character fill", "abc", "**abc", [5, "*"]), assertion("single-character fill", "abc", "**abc", [5, "*"]),
@@ -381,7 +401,9 @@ add("test/built-ins/String/prototype/padStart/max-length-not-greater-than-string
assertion("equal length", "abc", "abc", [3, "def"]), assertion("equal length", "abc", "abc", [3, "def"]),
assertion("fraction truncates", "abc", "abc", [3.9999, "def"]), assertion("fraction truncates", "abc", "abc", [3.9999, "def"]),
]) ])
add("test/built-ins/String/prototype/padEnd/fill-string-empty.js", "padEnd", [assertion("empty fill", "abc", "abc", [5, ""])]) add("test/built-ins/String/prototype/padEnd/fill-string-empty.js", "padEnd", [
assertion("empty fill", "abc", "abc", [5, ""]),
])
add("test/built-ins/String/prototype/padEnd/normal-operation.js", "padEnd", [ add("test/built-ins/String/prototype/padEnd/normal-operation.js", "padEnd", [
assertion("truncated multi-character fill", "abc", "abcdefd", [7, "def"]), assertion("truncated multi-character fill", "abc", "abcdefd", [7, "def"]),
assertion("single-character fill", "abc", "abc**", [5, "*"]), assertion("single-character fill", "abc", "abc**", [5, "*"]),
@@ -401,11 +423,29 @@ add("test/built-ins/String/prototype/padEnd/max-length-not-greater-than-string.j
]) ])
add("test/built-ins/String/prototype/charAt/S15.5.4.4_A1_T4.js", "charAt", [assertion("omitted position", "lego", "l")]) add("test/built-ins/String/prototype/charAt/S15.5.4.4_A1_T4.js", "charAt", [assertion("omitted position", "lego", "l")])
add("test/built-ins/String/prototype/charAt/S15.5.4.4_A1_T7.js", "charAt", [assertion("undefined position", "lego", "l", [undefined])]) add("test/built-ins/String/prototype/charAt/S15.5.4.4_A1_T7.js", "charAt", [
add("test/built-ins/String/prototype/charAt/S15.5.4.4_A1_T8.js", "charAt", [assertion("undefined position", "42", "4", [undefined])]) assertion("undefined position", "lego", "l", [undefined]),
add("test/built-ins/String/prototype/charAt/S15.5.4.4_A4_T1.js", "charAt", ["A", "B", "C", "A", "B", "C"].map((expected, position) => assertion(`position ${position}`, "ABCABC", expected, [position]))) ])
add("test/built-ins/String/prototype/charAt/S15.5.4.4_A4_T2.js", "charAt", [-2, -1].map((position) => assertion(`position ${position}`, "ABCABC", "", [position]))) add("test/built-ins/String/prototype/charAt/S15.5.4.4_A1_T8.js", "charAt", [
add("test/built-ins/String/prototype/charAt/S15.5.4.4_A4_T3.js", "charAt", [6, 7].map((position) => assertion(`position ${position}`, "ABCABC", "", [position]))) assertion("undefined position", "42", "4", [undefined]),
])
add(
"test/built-ins/String/prototype/charAt/S15.5.4.4_A4_T1.js",
"charAt",
["A", "B", "C", "A", "B", "C"].map((expected, position) =>
assertion(`position ${position}`, "ABCABC", expected, [position]),
),
)
add(
"test/built-ins/String/prototype/charAt/S15.5.4.4_A4_T2.js",
"charAt",
[-2, -1].map((position) => assertion(`position ${position}`, "ABCABC", "", [position])),
)
add(
"test/built-ins/String/prototype/charAt/S15.5.4.4_A4_T3.js",
"charAt",
[6, 7].map((position) => assertion(`position ${position}`, "ABCABC", "", [position])),
)
add("test/built-ins/String/prototype/charAt/S9.4_A1.js", "charAt", [assertion("NaN position", "abc", "a", [NaN])]) add("test/built-ins/String/prototype/charAt/S9.4_A1.js", "charAt", [assertion("NaN position", "abc", "a", [NaN])])
add("test/built-ins/String/prototype/charAt/S9.4_A2.js", "charAt", [ add("test/built-ins/String/prototype/charAt/S9.4_A2.js", "charAt", [
assertion("positive zero", "abc", "a", [0]), assertion("positive zero", "abc", "a", [0]),
@@ -420,9 +460,15 @@ add("test/built-ins/String/prototype/charAt/pos-rounding.js", "charAt", [
assertion("1.99999", "abc", "b", [1.99999]), assertion("1.99999", "abc", "b", [1.99999]),
]) ])
add("test/built-ins/String/prototype/charCodeAt/S15.5.4.5_A1_T4.js", "charCodeAt", [assertion("omitted position", "smart", 0x73)]) add("test/built-ins/String/prototype/charCodeAt/S15.5.4.5_A1_T4.js", "charCodeAt", [
add("test/built-ins/String/prototype/charCodeAt/S15.5.4.5_A1_T7.js", "charCodeAt", [assertion("undefined position", "lego", 0x6c, [undefined])]) assertion("omitted position", "smart", 0x73),
add("test/built-ins/String/prototype/charCodeAt/S15.5.4.5_A1_T8.js", "charCodeAt", [assertion("undefined position", "42", 0x34, [undefined])]) ])
add("test/built-ins/String/prototype/charCodeAt/S15.5.4.5_A1_T7.js", "charCodeAt", [
assertion("undefined position", "lego", 0x6c, [undefined]),
])
add("test/built-ins/String/prototype/charCodeAt/S15.5.4.5_A1_T8.js", "charCodeAt", [
assertion("undefined position", "42", 0x34, [undefined]),
])
add("test/built-ins/String/prototype/charCodeAt/pos-rounding.js", "charCodeAt", [ add("test/built-ins/String/prototype/charCodeAt/pos-rounding.js", "charCodeAt", [
assertion("-0.99999", "abc", 0x61, [-0.99999]), assertion("-0.99999", "abc", 0x61, [-0.99999]),
assertion("-0.00001", "abc", 0x61, [-0.00001]), assertion("-0.00001", "abc", 0x61, [-0.00001]),
@@ -433,55 +479,91 @@ add("test/built-ins/String/prototype/charCodeAt/pos-rounding.js", "charCodeAt",
]) ])
add("test/built-ins/String/prototype/codePointAt/return-single-code-unit.js", "codePointAt", [ add("test/built-ins/String/prototype/codePointAt/return-single-code-unit.js", "codePointAt", [
assertion("a", "abc", 97, [0]), assertion("b", "abc", 98, [1]), assertion("c", "abc", 99, [2]), assertion("a", "abc", 97, [0]),
assertion("ordinary BMP", "\uAAAA\uBBBB", 0xaaaa, [0]), assertion("before high-surrogate range", "\uD7FF\uAAAA", 0xd7ff, [0]), assertion("b", "abc", 98, [1]),
assertion("low surrogate", "\uDC00\uAAAA", 0xdc00, [0]), assertion("trailing D800", "123\uD800", 0xd800, [3]), assertion("c", "abc", 99, [2]),
assertion("trailing DAAA", "123\uDAAA", 0xdaaa, [3]), assertion("trailing DBFF", "123\uDBFF", 0xdbff, [3]), assertion("ordinary BMP", "\uAAAA\uBBBB", 0xaaaa, [0]),
assertion("before high-surrogate range", "\uD7FF\uAAAA", 0xd7ff, [0]),
assertion("low surrogate", "\uDC00\uAAAA", 0xdc00, [0]),
assertion("trailing D800", "123\uD800", 0xd800, [3]),
assertion("trailing DAAA", "123\uDAAA", 0xdaaa, [3]),
assertion("trailing DBFF", "123\uDBFF", 0xdbff, [3]),
]) ])
add("test/built-ins/String/prototype/codePointAt/return-first-code-unit.js", "codePointAt", [ add("test/built-ins/String/prototype/codePointAt/return-first-code-unit.js", "codePointAt", [
assertion("D800 before DBFF", "\uD800\uDBFF", 0xd800, [0]), assertion("D800 before E000", "\uD800\uE000", 0xd800, [0]), assertion("D800 before DBFF", "\uD800\uDBFF", 0xd800, [0]),
assertion("DAAA before DBFF", "\uDAAA\uDBFF", 0xdaaa, [0]), assertion("DAAA before E000", "\uDAAA\uE000", 0xdaaa, [0]), assertion("D800 before E000", "\uD800\uE000", 0xd800, [0]),
assertion("DBFF before DBFF", "\uDBFF\uDBFF", 0xdbff, [0]), assertion("DBFF before E000", "\uDBFF\uE000", 0xdbff, [0]), assertion("DAAA before DBFF", "\uDAAA\uDBFF", 0xdaaa, [0]),
assertion("D800 before NUL", "\uD800\u0000", 0xd800, [0]), assertion("D800 before FFFF", "\uD800\uFFFF", 0xd800, [0]), assertion("DAAA before E000", "\uDAAA\uE000", 0xdaaa, [0]),
assertion("DAAA before NUL", "\uDAAA\u0000", 0xdaaa, [0]), assertion("DAAA before FFFF", "\uDAAA\uFFFF", 0xdaaa, [0]), assertion("DBFF before DBFF", "\uDBFF\uDBFF", 0xdbff, [0]),
assertion("DBFF before E000", "\uDBFF\uE000", 0xdbff, [0]),
assertion("D800 before NUL", "\uD800\u0000", 0xd800, [0]),
assertion("D800 before FFFF", "\uD800\uFFFF", 0xd800, [0]),
assertion("DAAA before NUL", "\uDAAA\u0000", 0xdaaa, [0]),
assertion("DAAA before FFFF", "\uDAAA\uFFFF", 0xdaaa, [0]),
assertion("DBFF before FFFF", "\uDBFF\uFFFF", 0xdbff, [0]), assertion("DBFF before FFFF", "\uDBFF\uFFFF", 0xdbff, [0]),
]) ])
add("test/built-ins/String/prototype/codePointAt/return-utf16-decode.js", "codePointAt", [ add("test/built-ins/String/prototype/codePointAt/return-utf16-decode.js", "codePointAt", [
assertion("U+10000", "\uD800\uDC00", 65536, [0]), assertion("U+101D0", "\uD800\uDDD0", 66000, [0]), assertion("U+10000", "\uD800\uDC00", 65536, [0]),
assertion("U+103FF", "\uD800\uDFFF", 66559, [0]), assertion("U+BA800", "\uDAAA\uDC00", 763904, [0]), assertion("U+101D0", "\uD800\uDDD0", 66000, [0]),
assertion("U+BA9D0", "\uDAAA\uDDD0", 764368, [0]), assertion("U+BABFF", "\uDAAA\uDFFF", 764927, [0]), assertion("U+103FF", "\uD800\uDFFF", 66559, [0]),
assertion("U+10FC00", "\uDBFF\uDC00", 1113088, [0]), assertion("U+10FDD0", "\uDBFF\uDDD0", 1113552, [0]), assertion("U+BA800", "\uDAAA\uDC00", 763904, [0]),
assertion("U+BA9D0", "\uDAAA\uDDD0", 764368, [0]),
assertion("U+BABFF", "\uDAAA\uDFFF", 764927, [0]),
assertion("U+10FC00", "\uDBFF\uDC00", 1113088, [0]),
assertion("U+10FDD0", "\uDBFF\uDDD0", 1113552, [0]),
assertion("U+10FFFF", "\uDBFF\uDFFF", 1114111, [0]), assertion("U+10FFFF", "\uDBFF\uDFFF", 1114111, [0]),
]) ])
add("test/built-ins/String/prototype/codePointAt/return-code-unit-coerced-position.js", "codePointAt", [ add("test/built-ins/String/prototype/codePointAt/return-code-unit-coerced-position.js", "codePointAt", [
assertion("NaN", "\uD800\uDC00", 65536, [NaN]), assertion("undefined", "\uD800\uDC00", 65536, [undefined]), assertion("NaN", "\uD800\uDC00", 65536, [NaN]),
assertion("undefined", "\uD800\uDC00", 65536, [undefined]),
]) ])
add("test/built-ins/String/prototype/codePointAt/returns-undefined-on-position-less-than-zero.js", "codePointAt", [ add("test/built-ins/String/prototype/codePointAt/returns-undefined-on-position-less-than-zero.js", "codePointAt", [
{ label: "negative one", input: "abc", args: [-1], outcome: "undefined" }, { label: "negative one", input: "abc", args: [-1], outcome: "undefined" },
{ label: "negative infinity", input: "abc", args: [-Infinity], outcome: "undefined" }, { label: "negative infinity", input: "abc", args: [-Infinity], outcome: "undefined" },
]) ])
add("test/built-ins/String/prototype/codePointAt/returns-undefined-on-position-equal-or-more-than-size.js", "codePointAt", [ add(
{ label: "equal to size", input: "abc", args: [3], outcome: "undefined" }, "test/built-ins/String/prototype/codePointAt/returns-undefined-on-position-equal-or-more-than-size.js",
{ label: "greater than size", input: "abc", args: [4], outcome: "undefined" }, "codePointAt",
{ label: "positive infinity", input: "abc", args: [Infinity], outcome: "undefined" }, [
]) { label: "equal to size", input: "abc", args: [3], outcome: "undefined" },
{ label: "greater than size", input: "abc", args: [4], outcome: "undefined" },
{ label: "positive infinity", input: "abc", args: [Infinity], outcome: "undefined" },
],
)
add("test/built-ins/String/prototype/at/returns-code-unit.js", "at", [ add("test/built-ins/String/prototype/at/returns-code-unit.js", "at", [
assertion("position 0", "12\uD80034", "1", [0]), assertion("position 1", "12\uD80034", "2", [1]), assertion("position 0", "12\uD80034", "1", [0]),
assertion("unpaired surrogate", "12\uD80034", "\uD800", [2]), assertion("position 3", "12\uD80034", "3", [3]), assertion("position 1", "12\uD80034", "2", [1]),
assertion("unpaired surrogate", "12\uD80034", "\uD800", [2]),
assertion("position 3", "12\uD80034", "3", [3]),
assertion("position 4", "12\uD80034", "4", [4]), assertion("position 4", "12\uD80034", "4", [4]),
]) ])
add("test/built-ins/String/prototype/at/returns-item.js", "at", ["1", "2", "3", "4", "5"].map((expected, position) => assertion(`position ${position}`, "12345", expected, [position]))) add(
"test/built-ins/String/prototype/at/returns-item.js",
"at",
["1", "2", "3", "4", "5"].map((expected, position) =>
assertion(`position ${position}`, "12345", expected, [position]),
),
)
add("test/built-ins/String/prototype/at/returns-item-relative-index.js", "at", [ add("test/built-ins/String/prototype/at/returns-item-relative-index.js", "at", [
assertion("zero", "12345", "1", [0]), assertion("negative one", "12345", "5", [-1]), assertion("zero", "12345", "1", [0]),
assertion("negative three", "12345", "3", [-3]), assertion("negative four", "12345", "2", [-4]), assertion("negative one", "12345", "5", [-1]),
assertion("negative three", "12345", "3", [-3]),
assertion("negative four", "12345", "2", [-4]),
])
add(
"test/built-ins/String/prototype/at/returns-undefined-for-out-of-range-index.js",
"at",
[-2, 0, 1].map((position) => ({ label: `position ${position}`, input: "", args: [position], outcome: "undefined" })),
)
add("test/built-ins/String/prototype/at/index-non-numeric-argument-tointeger.js", "at", [
assertion("undefined", "01", "0", [undefined]),
]) ])
add("test/built-ins/String/prototype/at/returns-undefined-for-out-of-range-index.js", "at", [-2, 0, 1].map((position) => ({ label: `position ${position}`, input: "", args: [position], outcome: "undefined" })))
add("test/built-ins/String/prototype/at/index-non-numeric-argument-tointeger.js", "at", [assertion("undefined", "01", "0", [undefined])])
add("test/built-ins/String/prototype/concat/S15.5.4.6_A1_T4.js", "concat", [assertion("no arguments", "lego", "lego")]) add("test/built-ins/String/prototype/concat/S15.5.4.6_A1_T4.js", "concat", [assertion("no arguments", "lego", "lego")])
add("test/built-ins/String/prototype/toString/string-primitive.js", "toString", [ add("test/built-ins/String/prototype/toString/string-primitive.js", "toString", [
assertion("empty string", "", ""), assertion("non-empty string", "str", "str"), assertion("empty string", "", ""),
assertion("non-empty string", "str", "str"),
]) ])
add("test/built-ins/String/prototype/normalize/return-normalized-string.js", "normalize", [ add("test/built-ins/String/prototype/normalize/return-normalized-string.js", "normalize", [
@@ -492,11 +574,15 @@ add("test/built-ins/String/prototype/normalize/return-normalized-string.js", "no
assertion("NFC long", "\u00C5\u2ADC\u0958\u2126\u0344", "\xC5\u2ADD\u0338\u0915\u093C\u03A9\u0308\u0301", ["NFC"]), assertion("NFC long", "\u00C5\u2ADC\u0958\u2126\u0344", "\xC5\u2ADD\u0338\u0915\u093C\u03A9\u0308\u0301", ["NFC"]),
assertion("NFD long", "\u00C5\u2ADC\u0958\u2126\u0344", "A\u030A\u2ADD\u0338\u0915\u093C\u03A9\u0308\u0301", ["NFD"]), assertion("NFD long", "\u00C5\u2ADC\u0958\u2126\u0344", "A\u030A\u2ADD\u0338\u0915\u093C\u03A9\u0308\u0301", ["NFD"]),
assertion("NFKC long", "\u00C5\u2ADC\u0958\u2126\u0344", "\xC5\u2ADD\u0338\u0915\u093C\u03A9\u0308\u0301", ["NFKC"]), assertion("NFKC long", "\u00C5\u2ADC\u0958\u2126\u0344", "\xC5\u2ADD\u0338\u0915\u093C\u03A9\u0308\u0301", ["NFKC"]),
assertion("NFKD long", "\u00C5\u2ADC\u0958\u2126\u0344", "A\u030A\u2ADD\u0338\u0915\u093C\u03A9\u0308\u0301", ["NFKD"]), assertion("NFKD long", "\u00C5\u2ADC\u0958\u2126\u0344", "A\u030A\u2ADD\u0338\u0915\u093C\u03A9\u0308\u0301", [
"NFKD",
]),
]) ])
add("test/built-ins/String/prototype/normalize/return-normalized-string-using-default-parameter.js", "normalize", [ add("test/built-ins/String/prototype/normalize/return-normalized-string-using-default-parameter.js", "normalize", [
assertion("omitted", "\u00C5\u2ADC\u0958\u2126\u0344", "\xC5\u2ADD\u0338\u0915\u093C\u03A9\u0308\u0301"), assertion("omitted", "\u00C5\u2ADC\u0958\u2126\u0344", "\xC5\u2ADD\u0338\u0915\u093C\u03A9\u0308\u0301"),
assertion("undefined", "\u00C5\u2ADC\u0958\u2126\u0344", "\xC5\u2ADD\u0338\u0915\u093C\u03A9\u0308\u0301", [undefined]), assertion("undefined", "\u00C5\u2ADC\u0958\u2126\u0344", "\xC5\u2ADD\u0338\u0915\u093C\u03A9\u0308\u0301", [
undefined,
]),
]) ])
add("test/built-ins/String/prototype/normalize/form-is-not-valid-throws.js", "normalize", [ add("test/built-ins/String/prototype/normalize/form-is-not-valid-throws.js", "normalize", [
{ label: "bar", input: "foo", args: ["bar"], outcome: "RangeError" }, { label: "bar", input: "foo", args: ["bar"], outcome: "RangeError" },
@@ -504,51 +590,141 @@ add("test/built-ins/String/prototype/normalize/form-is-not-valid-throws.js", "no
]) ])
add("test/built-ins/String/prototype/localeCompare/15.5.4.9_CE.js", "localeCompare", [ add("test/built-ins/String/prototype/localeCompare/15.5.4.9_CE.js", "localeCompare", [
assertion("D70", "o\u0308", 0, ["ö"]), assertion("reordered diaeresis", "ä\u0323", 0, ["a\u0323\u0308"]), assertion("D70", "o\u0308", 0, ["ö"]),
assertion("reordered marks", "a\u0308\u0323", 0, ["a\u0323\u0308"]), assertion("precomposed dot below", "ạ\u0308", 0, ["a\u0323\u0308"]), assertion("reordered diaeresis", "ä\u0323", 0, ["a\u0323\u0308"]),
assertion("breve after diaeresis", "ä\u0306", 0, ["a\u0308\u0306"]), assertion("diaeresis after breve", "ă\u0308", 0, ["a\u0306\u0308"]), assertion("reordered marks", "a\u0308\u0323", 0, ["a\u0323\u0308"]),
assertion("Hangul", "\u1111\u1171\u11B6", 0, ["퓛"]), assertion("angstrom compatibility", "", 0, ["Å"]), assertion("precomposed dot below", "ạ\u0308", 0, ["a\u0323\u0308"]),
assertion("angstrom decomposed", "Å", 0, ["A\u030A"]), assertion("reordered horn and dot", "x\u031B\u0323", 0, ["x\u0323\u031B"]), assertion("breve after diaeresis", "ä\u0306", 0, ["a\u0308\u0306"]),
assertion("Vietnamese precomposed 1", "ự", 0, ["ụ\u031B"]), assertion("Vietnamese decomposed", "ự", 0, ["u\u031B\u0323"]), assertion("diaeresis after breve", "ă\u0308", 0, ["a\u0306\u0308"]),
assertion("Vietnamese precomposed 2", "ự", 0, ["ư\u0323"]), assertion("Vietnamese reordered", "ự", 0, ["u\u0323\u031B"]), assertion("Hangul", "\u1111\u1171\u11B6", 0, [""]),
assertion("cedilla", "Ç", 0, ["C\u0327"]), assertion("q reordered", "q\u0307\u0323", 0, ["q\u0323\u0307"]), assertion("angstrom compatibility", "Å", 0, ["Å"]),
assertion("Hangul syllable", "", 0, ["\u1100\u1161"]), assertion("ohm", "Ω", 0, ["]), assertion("angstrom decomposed", "", 0, ["A\u030A"]),
assertion("angstrom", "Å", 0, ["A\u030A"]), assertion("circumflex", ", 0, ["o\u0302"]), assertion("reordered horn and dot", "x\u031B\u0323", 0, ["x\u0323\u031B"]),
assertion("s with marks", "ṩ", 0, ["s\u0323\u0307"]), assertion("d composed plus dot", "ḋ\u0323", 0, ["d\u0323\u0307"]), assertion("Vietnamese precomposed 1", "", 0, ["\u031B"]),
assertion("Vietnamese decomposed", "ự", 0, ["u\u031B\u0323"]),
assertion("Vietnamese precomposed 2", "ự", 0, ["ư\u0323"]),
assertion("Vietnamese reordered", "ự", 0, ["u\u0323\u031B"]),
assertion("cedilla", "Ç", 0, ["C\u0327"]),
assertion("q reordered", "q\u0307\u0323", 0, ["q\u0323\u0307"]),
assertion("Hangul syllable", "가", 0, ["\u1100\u1161"]),
assertion("ohm", "Ω", 0, ["Ω"]),
assertion("angstrom", "Å", 0, ["A\u030A"]),
assertion("circumflex", "ô", 0, ["o\u0302"]),
assertion("s with marks", "ṩ", 0, ["s\u0323\u0307"]),
assertion("d composed plus dot", "ḋ\u0323", 0, ["d\u0323\u0307"]),
assertion("d two precompositions", "ḋ\u0323", 0, ["ḍ\u0307"]), assertion("d two precompositions", "ḋ\u0323", 0, ["ḍ\u0307"]),
]) ])
add("test/built-ins/String/fromCharCode/S15.5.3.2_A2.js", "fromCharCode", [{ label: "no arguments", expected: "" }], true) add(
add("test/built-ins/String/fromCharCode/S15.5.3.2_A3_T1.js", "fromCharCode", [{ label: "ABBA", args: [65, 66, 66, 65], expected: "ABBA" }], true) "test/built-ins/String/fromCharCode/S15.5.3.2_A2.js",
add("test/built-ins/String/fromCharCode/S9.7_A1.js", "fromCharCode", [ "fromCharCode",
{ label: "NaN", args: [NaN], expected: 0 }, { label: "zero", args: [0], expected: 0 }, { label: "negative zero", args: [-0], expected: 0 }, [{ label: "no arguments", expected: "" }],
{ label: "positive infinity", args: [Infinity], expected: 0 }, { label: "negative infinity", args: [-Infinity], expected: 0 }, true,
], true) )
add("test/built-ins/String/fromCharCode/S9.7_A2.1.js", "fromCharCode", [ add(
[0, 0], [1, 1], [-1, 65535], [65535, 65535], [65534, 65534], [65536, 0], [4294967295, 65535], [4294967294, 65534], [4294967296, 0], "test/built-ins/String/fromCharCode/S15.5.3.2_A3_T1.js",
].map(([input, expected]) => ({ label: String(input), args: [input!], expected })), true) "fromCharCode",
add("test/built-ins/String/fromCharCode/S9.7_A2.2.js", "fromCharCode", [ [{ label: "ABBA", args: [65, 66, 66, 65], expected: "ABBA" }],
[-32767, 32769], [-32768, 32768], [-32769, 32767], [-65535, 1], [-65536, 0], [-65537, 65535], [65535, 65535], [65536, 0], [65537, 1], [131071, 65535], [131072, 0], [131073, 1], true,
].map(([input, expected]) => ({ label: String(input), args: [input!], expected })), true) )
add("test/built-ins/String/fromCharCode/S9.7_A3.2_T1.js", "fromCharCode", [ add(
{ label: "positive fraction", args: [1.2345], expected: 1 }, { label: "negative fraction", args: [-5.4321], expected: 65531 }, "test/built-ins/String/fromCharCode/S9.7_A1.js",
], true) "fromCharCode",
[
{ label: "NaN", args: [NaN], expected: 0 },
{ label: "zero", args: [0], expected: 0 },
{ label: "negative zero", args: [-0], expected: 0 },
{ label: "positive infinity", args: [Infinity], expected: 0 },
{ label: "negative infinity", args: [-Infinity], expected: 0 },
],
true,
)
add(
"test/built-ins/String/fromCharCode/S9.7_A2.1.js",
"fromCharCode",
[
[0, 0],
[1, 1],
[-1, 65535],
[65535, 65535],
[65534, 65534],
[65536, 0],
[4294967295, 65535],
[4294967294, 65534],
[4294967296, 0],
].map(([input, expected]) => ({ label: String(input), args: [input!], expected })),
true,
)
add(
"test/built-ins/String/fromCharCode/S9.7_A2.2.js",
"fromCharCode",
[
[-32767, 32769],
[-32768, 32768],
[-32769, 32767],
[-65535, 1],
[-65536, 0],
[-65537, 65535],
[65535, 65535],
[65536, 0],
[65537, 1],
[131071, 65535],
[131072, 0],
[131073, 1],
].map(([input, expected]) => ({ label: String(input), args: [input!], expected })),
true,
)
add(
"test/built-ins/String/fromCharCode/S9.7_A3.2_T1.js",
"fromCharCode",
[
{ label: "positive fraction", args: [1.2345], expected: 1 },
{ label: "negative fraction", args: [-5.4321], expected: 65531 },
],
true,
)
add("test/built-ins/String/fromCodePoint/arguments-is-empty.js", "fromCodePoint", [{ label: "no arguments", expected: "" }], true) add(
add("test/built-ins/String/fromCodePoint/return-string-value.js", "fromCodePoint", [ "test/built-ins/String/fromCodePoint/arguments-is-empty.js",
{ label: "NUL", args: [0], expected: "\x00" }, { label: "asterisk", args: [42], expected: "*" }, "fromCodePoint",
{ label: "AZ", args: [65, 90], expected: "AZ" }, { label: "Cyrillic", args: [0x404], expected: "\u0404" }, [{ label: "no arguments", expected: "" }],
{ label: "hex supplementary", args: [0x2f804], expected: "\uD87E\uDC04" }, { label: "decimal supplementary", args: [194564], expected: "\uD87E\uDC04" }, true,
{ label: "mixed supplementary", args: [0x1d306, 0x61, 0x1d307], expected: "\uD834\uDF06a\uD834\uDF07" }, )
{ label: "maximum code point", args: [1114111], expected: "\uDBFF\uDFFF" }, add(
], true) "test/built-ins/String/fromCodePoint/return-string-value.js",
add("test/built-ins/String/fromCodePoint/argument-is-not-integer.js", "fromCodePoint", [ "fromCodePoint",
{ label: "fraction", args: [3.14], outcome: "RangeError" }, { label: "fraction after valid", args: [42, 3.14], outcome: "RangeError" }, [
], true) { label: "NUL", args: [0], expected: "\x00" },
add("test/built-ins/String/fromCodePoint/number-is-out-of-range.js", "fromCodePoint", [ { label: "asterisk", args: [42], expected: "*" },
{ label: "negative one", args: [-1], outcome: "RangeError" }, { label: "negative after valid", args: [1, -1], outcome: "RangeError" }, { label: "AZ", args: [65, 90], expected: "AZ" },
{ label: "above maximum", args: [1114112], outcome: "RangeError" }, { label: "infinity", args: [Infinity], outcome: "RangeError" }, { label: "Cyrillic", args: [0x404], expected: "\u0404" },
], true) { label: "hex supplementary", args: [0x2f804], expected: "\uD87E\uDC04" },
{ label: "decimal supplementary", args: [194564], expected: "\uD87E\uDC04" },
{ label: "mixed supplementary", args: [0x1d306, 0x61, 0x1d307], expected: "\uD834\uDF06a\uD834\uDF07" },
{ label: "maximum code point", args: [1114111], expected: "\uDBFF\uDFFF" },
],
true,
)
add(
"test/built-ins/String/fromCodePoint/argument-is-not-integer.js",
"fromCodePoint",
[
{ label: "fraction", args: [3.14], outcome: "RangeError" },
{ label: "fraction after valid", args: [42, 3.14], outcome: "RangeError" },
],
true,
)
add(
"test/built-ins/String/fromCodePoint/number-is-out-of-range.js",
"fromCodePoint",
[
{ label: "negative one", args: [-1], outcome: "RangeError" },
{ label: "negative after valid", args: [1, -1], outcome: "RangeError" },
{ label: "above maximum", args: [1114112], outcome: "RangeError" },
{ label: "infinity", args: [Infinity], outcome: "RangeError" },
],
true,
)
describe("Test262-adapted core String behavior", () => { describe("Test262-adapted core String behavior", () => {
for (const vector of vectors) { for (const vector of vectors) {
@@ -558,16 +734,18 @@ describe("Test262-adapted core String behavior", () => {
const expression = vector.static const expression = vector.static
? `String.${vector.method}(${args})` ? `String.${vector.method}(${args})`
: `${JSON.stringify(item.input)}.${vector.method}(${args})` : `${JSON.stringify(item.input)}.${vector.method}(${args})`
const observed = vector.static && vector.method === "fromCharCode" && typeof item.expected === "number" const observed =
? `${expression}.charCodeAt(0)` vector.static && vector.method === "fromCharCode" && typeof item.expected === "number"
: expression ? `${expression}.charCodeAt(0)`
const checked = item.outcome === "undefined" : expression
? `${observed} === undefined` const checked =
: item.outcome === "length" item.outcome === "undefined"
? `${observed}.length` ? `${observed} === undefined`
: item.outcome === "RangeError" : item.outcome === "length"
? `(() => { try { ${observed}; return false } catch (error) { return error instanceof RangeError } })()` ? `${observed}.length`
: observed : item.outcome === "RangeError"
? `(() => { try { ${observed}; return false } catch (error) { return error instanceof RangeError } })()`
: observed
return `{ label: ${JSON.stringify(item.label)}, value: ${checked} }` return `{ label: ${JSON.stringify(item.label)}, value: ${checked} }`
}) })
const expected = vector.assertions.map((item) => ({ const expected = vector.assertions.map((item) => ({
@@ -121,9 +121,35 @@ run("Test262-adapted regexp split behavior", [
] ]
`, `,
expected: [ expected: [
["x"], ["x"], ["", ""], ["", ""], ["", ""], ["x"], ["", ""], ["", ""], ["", ""], ["x"],
["x"], ["", ""], ["x"], ["x"], ["x"], ["x"], ["", ""], ["x"], ["x"], ["x"], ["x"],
["x"], ["x"], ["", ""], ["x"], ["x"], ["x"], ["", ""], ["x"], ["", ""], ["x"], ["", ""],
["", ""],
["", ""],
["x"],
["", ""],
["", ""],
["", ""],
["x"],
["", ""],
["x"],
["x"],
["x"],
["x"],
["", ""],
["x"],
["x"],
["x"],
["x"],
["x"],
["", ""],
["x"],
["x"],
["x"],
["", ""],
["x"],
["", ""],
["x"],
], ],
}, },
{ {
@@ -253,19 +279,71 @@ run("Test262-adapted replace behavior", [
return replacements.flatMap((replacement) => patterns.map((pattern) => str.replace(pattern, replacement))) return replacements.flatMap((replacement) => patterns.map((pattern) => str.replace(pattern, replacement)))
`, `,
expected: [ expected: [
"foo-|$0|-bar", "foo-|$0|-bar", "foo-|$0|-bar", "foo-|$0|-bar", "foo-|$0|-bar", "foo-|$0|-bar",
"foo-|$00|-bar", "foo-|$00|-bar", "foo-|$00|-bar", "foo-|$00|-bar", "foo-|$00|-bar", "foo-|$0|-bar",
"foo-|$000|-bar", "foo-|$000|-bar", "foo-|$000|-bar", "foo-|$000|-bar", "foo-|$000|-bar", "foo-|$0|-bar",
"foo-|$1|-bar", "foo-|$1|-bar", "foo-|x|-bar", "foo-|x|-bar", "foo-|x|-bar", "foo-|$0|-bar",
"foo-|$01|-bar", "foo-|$01|-bar", "foo-|x|-bar", "foo-|x|-bar", "foo-|x|-bar", "foo-|$0|-bar",
"foo-|$010|-bar", "foo-|$010|-bar", "foo-|x0|-bar", "foo-|x0|-bar", "foo-|x0|-bar", "foo-|$00|-bar",
"foo-|$2|-bar", "foo-|$2|-bar", "foo-|$2|-bar", "foo-||-bar", "foo-|x|-bar", "foo-|$00|-bar",
"foo-|$02|-bar", "foo-|$02|-bar", "foo-|$02|-bar", "foo-||-bar", "foo-|x|-bar", "foo-|$00|-bar",
"foo-|$020|-bar", "foo-|$020|-bar", "foo-|$020|-bar", "foo-|0|-bar", "foo-|x0|-bar", "foo-|$00|-bar",
"foo-|$10|-bar", "foo-|$10|-bar", "foo-|x0|-bar", "foo-|x0|-bar", "foo-|x|-bar", "foo-|$00|-bar",
"foo-|$100|-bar", "foo-|$100|-bar", "foo-|x00|-bar", "foo-|x00|-bar", "foo-|x0|-bar", "foo-|$000|-bar",
"foo-|$20|-bar", "foo-|$20|-bar", "foo-|$20|-bar", "foo-|0|-bar", "foo-|x0|-bar", "foo-|$000|-bar",
"foo-|$200|-bar", "foo-|$200|-bar", "foo-|$200|-bar", "foo-|00|-bar", "foo-|x00|-bar", "foo-|$000|-bar",
"foo-|$000|-bar",
"foo-|$000|-bar",
"foo-|$1|-bar",
"foo-|$1|-bar",
"foo-|x|-bar",
"foo-|x|-bar",
"foo-|x|-bar",
"foo-|$01|-bar",
"foo-|$01|-bar",
"foo-|x|-bar",
"foo-|x|-bar",
"foo-|x|-bar",
"foo-|$010|-bar",
"foo-|$010|-bar",
"foo-|x0|-bar",
"foo-|x0|-bar",
"foo-|x0|-bar",
"foo-|$2|-bar",
"foo-|$2|-bar",
"foo-|$2|-bar",
"foo-||-bar",
"foo-|x|-bar",
"foo-|$02|-bar",
"foo-|$02|-bar",
"foo-|$02|-bar",
"foo-||-bar",
"foo-|x|-bar",
"foo-|$020|-bar",
"foo-|$020|-bar",
"foo-|$020|-bar",
"foo-|0|-bar",
"foo-|x0|-bar",
"foo-|$10|-bar",
"foo-|$10|-bar",
"foo-|x0|-bar",
"foo-|x0|-bar",
"foo-|x|-bar",
"foo-|$100|-bar",
"foo-|$100|-bar",
"foo-|x00|-bar",
"foo-|x00|-bar",
"foo-|x0|-bar",
"foo-|$20|-bar",
"foo-|$20|-bar",
"foo-|$20|-bar",
"foo-|0|-bar",
"foo-|x0|-bar",
"foo-|$200|-bar",
"foo-|$200|-bar",
"foo-|$200|-bar",
"foo-|00|-bar",
"foo-|x00|-bar",
], ],
}, },
{ {
@@ -377,12 +455,30 @@ run("Test262-adapted replaceAll behavior", [
] ]
`, `,
expected: [ expected: [
"azc azc azc", "abc abc abc", "abc abc abc", "o ppercase!", "o Uppercase?", " UPPERCASE!", "azc azc azc",
"ca-bbcca-bbc", "abb$3cabb$3cabb$3cabb$3c", "abc abc abc",
"abc abc abc",
"o ppercase!",
"o Uppercase?",
" UPPERCASE!",
"ca-bbcca-bbc",
"abb$3cabb$3cabb$3cabb$3c",
"(aabaca)-(aaba)-(aabacadaea)f(agahai)-(agah)-(agahaiajak)(alaman)-(alam)-(alamano a )azaya", "(aabaca)-(aaba)-(aabacadaea)f(agahai)-(agah)-(agahaiajak)(alaman)-(alam)-(alamano a )azaya",
"acbacaa", "aacabca", "a(b)c(b)a", "a($<named)c($<named)a", "a()c()a", "($)bc($)bc", "acbacaa",
"($)bc($)bc", "($$)bc($$)bc", "($$)bc($$)bc", "($&)bc($&)bc", "($1)bc($1)bc", "aacabca",
"($`)bc($`)bc", "($')bc($')bc", "($<z>)bc($<z>)bc", "(abca)bc(abca)bc", "a(b)c(b)a",
"a($<named)c($<named)a",
"a()c()a",
"($)bc($)bc",
"($)bc($)bc",
"($$)bc($$)bc",
"($$)bc($$)bc",
"($&)bc($&)bc",
"($1)bc($1)bc",
"($`)bc($`)bc",
"($')bc($')bc",
"($<z>)bc($<z>)bc",
"(abca)bc(abca)bc",
], ],
}, },
{ {
@@ -472,7 +568,17 @@ run("Test262-adapted replaceAll behavior", [
const str = "ABC AAA ABC AAA" const str = "ABC AAA ABC AAA"
return ["$1", "$2", "$3", "$4", "$5", "$6", "$7", "$8", "$9"].map((replacement) => str.replaceAll("ABC", replacement)) return ["$1", "$2", "$3", "$4", "$5", "$6", "$7", "$8", "$9"].map((replacement) => str.replaceAll("ABC", replacement))
`, `,
expected: ["$1 AAA $1 AAA", "$2 AAA $2 AAA", "$3 AAA $3 AAA", "$4 AAA $4 AAA", "$5 AAA $5 AAA", "$6 AAA $6 AAA", "$7 AAA $7 AAA", "$8 AAA $8 AAA", "$9 AAA $9 AAA"], expected: [
"$1 AAA $1 AAA",
"$2 AAA $2 AAA",
"$3 AAA $3 AAA",
"$4 AAA $4 AAA",
"$5 AAA $5 AAA",
"$6 AAA $6 AAA",
"$7 AAA $7 AAA",
"$8 AAA $8 AAA",
"$9 AAA $9 AAA",
],
}, },
{ {
path: "test/built-ins/String/prototype/replaceAll/getSubstitution-0x0024NN.js", path: "test/built-ins/String/prototype/replaceAll/getSubstitution-0x0024NN.js",
@@ -248,8 +248,12 @@ const cases = [
code: `const result = "one two three four five".split(" "); return [result.length, ...result]`, code: `const result = "one two three four five".split(" "); return [result.length, ...result]`,
expected: [5, "one", "two", "three", "four", "five"], expected: [5, "one", "two", "three", "four", "five"],
labels: [ labels: [
"The value of __split.length is 5", 'The value of __split[0] is "one"', 'The value of __split[1] is "two"', "The value of __split.length is 5",
'The value of __split[2] is "three"', 'The value of __split[3] is "four"', 'The value of __split[4] is "five"', 'The value of __split[0] is "one"',
'The value of __split[1] is "two"',
'The value of __split[2] is "three"',
'The value of __split[3] is "four"',
'The value of __split[4] is "five"',
], ],
}, },
{ {
@@ -257,8 +261,10 @@ const cases = [
code: `const result = "one two three".split(""); return [result[0], result[1], result[11], result[12]]`, code: `const result = "one two three".split(""); return [result[0], result[1], result[11], result[12]]`,
expected: ["o", "n", "e", "e"], expected: ["o", "n", "e", "e"],
labels: [ labels: [
'The value of __split[0] is "o"', 'The value of __split[1] is "n"', 'The value of __split[0] is "o"',
'The value of __split[11] is "e"', 'The value of __split[12] is "e"', 'The value of __split[1] is "n"',
'The value of __split[11] is "e"',
'The value of __split[12] is "e"',
], ],
}, },
{ {
@@ -314,7 +320,11 @@ const cases = [
path: "test/built-ins/String/prototype/split/separator-undef.js", path: "test/built-ins/String/prototype/split/separator-undef.js",
code: `const result = "undefined is not a function".split(); return [Array.isArray(result), result.length, result[0]]`, code: `const result = "undefined is not a function".split(); return [Array.isArray(result), result.length, result[0]]`,
expected: [true, 1, "undefined is not a function"], expected: [true, 1, "undefined is not a function"],
labels: ["implicit separator, result is array", "implicit separator, result.length", "implicit separator, [0] is the same string"], labels: [
"implicit separator, result is array",
"implicit separator, result.length",
"implicit separator, [0] is the same string",
],
}, },
{ {
path: "test/built-ins/String/prototype/slice/S15.5.4.13_A1_T6.js", path: "test/built-ins/String/prototype/slice/S15.5.4.13_A1_T6.js",
@@ -456,23 +466,33 @@ const cases = [
}, },
{ {
path: "test/built-ins/String/prototype/includes/String.prototype.includes_FailMissingLetter.js", path: "test/built-ins/String/prototype/includes/String.prototype.includes_FailMissingLetter.js",
code: `return ["word".includes("a", 0)]`, expected: [false], labels: ['"word".includes("a", 0)'], code: `return ["word".includes("a", 0)]`,
expected: [false],
labels: ['"word".includes("a", 0)'],
}, },
{ {
path: "test/built-ins/String/prototype/includes/String.prototype.includes_SuccessNoLocation.js", path: "test/built-ins/String/prototype/includes/String.prototype.includes_SuccessNoLocation.js",
code: `return ["word".includes("w")]`, expected: [true], labels: ['"word".includes("w")'], code: `return ["word".includes("w")]`,
expected: [true],
labels: ['"word".includes("w")'],
}, },
{ {
path: "test/built-ins/String/prototype/includes/String.prototype.includes_FailBadLocation.js", path: "test/built-ins/String/prototype/includes/String.prototype.includes_FailBadLocation.js",
code: `return ["word".includes("w", 5)]`, expected: [false], labels: ['"word".includes("w", 5)'], code: `return ["word".includes("w", 5)]`,
expected: [false],
labels: ['"word".includes("w", 5)'],
}, },
{ {
path: "test/built-ins/String/prototype/includes/String.prototype.includes_FailLocation.js", path: "test/built-ins/String/prototype/includes/String.prototype.includes_FailLocation.js",
code: `return ["word".includes("o", 3)]`, expected: [false], labels: ['"word".includes("o", 3)'], code: `return ["word".includes("o", 3)]`,
expected: [false],
labels: ['"word".includes("o", 3)'],
}, },
{ {
path: "test/built-ins/String/prototype/includes/String.prototype.includes_Success.js", path: "test/built-ins/String/prototype/includes/String.prototype.includes_Success.js",
code: `return ["word".includes("w", 0)]`, expected: [true], labels: ['"word".includes("w", 0)'], code: `return ["word".includes("w", 0)]`,
expected: [true],
labels: ['"word".includes("w", 0)'],
}, },
{ {
path: "test/built-ins/String/prototype/includes/searchstring-found-with-position.js", path: "test/built-ins/String/prototype/includes/searchstring-found-with-position.js",
@@ -503,7 +523,8 @@ const cases = [
{ {
path: "test/built-ins/String/prototype/includes/searchstring-not-found-without-position.js", path: "test/built-ins/String/prototype/includes/searchstring-not-found-without-position.js",
code: `const text = "The future is cool!"; return [text.includes("Flash"), text.includes("FUTURE")]`, code: `const text = "The future is cool!"; return [text.includes("Flash"), text.includes("FUTURE")]`,
expected: [false, false], labels: ["Flash if not included", "includes is case sensitive"], expected: [false, false],
labels: ["Flash if not included", "includes is case sensitive"],
}, },
{ {
path: "test/built-ins/String/prototype/includes/return-false-with-out-of-bounds-position.js", path: "test/built-ins/String/prototype/includes/return-false-with-out-of-bounds-position.js",
@@ -512,15 +533,21 @@ const cases = [
]`, ]`,
expected: [false, false, false, false], expected: [false, false, false, false],
labels: [ labels: [
'str.includes("!", str.length + 1) returns false', 'str.includes("!", 100) returns false', 'str.includes("!", str.length + 1) returns false',
'str.includes("!", Infinity) returns false', 'str.includes("!", str.length) returns false', 'str.includes("!", 100) returns false',
'str.includes("!", Infinity) returns false',
'str.includes("!", str.length) returns false',
], ],
}, },
{ {
path: "test/built-ins/String/prototype/includes/return-true-if-searchstring-is-empty.js", path: "test/built-ins/String/prototype/includes/return-true-if-searchstring-is-empty.js",
code: `const text = "The future is cool!"; return [text.includes("", text.length), text.includes(""), text.includes("", Infinity)]`, code: `const text = "The future is cool!"; return [text.includes("", text.length), text.includes(""), text.includes("", Infinity)]`,
expected: [true, true, true], expected: [true, true, true],
labels: ['str.includes("", str.length) returns true', 'str.includes("") returns true', 'str.includes("", Infinity) returns true'], labels: [
'str.includes("", str.length) returns true',
'str.includes("") returns true',
'str.includes("", Infinity) returns true',
],
}, },
{ {
path: "test/built-ins/String/prototype/includes/coerced-values-of-position.js", path: "test/built-ins/String/prototype/includes/coerced-values-of-position.js",
@@ -536,7 +563,8 @@ const cases = [
code: `const text = "The future is cool!"; return [text.startsWith("The future", 0), text.startsWith("future", 4), text.startsWith(" is cool!", 10)]`, code: `const text = "The future is cool!"; return [text.startsWith("The future", 0), text.startsWith("future", 4), text.startsWith(" is cool!", 10)]`,
expected: [true, true, true], expected: [true, true, true],
labels: [ labels: [
'str.startsWith("The future", 0) === true', 'str.startsWith("future", 4) === true', 'str.startsWith("The future", 0) === true',
'str.startsWith("future", 4) === true',
'str.startsWith(" is cool!", 10) === true', 'str.startsWith(" is cool!", 10) === true',
], ],
}, },
@@ -544,7 +572,11 @@ const cases = [
path: "test/built-ins/String/prototype/startsWith/searchstring-found-without-position.js", path: "test/built-ins/String/prototype/startsWith/searchstring-found-without-position.js",
code: `const text = "The future is cool!"; return [text.startsWith("The "), text.startsWith("The future"), text.startsWith(text)]`, code: `const text = "The future is cool!"; return [text.startsWith("The "), text.startsWith("The future"), text.startsWith(text)]`,
expected: [true, true, true], expected: [true, true, true],
labels: ['str.startsWith("The ") === true', 'str.startsWith("The future") === true', "str.startsWith(str) === true"], labels: [
'str.startsWith("The ") === true',
'str.startsWith("The future") === true',
"str.startsWith(str) === true",
],
}, },
{ {
path: "test/built-ins/String/prototype/startsWith/searchstring-not-found-with-position.js", path: "test/built-ins/String/prototype/startsWith/searchstring-not-found-with-position.js",
@@ -556,7 +588,11 @@ const cases = [
path: "test/built-ins/String/prototype/startsWith/searchstring-not-found-without-position.js", path: "test/built-ins/String/prototype/startsWith/searchstring-not-found-without-position.js",
code: `const text = "The future is cool!"; return [text.startsWith("Flash"), text.startsWith("THE FUTURE"), text.startsWith("future is cool!")]`, code: `const text = "The future is cool!"; return [text.startsWith("Flash"), text.startsWith("THE FUTURE"), text.startsWith("future is cool!")]`,
expected: [false, false, false], expected: [false, false, false],
labels: ['str.startsWith("Flash") === false', "startsWith is case sensitive", 'str.startsWith("future is cool!") === false'], labels: [
'str.startsWith("Flash") === false',
"startsWith is case sensitive",
'str.startsWith("future is cool!") === false',
],
}, },
{ {
path: "test/built-ins/String/prototype/startsWith/out-of-bounds-position.js", path: "test/built-ins/String/prototype/startsWith/out-of-bounds-position.js",
@@ -566,8 +602,10 @@ const cases = [
]`, ]`,
expected: [false, false, false, true, true], expected: [false, false, false, true, true],
labels: [ labels: [
'str.startsWith("!", str.length) returns false', 'str.startsWith("!", 100) returns false', 'str.startsWith("!", str.length) returns false',
'str.startsWith("!", Infinity) returns false', "position argument < 0 will search from the start of the string (-1)", 'str.startsWith("!", 100) returns false',
'str.startsWith("!", Infinity) returns false',
"position argument < 0 will search from the start of the string (-1)",
"position argument < 0 will search from the start of the string (-Infinity)", "position argument < 0 will search from the start of the string (-Infinity)",
], ],
}, },
@@ -575,7 +613,11 @@ const cases = [
path: "test/built-ins/String/prototype/startsWith/return-true-if-searchstring-is-empty.js", path: "test/built-ins/String/prototype/startsWith/return-true-if-searchstring-is-empty.js",
code: `const text = "The future is cool!"; return [text.startsWith(""), text.startsWith("", text.length), text.startsWith("", Infinity)]`, code: `const text = "The future is cool!"; return [text.startsWith(""), text.startsWith("", text.length), text.startsWith("", Infinity)]`,
expected: [true, true, true], expected: [true, true, true],
labels: ['str.startsWith("") returns true', 'str.startsWith("", str.length) returns true', 'str.startsWith("", Infinity) returns true'], labels: [
'str.startsWith("") returns true',
'str.startsWith("", str.length) returns true',
'str.startsWith("", Infinity) returns true',
],
}, },
{ {
path: "test/built-ins/String/prototype/startsWith/coerced-values-of-position.js", path: "test/built-ins/String/prototype/startsWith/coerced-values-of-position.js",
@@ -588,34 +630,47 @@ const cases = [
}, },
{ {
path: "test/built-ins/String/prototype/endsWith/String.prototype.endsWith_Success.js", path: "test/built-ins/String/prototype/endsWith/String.prototype.endsWith_Success.js",
code: `return ["word".endsWith("d")]`, expected: [true], labels: ['"word".endsWith("d")'], code: `return ["word".endsWith("d")]`,
expected: [true],
labels: ['"word".endsWith("d")'],
}, },
{ {
path: "test/built-ins/String/prototype/endsWith/String.prototype.endsWith_Success_2.js", path: "test/built-ins/String/prototype/endsWith/String.prototype.endsWith_Success_2.js",
code: `return ["word".endsWith("d", 4)]`, expected: [true], labels: ['"word".endsWith("d", 4)'], code: `return ["word".endsWith("d", 4)]`,
expected: [true],
labels: ['"word".endsWith("d", 4)'],
}, },
{ {
path: "test/built-ins/String/prototype/endsWith/String.prototype.endsWith_Success_3.js", path: "test/built-ins/String/prototype/endsWith/String.prototype.endsWith_Success_3.js",
code: `return ["word".endsWith("d", 25)]`, expected: [true], labels: ['"word".endsWith("d", 25)'], code: `return ["word".endsWith("d", 25)]`,
expected: [true],
labels: ['"word".endsWith("d", 25)'],
}, },
{ {
path: "test/built-ins/String/prototype/endsWith/String.prototype.endsWith_Success_4.js", path: "test/built-ins/String/prototype/endsWith/String.prototype.endsWith_Success_4.js",
code: `return ["word".endsWith("r", 3)]`, expected: [true], labels: ['"word".endsWith("r", 3)'], code: `return ["word".endsWith("r", 3)]`,
expected: [true],
labels: ['"word".endsWith("r", 3)'],
}, },
{ {
path: "test/built-ins/String/prototype/endsWith/String.prototype.endsWith_Fail.js", path: "test/built-ins/String/prototype/endsWith/String.prototype.endsWith_Fail.js",
code: `return ["word".endsWith("r")]`, expected: [false], labels: ['"word".endsWith("r")'], code: `return ["word".endsWith("r")]`,
expected: [false],
labels: ['"word".endsWith("r")'],
}, },
{ {
path: "test/built-ins/String/prototype/endsWith/String.prototype.endsWith_Fail_2.js", path: "test/built-ins/String/prototype/endsWith/String.prototype.endsWith_Fail_2.js",
code: `return ["word".endsWith("d", 3)]`, expected: [false], labels: ['"word".endsWith("d", 3)'], code: `return ["word".endsWith("d", 3)]`,
expected: [false],
labels: ['"word".endsWith("d", 3)'],
}, },
{ {
path: "test/built-ins/String/prototype/endsWith/searchstring-found-with-position.js", path: "test/built-ins/String/prototype/endsWith/searchstring-found-with-position.js",
code: `const text = "The future is cool!"; return [text.endsWith("The future", 10), text.endsWith("future", 10), text.endsWith(" is cool!", text.length)]`, code: `const text = "The future is cool!"; return [text.endsWith("The future", 10), text.endsWith("future", 10), text.endsWith(" is cool!", text.length)]`,
expected: [true, true, true], expected: [true, true, true],
labels: [ labels: [
'str.endsWith("The future", 10) === true', 'str.endsWith("future", 10) === true', 'str.endsWith("The future", 10) === true',
'str.endsWith("future", 10) === true',
'str.endsWith(" is cool!", str.length) === true', 'str.endsWith(" is cool!", str.length) === true',
], ],
}, },
@@ -635,7 +690,11 @@ const cases = [
path: "test/built-ins/String/prototype/endsWith/searchstring-not-found-without-position.js", path: "test/built-ins/String/prototype/endsWith/searchstring-not-found-without-position.js",
code: `const text = "The future is cool!"; return [text.endsWith("is Flash!"), text.endsWith("IS COOL!"), text.endsWith("The future")]`, code: `const text = "The future is cool!"; return [text.endsWith("is Flash!"), text.endsWith("IS COOL!"), text.endsWith("The future")]`,
expected: [false, false, false], expected: [false, false, false],
labels: ['str.endsWith("is Flash!") === false', "endsWith is case sensitive", 'str.endsWith("The future") === false'], labels: [
'str.endsWith("is Flash!") === false',
"endsWith is case sensitive",
'str.endsWith("The future") === false',
],
}, },
{ {
path: "test/built-ins/String/prototype/endsWith/return-false-if-search-start-is-less-than-zero.js", path: "test/built-ins/String/prototype/endsWith/return-false-if-search-start-is-less-than-zero.js",
@@ -651,8 +710,11 @@ const cases = [
]`, ]`,
expected: [true, true, true, true, true], expected: [true, true, true, true, true],
labels: [ labels: [
'str.endsWith("") returns true', 'str.endsWith("", str.length) returns true', 'str.endsWith("", Infinity) returns true', 'str.endsWith("") returns true',
'str.endsWith("", -1) returns true', 'str.endsWith("", -Infinity) returns true', 'str.endsWith("", str.length) returns true',
'str.endsWith("", Infinity) returns true',
'str.endsWith("", -1) returns true',
'str.endsWith("", -Infinity) returns true',
], ],
}, },
{ {
@@ -665,27 +727,39 @@ const cases = [
}, },
{ {
path: "test/built-ins/String/prototype/indexOf/S15.5.4.7_A2_T1.js", path: "test/built-ins/String/prototype/indexOf/S15.5.4.7_A2_T1.js",
code: `return ["abcd".indexOf("abcdab")]`, expected: [-1], labels: ['#1: "abcd".indexOf("abcdab")===-1'], code: `return ["abcd".indexOf("abcdab")]`,
expected: [-1],
labels: ['#1: "abcd".indexOf("abcdab")===-1'],
}, },
{ {
path: "test/built-ins/String/prototype/indexOf/S15.5.4.7_A2_T2.js", path: "test/built-ins/String/prototype/indexOf/S15.5.4.7_A2_T2.js",
code: `return ["abcd".indexOf("abcdab", 0)]`, expected: [-1], labels: ['#1: "abcd".indexOf("abcdab",0)===-1'], code: `return ["abcd".indexOf("abcdab", 0)]`,
expected: [-1],
labels: ['#1: "abcd".indexOf("abcdab",0)===-1'],
}, },
{ {
path: "test/built-ins/String/prototype/indexOf/S15.5.4.7_A2_T3.js", path: "test/built-ins/String/prototype/indexOf/S15.5.4.7_A2_T3.js",
code: `return ["abcd".indexOf("abcdab", 99)]`, expected: [-1], labels: ['#1: "abcd".indexOf("abcdab",99)===-1'], code: `return ["abcd".indexOf("abcdab", 99)]`,
expected: [-1],
labels: ['#1: "abcd".indexOf("abcdab",99)===-1'],
}, },
{ {
path: "test/built-ins/String/prototype/indexOf/S15.5.4.7_A2_T4.js", path: "test/built-ins/String/prototype/indexOf/S15.5.4.7_A2_T4.js",
code: `return ["abcd".indexOf("abcdab", NaN)]`, expected: [-1], labels: ['#1: "abcd".indexOf("abcdab",NaN)===-1'], code: `return ["abcd".indexOf("abcdab", NaN)]`,
expected: [-1],
labels: ['#1: "abcd".indexOf("abcdab",NaN)===-1'],
}, },
{ {
path: "test/built-ins/String/prototype/indexOf/S15.5.4.7_A3_T1.js", path: "test/built-ins/String/prototype/indexOf/S15.5.4.7_A3_T1.js",
code: `return ["$$abcdabcd".indexOf("ab", NaN)]`, expected: [2], labels: ['#1: "$$abcdabcd".indexOf("ab",NaN)===2'], code: `return ["$$abcdabcd".indexOf("ab", NaN)]`,
expected: [2],
labels: ['#1: "$$abcdabcd".indexOf("ab",NaN)===2'],
}, },
{ {
path: "test/built-ins/String/prototype/indexOf/S15.5.4.7_A3_T3.js", path: "test/built-ins/String/prototype/indexOf/S15.5.4.7_A3_T3.js",
code: `return ["$$abcdabcd".indexOf("ab", -Infinity)]`, expected: [2], labels: ['#1: "$$abcdabcd".indexOf("ab", function(){return -Infinity;}())===2'], code: `return ["$$abcdabcd".indexOf("ab", -Infinity)]`,
expected: [2],
labels: ['#1: "$$abcdabcd".indexOf("ab", function(){return -Infinity;}())===2'],
}, },
{ {
path: "test/built-ins/String/prototype/indexOf/position-tointeger.js", path: "test/built-ins/String/prototype/indexOf/position-tointeger.js",
@@ -697,21 +771,31 @@ const cases = [
]`, ]`,
expected: [0, 1, 0, 0, 1, 0, -1, 0, 2, 2], expected: [0, 1, 0, 0, 1, 0, -1, 0, 2, 2],
labels: [ labels: [
"position 0", "position 1", "ToInteger: truncate towards 0 (-0.9)", "ToInteger: truncate towards 0 (0.9)", "position 0",
"ToInteger: truncate towards 0 (1.9)", "ToInteger: NaN => 0", "position Infinity", "position 1",
"ToInteger: undefined => NaN => 0", "position 2", "ToInteger: truncate towards 0 (2.9)", "ToInteger: truncate towards 0 (-0.9)",
"ToInteger: truncate towards 0 (0.9)",
"ToInteger: truncate towards 0 (1.9)",
"ToInteger: NaN => 0",
"position Infinity",
"ToInteger: undefined => NaN => 0",
"position 2",
"ToInteger: truncate towards 0 (2.9)",
], ],
}, },
{ {
path: "test/built-ins/String/prototype/indexOf/searchstring-tostring.js", path: "test/built-ins/String/prototype/indexOf/searchstring-tostring.js",
code: `return ["foo".indexOf(""), "__foo__".indexOf("foo")]`, code: `return ["foo".indexOf(""), "__foo__".indexOf("foo")]`,
expected: [0, 2], labels: ['"foo".indexOf("")', '"__foo__".indexOf("foo")'], expected: [0, 2],
labels: ['"foo".indexOf("")', '"__foo__".indexOf("foo")'],
}, },
{ {
path: "test/built-ins/String/prototype/lastIndexOf/not-a-substring.js", path: "test/built-ins/String/prototype/lastIndexOf/not-a-substring.js",
code: `return ["abc".lastIndexOf("d")]`, code: `return ["abc".lastIndexOf("d")]`,
expected: [-1], expected: [-1],
labels: ["String.prototype.lastIndexOf returns -1 when searchString is shorter than this and searchString is not a substring of this."], labels: [
"String.prototype.lastIndexOf returns -1 when searchString is shorter than this and searchString is not a substring of this.",
],
}, },
] as const ] as const
@@ -0,0 +1,21 @@
import type { APIEvent } from "@solidjs/start/server"
import { Workspace } from "@opencode-ai/console-core/workspace.js"
import { safeEqual } from "@opencode-ai/console-core/util/crypto.js"
import { Resource } from "@opencode-ai/console-resource"
import z from "zod"
const Body = z.object({ workspaceID: z.string().startsWith("wrk_") })
export async function POST(event: APIEvent) {
if (!safeEqual(event.request.headers.get("authorization") ?? "", `Bearer ${Resource.SUPPORT_API_KEY.value}`)) {
return Response.json({ error: "Unauthorized" }, { status: 401 })
}
const body = Body.safeParse(await event.request.json().catch(() => undefined))
if (!body.success) {
return Response.json({ error: "Invalid request", issues: body.error.issues }, { status: 400 })
}
return Workspace.unblock(body.data.workspaceID)
.then(() => Response.json({ success: true, message: "Workspace unblocked" }))
.catch((error) => Response.json({ error: error instanceof Error ? error.message : String(error) }, { status: 400 }))
}
+12
View File
@@ -103,4 +103,16 @@ export namespace Workspace {
.where(eq(WorkspaceTable.id, Actor.workspace())), .where(eq(WorkspaceTable.id, Actor.workspace())),
) )
}) })
export const unblock = fn(z.string().startsWith("wrk_"), async (workspaceID) => {
await Database.transaction(async (tx) => {
const workspace = await tx
.select({ id: WorkspaceTable.id })
.from(WorkspaceTable)
.where(eq(WorkspaceTable.id, workspaceID))
.then((rows) => rows[0])
if (!workspace) throw new Error("Workspace not found")
await tx.update(WorkspaceTable).set({ is_blocked: false }).where(eq(WorkspaceTable.id, workspaceID))
})
})
} }
+1
View File
@@ -18,6 +18,7 @@
}, },
"exports": { "exports": {
"./environment": "./src/environment/index.ts", "./environment": "./src/environment/index.ts",
"./testing/environment-conformance": "./test/lib/environment-conformance.ts",
"./session/runner": "./src/session/runner/index.ts", "./session/runner": "./src/session/runner/index.ts",
"./instructions": "./src/instructions/index.ts", "./instructions": "./src/instructions/index.ts",
"./*": "./src/*.ts" "./*": "./src/*.ts"
+39 -118
View File
@@ -2,9 +2,7 @@
"version": "7", "version": "7",
"dialect": "sqlite", "dialect": "sqlite",
"id": "2d214a71-3b0a-48c1-a667-741952c4e188", "id": "2d214a71-3b0a-48c1-a667-741952c4e188",
"prevIds": [ "prevIds": ["f14a9b18-8207-487e-a3d3-227e629ba9ad"],
"f14a9b18-8207-487e-a3d3-227e629ba9ad"
],
"ddl": [ "ddl": [
{ {
"name": "workspace", "name": "workspace",
@@ -1385,13 +1383,9 @@
"table": "session_v2" "table": "session_v2"
}, },
{ {
"columns": [ "columns": ["project_id"],
"project_id"
],
"tableTo": "project", "tableTo": "project",
"columnsTo": [ "columnsTo": ["id"],
"id"
],
"onUpdate": "NO ACTION", "onUpdate": "NO ACTION",
"onDelete": "CASCADE", "onDelete": "CASCADE",
"nameExplicit": false, "nameExplicit": false,
@@ -1400,13 +1394,9 @@
"table": "workspace" "table": "workspace"
}, },
{ {
"columns": [ "columns": ["active_account_id"],
"active_account_id"
],
"tableTo": "account", "tableTo": "account",
"columnsTo": [ "columnsTo": ["id"],
"id"
],
"onUpdate": "NO ACTION", "onUpdate": "NO ACTION",
"onDelete": "SET NULL", "onDelete": "SET NULL",
"nameExplicit": false, "nameExplicit": false,
@@ -1415,13 +1405,9 @@
"table": "account_state" "table": "account_state"
}, },
{ {
"columns": [ "columns": ["aggregate_id"],
"aggregate_id"
],
"tableTo": "event_sequence", "tableTo": "event_sequence",
"columnsTo": [ "columnsTo": ["aggregate_id"],
"aggregate_id"
],
"onUpdate": "NO ACTION", "onUpdate": "NO ACTION",
"onDelete": "CASCADE", "onDelete": "CASCADE",
"nameExplicit": false, "nameExplicit": false,
@@ -1430,13 +1416,9 @@
"table": "event" "table": "event"
}, },
{ {
"columns": [ "columns": ["project_id"],
"project_id"
],
"tableTo": "project", "tableTo": "project",
"columnsTo": [ "columnsTo": ["id"],
"id"
],
"onUpdate": "NO ACTION", "onUpdate": "NO ACTION",
"onDelete": "CASCADE", "onDelete": "CASCADE",
"nameExplicit": false, "nameExplicit": false,
@@ -1445,13 +1427,9 @@
"table": "permission" "table": "permission"
}, },
{ {
"columns": [ "columns": ["project_id"],
"project_id"
],
"tableTo": "project", "tableTo": "project",
"columnsTo": [ "columnsTo": ["id"],
"id"
],
"onUpdate": "NO ACTION", "onUpdate": "NO ACTION",
"onDelete": "CASCADE", "onDelete": "CASCADE",
"nameExplicit": false, "nameExplicit": false,
@@ -1460,13 +1438,9 @@
"table": "project_directory" "table": "project_directory"
}, },
{ {
"columns": [ "columns": ["session_id"],
"session_id"
],
"tableTo": "session_v2", "tableTo": "session_v2",
"columnsTo": [ "columnsTo": ["id"],
"id"
],
"onUpdate": "NO ACTION", "onUpdate": "NO ACTION",
"onDelete": "CASCADE", "onDelete": "CASCADE",
"nameExplicit": false, "nameExplicit": false,
@@ -1475,13 +1449,9 @@
"table": "instruction_entry" "table": "instruction_entry"
}, },
{ {
"columns": [ "columns": ["session_id"],
"session_id"
],
"tableTo": "session_v2", "tableTo": "session_v2",
"columnsTo": [ "columnsTo": ["id"],
"id"
],
"onUpdate": "NO ACTION", "onUpdate": "NO ACTION",
"onDelete": "CASCADE", "onDelete": "CASCADE",
"nameExplicit": false, "nameExplicit": false,
@@ -1490,13 +1460,9 @@
"table": "instruction_state" "table": "instruction_state"
}, },
{ {
"columns": [ "columns": ["session_id"],
"session_id"
],
"tableTo": "session_v2", "tableTo": "session_v2",
"columnsTo": [ "columnsTo": ["id"],
"id"
],
"onUpdate": "NO ACTION", "onUpdate": "NO ACTION",
"onDelete": "CASCADE", "onDelete": "CASCADE",
"nameExplicit": false, "nameExplicit": false,
@@ -1505,13 +1471,9 @@
"table": "session_message" "table": "session_message"
}, },
{ {
"columns": [ "columns": ["session_id"],
"session_id"
],
"tableTo": "session_v2", "tableTo": "session_v2",
"columnsTo": [ "columnsTo": ["id"],
"id"
],
"onUpdate": "NO ACTION", "onUpdate": "NO ACTION",
"onDelete": "CASCADE", "onDelete": "CASCADE",
"nameExplicit": false, "nameExplicit": false,
@@ -1520,13 +1482,9 @@
"table": "session_pending" "table": "session_pending"
}, },
{ {
"columns": [ "columns": ["project_id"],
"project_id"
],
"tableTo": "project", "tableTo": "project",
"columnsTo": [ "columnsTo": ["id"],
"id"
],
"onUpdate": "NO ACTION", "onUpdate": "NO ACTION",
"onDelete": "CASCADE", "onDelete": "CASCADE",
"nameExplicit": false, "nameExplicit": false,
@@ -1535,156 +1493,119 @@
"table": "session_v2" "table": "session_v2"
}, },
{ {
"columns": [ "columns": ["email", "url"],
"email",
"url"
],
"nameExplicit": false, "nameExplicit": false,
"name": "control_account_pk", "name": "control_account_pk",
"entityType": "pks", "entityType": "pks",
"table": "control_account" "table": "control_account"
}, },
{ {
"columns": [ "columns": ["project_id", "directory"],
"project_id",
"directory"
],
"nameExplicit": false, "nameExplicit": false,
"name": "project_directory_pk", "name": "project_directory_pk",
"entityType": "pks", "entityType": "pks",
"table": "project_directory" "table": "project_directory"
}, },
{ {
"columns": [ "columns": ["session_id", "key"],
"session_id",
"key"
],
"nameExplicit": false, "nameExplicit": false,
"name": "instruction_entry_pk", "name": "instruction_entry_pk",
"entityType": "pks", "entityType": "pks",
"table": "instruction_entry" "table": "instruction_entry"
}, },
{ {
"columns": [ "columns": ["id"],
"id"
],
"nameExplicit": false, "nameExplicit": false,
"name": "workspace_pk", "name": "workspace_pk",
"table": "workspace", "table": "workspace",
"entityType": "pks" "entityType": "pks"
}, },
{ {
"columns": [ "columns": ["id"],
"id"
],
"nameExplicit": false, "nameExplicit": false,
"name": "account_state_pk", "name": "account_state_pk",
"table": "account_state", "table": "account_state",
"entityType": "pks" "entityType": "pks"
}, },
{ {
"columns": [ "columns": ["id"],
"id"
],
"nameExplicit": false, "nameExplicit": false,
"name": "account_pk", "name": "account_pk",
"table": "account", "table": "account",
"entityType": "pks" "entityType": "pks"
}, },
{ {
"columns": [ "columns": ["id"],
"id"
],
"nameExplicit": false, "nameExplicit": false,
"name": "credential_pk", "name": "credential_pk",
"table": "credential", "table": "credential",
"entityType": "pks" "entityType": "pks"
}, },
{ {
"columns": [ "columns": ["aggregate_id"],
"aggregate_id"
],
"nameExplicit": false, "nameExplicit": false,
"name": "event_sequence_pk", "name": "event_sequence_pk",
"table": "event_sequence", "table": "event_sequence",
"entityType": "pks" "entityType": "pks"
}, },
{ {
"columns": [ "columns": ["id"],
"id"
],
"nameExplicit": false, "nameExplicit": false,
"name": "event_pk", "name": "event_pk",
"table": "event", "table": "event",
"entityType": "pks" "entityType": "pks"
}, },
{ {
"columns": [ "columns": ["key"],
"key"
],
"nameExplicit": false, "nameExplicit": false,
"name": "kv_pk", "name": "kv_pk",
"table": "kv", "table": "kv",
"entityType": "pks" "entityType": "pks"
}, },
{ {
"columns": [ "columns": ["id"],
"id"
],
"nameExplicit": false, "nameExplicit": false,
"name": "permission_pk", "name": "permission_pk",
"table": "permission", "table": "permission",
"entityType": "pks" "entityType": "pks"
}, },
{ {
"columns": [ "columns": ["id"],
"id"
],
"nameExplicit": false, "nameExplicit": false,
"name": "project_pk", "name": "project_pk",
"table": "project", "table": "project",
"entityType": "pks" "entityType": "pks"
}, },
{ {
"columns": [ "columns": ["hash"],
"hash"
],
"nameExplicit": false, "nameExplicit": false,
"name": "instruction_blob_pk", "name": "instruction_blob_pk",
"table": "instruction_blob", "table": "instruction_blob",
"entityType": "pks" "entityType": "pks"
}, },
{ {
"columns": [ "columns": ["session_id"],
"session_id"
],
"nameExplicit": false, "nameExplicit": false,
"name": "instruction_state_pk", "name": "instruction_state_pk",
"table": "instruction_state", "table": "instruction_state",
"entityType": "pks" "entityType": "pks"
}, },
{ {
"columns": [ "columns": ["id"],
"id"
],
"nameExplicit": false, "nameExplicit": false,
"name": "session_message_pk", "name": "session_message_pk",
"table": "session_message", "table": "session_message",
"entityType": "pks" "entityType": "pks"
}, },
{ {
"columns": [ "columns": ["id"],
"id"
],
"nameExplicit": false, "nameExplicit": false,
"name": "session_pending_pk", "name": "session_pending_pk",
"table": "session_pending", "table": "session_pending",
"entityType": "pks" "entityType": "pks"
}, },
{ {
"columns": [ "columns": ["id"],
"id"
],
"nameExplicit": false, "nameExplicit": false,
"name": "session_v2_pk", "name": "session_v2_pk",
"table": "session_v2", "table": "session_v2",
@@ -1940,4 +1861,4 @@
} }
], ],
"renames": [] "renames": []
} }
+1 -3
View File
@@ -20,9 +20,7 @@ if (!Number.isInteger(iterations) || iterations < 1) {
} }
const ref = Location.Ref.make({ directory: AbsolutePath.make(path.resolve(directory)) }) const ref = Location.Ref.make({ directory: AbsolutePath.make(path.resolve(directory)) })
const layer = AppNodeBuilder.build( const layer = AppNodeBuilder.build(LayerNode.group([Database.node, Bus.node, SdkPlugins.node, LocationServiceMap.node]))
LayerNode.group([Database.node, Bus.node, SdkPlugins.node, LocationServiceMap.node]),
)
const measure = <A, E, R>(effect: Effect.Effect<A, E, R>) => const measure = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
Effect.gen(function* () { Effect.gen(function* () {
-101
View File
@@ -1,101 +0,0 @@
export * as Account from "./account"
import { Schema } from "effect"
import type { HttpClientError } from "effect/unstable/http"
export const ID = Schema.String.pipe(Schema.brand("AccountID"))
export type ID = Schema.Schema.Type<typeof ID>
export const OrgID = Schema.String.pipe(Schema.brand("OrgID"))
export type OrgID = Schema.Schema.Type<typeof OrgID>
export const AccessToken = Schema.String.pipe(Schema.brand("AccessToken"))
export type AccessToken = Schema.Schema.Type<typeof AccessToken>
export const RefreshToken = Schema.String.pipe(Schema.brand("RefreshToken"))
export type RefreshToken = Schema.Schema.Type<typeof RefreshToken>
export const DeviceCode = Schema.String.pipe(Schema.brand("DeviceCode"))
export type DeviceCode = Schema.Schema.Type<typeof DeviceCode>
export const UserCode = Schema.String.pipe(Schema.brand("UserCode"))
export type UserCode = Schema.Schema.Type<typeof UserCode>
export class Info extends Schema.Class<Info>("Account")({
id: ID,
email: Schema.String,
url: Schema.String,
active_org_id: Schema.NullOr(OrgID),
}) {}
export class Org extends Schema.Class<Org>("Org")({
id: OrgID,
name: Schema.String,
}) {}
export class AccountRepoError extends Schema.TaggedErrorClass<AccountRepoError>()("AccountRepoError", {
message: Schema.String,
cause: Schema.optional(Schema.Defect()),
}) {}
export class AccountServiceError extends Schema.TaggedErrorClass<AccountServiceError>()("AccountServiceError", {
message: Schema.String,
cause: Schema.optional(Schema.Defect()),
}) {}
export class AccountTransportError extends Schema.TaggedErrorClass<AccountTransportError>()("AccountTransportError", {
method: Schema.String,
url: Schema.String,
description: Schema.optional(Schema.String),
cause: Schema.optional(Schema.Defect()),
}) {
static fromHttpClientError(error: HttpClientError.TransportError): AccountTransportError {
return new AccountTransportError({
method: error.request.method,
url: error.request.url,
description: error.description,
cause: error.cause,
})
}
override get message(): string {
return [
`Could not reach ${this.method} ${this.url}.`,
`This failed before the server returned an HTTP response.`,
this.description,
`Check your network, proxy, or VPN configuration and try again.`,
]
.filter(Boolean)
.join("\n")
}
}
export type AccountError = AccountRepoError | AccountServiceError | AccountTransportError
export class Login extends Schema.Class<Login>("Login")({
code: DeviceCode,
user: UserCode,
url: Schema.String,
server: Schema.String,
expiry: Schema.Duration,
interval: Schema.Duration,
}) {}
export class PollSuccess extends Schema.TaggedClass<PollSuccess>()("PollSuccess", {
email: Schema.String,
}) {}
export class PollPending extends Schema.TaggedClass<PollPending>()("PollPending", {}) {}
export class PollSlow extends Schema.TaggedClass<PollSlow>()("PollSlow", {}) {}
export class PollExpired extends Schema.TaggedClass<PollExpired>()("PollExpired", {}) {}
export class PollDenied extends Schema.TaggedClass<PollDenied>()("PollDenied", {}) {}
export class PollError extends Schema.TaggedClass<PollError>()("PollError", {
cause: Schema.Defect(),
}) {}
export const PollResult = Schema.Union([PollSuccess, PollPending, PollSlow, PollExpired, PollDenied, PollError])
export type PollResult = Schema.Schema.Type<typeof PollResult>
+7 -10
View File
@@ -1,24 +1,21 @@
import { sqliteTable, text, integer, primaryKey } from "drizzle-orm/sqlite-core" import { sqliteTable, text, integer, primaryKey } from "drizzle-orm/sqlite-core"
import { Account } from "../account"
import { Timestamps } from "../database/schema.sql" import { Timestamps } from "../database/schema.sql"
export const AccountTable = sqliteTable("account", { export const AccountTable = sqliteTable("account", {
id: text().$type<Account.ID>().primaryKey(), id: text().primaryKey(),
email: text().notNull(), email: text().notNull(),
url: text().notNull(), url: text().notNull(),
access_token: text().$type<Account.AccessToken>().notNull(), access_token: text().notNull(),
refresh_token: text().$type<Account.RefreshToken>().notNull(), refresh_token: text().notNull(),
token_expiry: integer(), token_expiry: integer(),
...Timestamps, ...Timestamps,
}) })
export const AccountStateTable = sqliteTable("account_state", { export const AccountStateTable = sqliteTable("account_state", {
id: integer().primaryKey(), id: integer().primaryKey(),
active_account_id: text() active_account_id: text().references(() => AccountTable.id, { onDelete: "set null" }),
.$type<Account.ID>() active_org_id: text(),
.references(() => AccountTable.id, { onDelete: "set null" }),
active_org_id: text().$type<Account.OrgID>(),
}) })
// LEGACY // LEGACY
@@ -27,8 +24,8 @@ export const ControlAccountTable = sqliteTable(
{ {
email: text().notNull(), email: text().notNull(),
url: text().notNull(), url: text().notNull(),
access_token: text().$type<Account.AccessToken>().notNull(), access_token: text().notNull(),
refresh_token: text().$type<Account.RefreshToken>().notNull(), refresh_token: text().notNull(),
token_expiry: integer(), token_expiry: integer(),
active: integer({ mode: "boolean" }) active: integer({ mode: "boolean" })
.notNull() .notNull()
+532 -521
View File
File diff suppressed because it is too large Load Diff
+2 -4
View File
@@ -115,8 +115,7 @@ const layer = Layer.effect(
} }
draft.providers.set(providerID, record) draft.providers.set(providerID, record)
} }
const model = const model = record.models.get(modelID) ?? (Model.Info.default(providerID, modelID) as Model.MutableInfo)
record.models.get(modelID) ?? (Model.Info.default(providerID, modelID) as Model.MutableInfo)
if (!record.models.has(modelID)) record.models.set(modelID, model) if (!record.models.has(modelID)) record.models.set(modelID, model)
fn(model) fn(model)
model.id = modelID model.id = modelID
@@ -249,8 +248,7 @@ const layer = Layer.effect(
items, items,
Order.mapInput( Order.mapInput(
Order.Number, Order.Number,
(item: (typeof candidates)[number]) => (item: (typeof candidates)[number]) => (item.cost / maxCost) * 0.8 + (item.age / maxAge) * 0.2,
(item.cost / maxCost) * 0.8 + (item.age / maxAge) * 0.2,
), ),
) )
return projectModel(selected.model, provider) return projectModel(selected.model, provider)
+1 -3
View File
@@ -46,9 +46,7 @@ export function summarize(entries: ReadonlyArray<Entry>, budget = INLINE_BUDGET)
.map((entry) => { .map((entry) => {
const firstLine = entry.description.split("\n", 1)[0]?.trim() ?? "" const firstLine = entry.description.split("\n", 1)[0]?.trim() ?? ""
const description = const description =
firstLine.length > DESCRIPTION_LIMIT firstLine.length > DESCRIPTION_LIMIT ? firstLine.slice(0, DESCRIPTION_LIMIT - 3) + "..." : firstLine
? firstLine.slice(0, DESCRIPTION_LIMIT - 3) + "..."
: firstLine
const suffix = description.length === 0 ? "" : ` // ${description}` const suffix = description.length === 0 ? "" : ` // ${description}`
return { path: entry.path, line: ` - ${entry.signature}${suffix}` } return { path: entry.path, line: ` - ${entry.signature}${suffix}` }
}) })
+4 -9
View File
@@ -43,14 +43,9 @@ const description = [
export const create = ( export const create = (
registrations: ReadonlyMap<string, Info>, registrations: ReadonlyMap<string, Info>,
executeTool: ( executeTool: (name: string, tool: Info, input: unknown, context: Context) => Effect.Effect<Result, Error>,
name: string,
tool: Info,
input: unknown,
context: Context,
) => Effect.Effect<Result, Error>,
) => { ) => {
return ({ return {
name: "execute", name: "execute",
description, description,
input: CodeMode.Input, input: CodeMode.Input,
@@ -76,7 +71,7 @@ export const create = (
const content = const content =
typeof executed.content === "string" typeof executed.content === "string"
? [{ type: "text" as const, text: executed.content }] ? [{ type: "text" as const, text: executed.content }]
: executed.content ?? [] : (executed.content ?? [])
const outputFileParts = outputFiles(content) const outputFileParts = outputFiles(content)
if (outputFileParts.length > 0) if (outputFileParts.length > 0)
yield* Ref.update(files, (items) => [...items, { index, files: outputFileParts }]) yield* Ref.update(files, (items) => [...items, { index, files: outputFileParts }])
@@ -134,7 +129,7 @@ export const create = (
metadata, metadata,
} }
}), }),
}) satisfies Info } satisfies Info
} }
export const catalog = (registrations: ReadonlyMap<string, Info>) => { export const catalog = (registrations: ReadonlyMap<string, Info>) => {
+108 -93
View File
@@ -52,103 +52,108 @@ export interface Interface extends State.Transformable<Draft> {
export class Service extends Context.Service<Service, Interface>()("@opencode/Command") {} export class Service extends Context.Service<Service, Interface>()("@opencode/Command") {}
export const layer = (options?: ShellSelect.Options) => Layer.effect( export const layer = (options?: ShellSelect.Options) =>
Service, Layer.effect(
Effect.gen(function* () { Service,
const mcp = yield* MCP.Service Effect.gen(function* () {
const bus = yield* Bus.Service const mcp = yield* MCP.Service
const processes = yield* AppProcess.Service const bus = yield* Bus.Service
const config = yield* Config.Service const processes = yield* AppProcess.Service
const location = yield* Location.Service const config = yield* Config.Service
const state = State.create<Data, Draft>({ const location = yield* Location.Service
name: "command", const state = State.create<Data, Draft>({
initial: () => ({ commands: new Map() }), name: "command",
draft: (draft) => ({ initial: () => ({ commands: new Map() }),
list: () => Array.from(draft.commands.values()) as Info[], draft: (draft) => ({
get: (name) => draft.commands.get(name), list: () => Array.from(draft.commands.values()) as Info[],
update: (name, update) => { get: (name) => draft.commands.get(name),
const current = draft.commands.get(name) ?? ({ name, template: "" } as Types.DeepMutable<Info>) update: (name, update) => {
if (!draft.commands.has(name)) draft.commands.set(name, current) const current = draft.commands.get(name) ?? ({ name, template: "" } as Types.DeepMutable<Info>)
update(current) if (!draft.commands.has(name)) draft.commands.set(name, current)
current.name = name update(current)
}, current.name = name
remove: (name) => { },
draft.commands.delete(name) remove: (name) => {
}, draft.commands.delete(name)
}), },
finalize: () => bus.publish(Command.Event.Updated, {}).pipe(Effect.asVoid),
})
const staticCommand = (name: string) => state.get().commands.get(name) as Info | undefined
const mcpCommands = Effect.fnUntraced(function* () {
return (yield* mcp.prompts()).map((prompt) =>
Info.make({
name: mcpCommandName(prompt.server, prompt.name),
template: "",
description: prompt.description,
}), }),
) finalize: () => bus.publish(Command.Event.Updated, {}).pipe(Effect.asVoid),
}) })
const staticCommand = (name: string) => state.get().commands.get(name) as Info | undefined
const mcpCommands = Effect.fnUntraced(function* () {
return (yield* mcp.prompts()).map((prompt) =>
Info.make({
name: mcpCommandName(prompt.server, prompt.name),
template: "",
description: prompt.description,
}),
)
})
return Service.of({ return Service.of({
reload: state.reload, reload: state.reload,
transform: state.transform, transform: state.transform,
get: Effect.fn("Command.get")(function* (name) { get: Effect.fn("Command.get")(function* (name) {
const command = staticCommand(name) const command = staticCommand(name)
if (command) return command if (command) return command
return (yield* mcpCommands()).find((command) => command.name === name) return (yield* mcpCommands()).find((command) => command.name === name)
}), }),
list: Effect.fn("Command.list")(function* () { list: Effect.fn("Command.list")(function* () {
const commands = Array.from(state.get().commands.values()) as Info[] const commands = Array.from(state.get().commands.values()) as Info[]
const names = new Set(commands.map((command) => command.name)) const names = new Set(commands.map((command) => command.name))
return [ return [...commands, ...(yield* mcpCommands()).filter((command) => !names.has(command.name))]
...commands, }),
...(yield* mcpCommands()).filter((command) => !names.has(command.name)), evaluate: Effect.fn("Command.evaluate")(function* (input) {
] const command = staticCommand(input.name)
}), if (command)
evaluate: Effect.fn("Command.evaluate")(function* (input) { return yield* evaluateTemplate(input.name, command.template, input.arguments ?? "", {
const command = staticCommand(input.name) config,
if (command) return yield* evaluateTemplate(input.name, command.template, input.arguments ?? "", { location,
config, processes,
location, shell: options,
processes, })
shell: options,
})
const prompt = (yield* mcp.prompts()).find((prompt) => mcpCommandName(prompt.server, prompt.name) === input.name) const prompt = (yield* mcp.prompts()).find(
if (!prompt) return yield* new NotFoundError({ command: input.name, message: `Command not found: ${input.name}` }) (prompt) => mcpCommandName(prompt.server, prompt.name) === input.name,
const result = yield* mcp )
.prompt({ if (!prompt)
server: prompt.server, return yield* new NotFoundError({ command: input.name, message: `Command not found: ${input.name}` })
name: prompt.name, const result = yield* mcp
args: Object.fromEntries( .prompt({
(prompt.arguments ?? []).map((argument, index) => [ server: prompt.server,
argument.name, name: prompt.name,
parseArguments(input.arguments ?? "")[index] ?? "", args: Object.fromEntries(
]), (prompt.arguments ?? []).map((argument, index) => [
), argument.name,
}) parseArguments(input.arguments ?? "")[index] ?? "",
.pipe( ]),
Effect.catchTag( ),
"MCP.NotFoundError", })
() => .pipe(
Effect.catchTag("MCP.NotFoundError", () =>
Effect.fail( Effect.fail(
new EvaluationError({ new EvaluationError({
command: input.name, command: input.name,
message: `MCP server could not be found while evaluating prompt: ${prompt.server}`, message: `MCP server could not be found while evaluating prompt: ${prompt.server}`,
}), }),
), ),
), ),
) )
if (!result) if (!result)
return yield* new EvaluationError({ return yield* new EvaluationError({
command: input.name, command: input.name,
message: `MCP prompt could not be evaluated: ${prompt.server}:${prompt.name}`, message: `MCP prompt could not be evaluated: ${prompt.server}:${prompt.name}`,
}) })
return { text: result.messages.map((message) => promptMessageText(message.content)).join("\n").trim() } return {
}), text: result.messages
}) .map((message) => promptMessageText(message.content))
}), .join("\n")
) .trim(),
}
}),
})
}),
)
function evaluateTemplate( function evaluateTemplate(
command: string, command: string,
@@ -179,7 +184,8 @@ function evaluateArguments(template: string, input: string) {
return args[argIndex] return args[argIndex]
}) })
const withArguments = expanded.replaceAll("$ARGUMENTS", input) const withArguments = expanded.replaceAll("$ARGUMENTS", input)
if (placeholders.length === 0 && !template.includes("$ARGUMENTS") && input.trim()) return `${withArguments}\n\n${input}`.trim() if (placeholders.length === 0 && !template.includes("$ARGUMENTS") && input.trim())
return `${withArguments}\n\n${input}`.trim()
return withArguments.trim() return withArguments.trim()
} }
@@ -201,14 +207,23 @@ const evaluateShell = Effect.fnUntraced(function* (
(match) => { (match) => {
const source = match[1] ?? "" const source = match[1] ?? ""
return services.processes return services.processes
.run(ChildProcess.make(shell, ShellSelect.args(shell, source), { cwd: services.location.directory, stdin: "ignore" }), { .run(
combineOutput: true, ChildProcess.make(shell, ShellSelect.args(shell, source), {
}) cwd: services.location.directory,
stdin: "ignore",
}),
{
combineOutput: true,
},
)
.pipe( .pipe(
Effect.map((result) => (result.output ?? Buffer.concat([result.stdout, result.stderr])).toString("utf8")), Effect.map((result) => (result.output ?? Buffer.concat([result.stdout, result.stderr])).toString("utf8")),
Effect.mapError( Effect.mapError(
(error) => (error) =>
new EvaluationError({ command, message: `Shell interpolation failed for ${JSON.stringify(source)}: ${error.message}` }), new EvaluationError({
command,
message: `Shell interpolation failed for ${JSON.stringify(source)}: ${error.message}`,
}),
), ),
) )
}, },
+265 -260
View File
@@ -78,287 +78,292 @@ export const testLayer = (initial: Entry[] = []) =>
}), }),
) )
export const layer = (options?: Options) => Layer.effect( export const layer = (options?: Options) =>
Service, Layer.effect(
Effect.gen(function* () { Service,
const fs = yield* FSUtil.Service Effect.gen(function* () {
const global = yield* Global.Service const fs = yield* FSUtil.Service
const location = yield* Location.Service const global = yield* Global.Service
const watcher = yield* Watcher.Service const location = yield* Location.Service
const bus = yield* Bus.Service const watcher = yield* Watcher.Service
const credentials = yield* Credential.Service const bus = yield* Bus.Service
const wellknown = yield* WellKnown.Service const credentials = yield* Credential.Service
const names = ["opencode.json", "opencode.jsonc"] const wellknown = yield* WellKnown.Service
const reloadLock = Semaphore.makeUnsafe(1) const names = ["opencode.json", "opencode.jsonc"]
const decodeOptions = { errors: "all", onExcessProperty: "ignore", propertyOrder: "original" } as const const reloadLock = Semaphore.makeUnsafe(1)
const decodeInfo = Schema.decodeUnknownOption(Info, decodeOptions) const decodeOptions = { errors: "all", onExcessProperty: "ignore", propertyOrder: "original" } as const
const parseInfo = Effect.fn("Config.parseInfo")(function* (text: string, source: string) { const decodeInfo = Schema.decodeUnknownOption(Info, decodeOptions)
const errors: ParseError[] = [] const parseInfo = Effect.fn("Config.parseInfo")(function* (text: string, source: string) {
const input: unknown = parse(text, errors, { allowTrailingComma: true }) const errors: ParseError[] = []
if (errors.length) { const input: unknown = parse(text, errors, { allowTrailingComma: true })
if (errors.length) {
yield* Effect.logWarning("configuration normalization diagnostic", {
source,
path: "$",
kind: "invalid",
action: "rejected malformed JSON or JSONC document",
})
return
}
const result = ConfigNormalize.normalize(input)
yield* Effect.forEach(result.diagnostics, (diagnostic) =>
Effect.logWarning("configuration normalization diagnostic", {
source,
path: diagnostic.path[0] === "$" ? "$" : `$.${diagnostic.path.join(".")}`,
kind: diagnostic.kind,
action: diagnostic.message,
}),
)
if (result.type === "rejected") return
const info = Option.getOrUndefined(decodeInfo(result.encoded))
if (info) return info
yield* Effect.logWarning("configuration normalization diagnostic", { yield* Effect.logWarning("configuration normalization diagnostic", {
source, source,
path: "$", path: "$",
kind: "invalid", kind: "invalid",
action: "rejected malformed JSON or JSONC document", action: "rejected canonical configuration after final validation",
}) })
return
}
const result = ConfigNormalize.normalize(input)
yield* Effect.forEach(result.diagnostics, (diagnostic) =>
Effect.logWarning("configuration normalization diagnostic", {
source,
path: diagnostic.path[0] === "$" ? "$" : `$.${diagnostic.path.join(".")}`,
kind: diagnostic.kind,
action: diagnostic.message,
}),
)
if (result.type === "rejected") return
const info = Option.getOrUndefined(decodeInfo(result.encoded))
if (info) return info
yield* Effect.logWarning("configuration normalization diagnostic", {
source,
path: "$",
kind: "invalid",
action: "rejected canonical configuration after final validation",
}) })
})
const loadFile = Effect.fnUntraced(function* (filepath: string) { const loadFile = Effect.fnUntraced(function* (filepath: string) {
const text = yield* fs.readFileStringSafe(filepath) const text = yield* fs.readFileStringSafe(filepath)
if (text === undefined) return if (text === undefined) return
const substituted = yield* ConfigVariable.substitute({ type: "path", path: filepath, text }) const substituted = yield* ConfigVariable.substitute({ type: "path", path: filepath, text })
const info = yield* parseInfo(substituted, filepath) const info = yield* parseInfo(substituted, filepath)
if (!info) return if (!info) return
return new Document({ type: "document", path: filepath, info }) return new Document({ type: "document", path: filepath, info })
}) })
const loadWellknown = Effect.fn("Config.loadWellknown")(function* () { const loadWellknown = Effect.fn("Config.loadWellknown")(function* () {
const entries = yield* wellknown const entries = yield* wellknown
.entries() .entries()
.pipe( .pipe(
Effect.catch((error) => Effect.catch((error) =>
Effect.logWarning("failed to discover wellknown config", { error }).pipe(Effect.as([] as const)), Effect.logWarning("failed to discover wellknown config", { error }).pipe(Effect.as([] as const)),
),
)
return yield* Effect.forEach(entries, (entry) =>
Effect.gen(function* () {
const auth = entry.manifest.auth
if (!auth) return []
const credential = (yield* credentials.list(entry.integrationID)).findLast(
(credential) => credential.value.type === "key",
)
if (!credential || credential.value.type !== "key") return []
const variables = { [auth.env]: credential.value.key }
const configs = yield* wellknown.resolve(entry, variables).pipe(Effect.orDie)
return yield* Effect.forEach(configs, (config) =>
ConfigVariable.substitute({
type: "virtual",
source: entry.origin,
dir: entry.origin,
text: JSON.stringify(config),
env: variables,
}).pipe(
Effect.flatMap((text) => parseInfo(text, entry.origin)),
Effect.map((info) => (info ? new Document({ type: "document", info }) : undefined)),
), ),
).pipe(Effect.map((documents) => documents.filter((document) => document !== undefined))) )
}), return yield* Effect.forEach(entries, (entry) =>
).pipe(Effect.map((documents) => documents.flat())) Effect.gen(function* () {
}) const auth = entry.manifest.auth
if (!auth) return []
const credential = (yield* credentials.list(entry.integrationID)).findLast(
(credential) => credential.value.type === "key",
)
if (!credential || credential.value.type !== "key") return []
const variables = { [auth.env]: credential.value.key }
const configs = yield* wellknown.resolve(entry, variables).pipe(Effect.orDie)
return yield* Effect.forEach(configs, (config) =>
ConfigVariable.substitute({
type: "virtual",
source: entry.origin,
dir: entry.origin,
text: JSON.stringify(config),
env: variables,
}).pipe(
Effect.flatMap((text) => parseInfo(text, entry.origin)),
Effect.map((info) => (info ? new Document({ type: "document", info }) : undefined)),
),
).pipe(Effect.map((documents) => documents.filter((document) => document !== undefined)))
}),
).pipe(Effect.map((documents) => documents.flat()))
})
const loadDirectory = Effect.fnUntraced(function* (directory: AbsolutePath) { const loadDirectory = Effect.fnUntraced(function* (directory: AbsolutePath) {
return [ return [
...(yield* Effect.forEach(names, (file) => loadFile(path.join(directory, file))).pipe( ...(yield* Effect.forEach(names, (file) => loadFile(path.join(directory, file))).pipe(
Effect.map((configs) => configs.filter((config): config is Document => config !== undefined)), Effect.map((configs) => configs.filter((config): config is Document => config !== undefined)),
)), )),
new Directory({ type: "directory", path: directory }), new Directory({ type: "directory", path: directory }),
] ]
}) })
const discover = Effect.fn("Config.discover")(function* () { const discover = Effect.fn("Config.discover")(function* () {
const globalDirectory = AbsolutePath.make(global.config) const globalDirectory = AbsolutePath.make(global.config)
const globalAgentsDirectory = AbsolutePath.make(path.join(global.home, ".agents")) const globalAgentsDirectory = AbsolutePath.make(path.join(global.home, ".agents"))
const globalClaudeDirectory = AbsolutePath.make(path.join(global.home, ".claude")) const globalClaudeDirectory = AbsolutePath.make(path.join(global.home, ".claude"))
const locationIsGlobal = path.resolve(location.directory) === path.resolve(global.config) const locationIsGlobal = path.resolve(location.directory) === path.resolve(global.config)
const discovered = locationIsGlobal || options?.project === false const discovered =
? [] locationIsGlobal || options?.project === false
: yield* fs ? []
.up({ : yield* fs
targets: [".opencode", ".claude", ".agents", ...names.toReversed()], .up({
start: location.directory, targets: [".opencode", ".claude", ".agents", ...names.toReversed()],
}) start: location.directory,
.pipe(Effect.orDie) })
.pipe(Effect.orDie)
// We load certain files from a few other folders in the ecosystem // We load certain files from a few other folders in the ecosystem
const claude = [ const claude = [
...new Set([ ...new Set([
...((yield* fs.isDir(globalClaudeDirectory)) ? [globalClaudeDirectory] : []), ...((yield* fs.isDir(globalClaudeDirectory)) ? [globalClaudeDirectory] : []),
...discovered.filter((item) => path.basename(item) === ".claude"), ...discovered.filter((item) => path.basename(item) === ".claude"),
]),
].map((directory) => new ClaudeDirectory({ type: "claude", path: AbsolutePath.make(directory) }))
const agents = [
...new Set([
...((yield* fs.isDir(globalAgentsDirectory)) ? [globalAgentsDirectory] : []),
...discovered.filter((item) => path.basename(item) === ".agents"),
]),
].map((directory) => new AgentsDirectory({ type: "agents", path: AbsolutePath.make(directory) }))
const directories = [
globalDirectory,
...discovered
.filter((item) => path.basename(item) === ".opencode")
.toReversed()
.map((directory) => AbsolutePath.make(directory)),
]
const directPaths = discovered
.filter((item) => ![".agents", ".claude", ".opencode"].includes(path.basename(item)))
.toReversed()
const direct = yield* Effect.forEach(directPaths, (filepath) =>
loadFile(filepath).pipe(
Effect.map((config) => [
...(config ? [config] : []),
new File({ type: "file", path: AbsolutePath.make(filepath) }),
]), ]),
), ].map((directory) => new ClaudeDirectory({ type: "claude", path: AbsolutePath.make(directory) }))
).pipe( const agents = [
Effect.orDie, ...new Set([
Effect.map((entries) => entries.flat()), ...((yield* fs.isDir(globalAgentsDirectory)) ? [globalAgentsDirectory] : []),
) ...discovered.filter((item) => path.basename(item) === ".agents"),
]),
].map((directory) => new AgentsDirectory({ type: "agents", path: AbsolutePath.make(directory) }))
const file = options?.file const directories = [
const explicit = file globalDirectory,
? yield* loadFile(path.resolve(file)).pipe( ...discovered
.filter((item) => path.basename(item) === ".opencode")
.toReversed()
.map((directory) => AbsolutePath.make(directory)),
]
const directPaths = discovered
.filter((item) => ![".agents", ".claude", ".opencode"].includes(path.basename(item)))
.toReversed()
const direct = yield* Effect.forEach(directPaths, (filepath) =>
loadFile(filepath).pipe(
Effect.map((config) => [ Effect.map((config) => [
...(config ? [config] : []), ...(config ? [config] : []),
new File({ type: "file", path: AbsolutePath.make(path.resolve(file)) }), new File({ type: "file", path: AbsolutePath.make(filepath) }),
]), ]),
Effect.orDie, ),
) ).pipe(
: [] Effect.orDie,
const content = options?.content !== undefined Effect.map((entries) => entries.flat()),
? yield* ConfigVariable.substitute({
type: "virtual",
source: "OPENCODE_CONFIG_CONTENT",
dir: location.directory,
text: options.content,
}).pipe(
Effect.flatMap((text) => parseInfo(text, "OPENCODE_CONFIG_CONTENT")),
Effect.map((info) => (info ? [new Document({ type: "document", info })] : [])),
Effect.orDie,
)
: []
const supplementary = yield* Effect.forEach(directories, loadDirectory).pipe(Effect.orDie)
return [
...(yield* loadWellknown().pipe(Effect.orDie)),
...claude,
...agents,
...(supplementary[0] ?? []),
...explicit,
...direct,
...supplementary.slice(1).flat(),
...content,
]
})
const initial = yield* discover()
let configs = initial
const updates = yield* PubSub.unbounded<Watcher.Update>()
// Vendored trees inside config roots (a plugin's node_modules, a nested
// .git) produce event blizzards that can never change discovery output.
const ignore = ["node_modules", ".git", "**/{node_modules,.git}/**"]
// Watch-once: roots leave discovery only by deletion, so a stale watch is
// inert, bounded, and dies with this layer — and keeping a deleted root's
// watch alive is exactly what makes its recreation observable.
const watched = new Set<string>()
const reconcile = Effect.fn("Config.reconcileWatches")(function* (entries: readonly Entry[]) {
const directories = entries.flatMap((entry) => (entry.type === "directory" ? [entry.path] : []))
const files = entries.flatMap((entry) => (entry.type === "file" ? [entry.path] : []))
const targets = [
...directories.map((path) => ({ path, type: "directory" as const, ignore })),
...files
.filter((file) => !directories.some((directory) => FSUtil.contains(directory, file)))
.map((path) => ({ path, type: "file" as const })),
]
for (const target of targets) {
const key = JSON.stringify(target)
if (watched.has(key)) continue
watched.add(key)
const stream = yield* watcher.subscribe(target)
yield* stream.pipe(
Stream.runForEach((update) => PubSub.publish(updates, update)),
Effect.forkScoped({ startImmediately: true }),
) )
}
})
const reload = Effect.fn("Config.reload")(() => const file = options?.file
reloadLock.withPermit( const explicit = file
Effect.gen(function* () { ? yield* loadFile(path.resolve(file)).pipe(
const next = yield* discover() Effect.map((config) => [
if (isDeepStrictEqual(configs, next)) return ...(config ? [config] : []),
configs = next new File({ type: "file", path: AbsolutePath.make(path.resolve(file)) }),
yield* reconcile(next) ]),
yield* bus.publish(Event.Updated, {}) Effect.orDie,
}),
),
)
yield* Stream.fromPubSub(updates).pipe(
Stream.debounce("100 millis"),
Stream.runForEach((update) =>
reload().pipe(
Effect.catchCause((cause) => Effect.logError("failed to reload config", { path: update.path, cause })),
),
),
Effect.forkScoped({ startImmediately: true }),
)
yield* bus.subscribe(Integration.Event.ConnectionUpdated).pipe(
Stream.filterEffect((event) =>
wellknown.entries().pipe(
Effect.map((entries) => entries.some((entry) => entry.integrationID === event.data.integrationID)),
Effect.catch(() => Effect.succeed(false)),
),
),
Stream.runForEach(() =>
reload().pipe(Effect.catchCause((cause) => Effect.logError("failed to reload wellknown config", { cause }))),
),
Effect.forkScoped({ startImmediately: true }),
)
yield* bus.subscribe(WellKnown.Event.Updated).pipe(
Stream.runForEach(() =>
reload().pipe(Effect.catchCause((cause) => Effect.logError("failed to reload wellknown sources", { cause }))),
),
Effect.forkScoped({ startImmediately: true }),
)
yield* Effect.sleep("10 minutes").pipe(
Effect.andThen(
Effect.suspend(() => {
if (!wellknown.snapshot().length) return Effect.void
return Effect.gen(function* () {
const changed = yield* wellknown.refresh().pipe(
Effect.catch((error) =>
Effect.logWarning("failed to refresh wellknown manifests", { error }).pipe(Effect.as(false)),
),
) )
if (!changed) yield* reload() : []
}).pipe(Effect.catchCause((cause) => Effect.logWarning("failed to refresh wellknown config", { cause }))) const content =
}), options?.content !== undefined
), ? yield* ConfigVariable.substitute({
Effect.forever, type: "virtual",
Effect.forkScoped({ startImmediately: true }), source: "OPENCODE_CONFIG_CONTENT",
) dir: location.directory,
yield* reconcile(initial) text: options.content,
}).pipe(
Effect.flatMap((text) => parseInfo(text, "OPENCODE_CONFIG_CONTENT")),
Effect.map((info) => (info ? [new Document({ type: "document", info })] : [])),
Effect.orDie,
)
: []
return Service.of({ const supplementary = yield* Effect.forEach(directories, loadDirectory).pipe(Effect.orDie)
entries: Effect.fn("Config.entries")(function* () { return [
return configs ...(yield* loadWellknown().pipe(Effect.orDie)),
}), ...claude,
changes: () => Stream.fromPubSub(updates), ...agents,
}) ...(supplementary[0] ?? []),
}), ...explicit,
) ...direct,
...supplementary.slice(1).flat(),
...content,
]
})
const initial = yield* discover()
let configs = initial
const updates = yield* PubSub.unbounded<Watcher.Update>()
// Vendored trees inside config roots (a plugin's node_modules, a nested
// .git) produce event blizzards that can never change discovery output.
const ignore = ["node_modules", ".git", "**/{node_modules,.git}/**"]
// Watch-once: roots leave discovery only by deletion, so a stale watch is
// inert, bounded, and dies with this layer — and keeping a deleted root's
// watch alive is exactly what makes its recreation observable.
const watched = new Set<string>()
const reconcile = Effect.fn("Config.reconcileWatches")(function* (entries: readonly Entry[]) {
const directories = entries.flatMap((entry) => (entry.type === "directory" ? [entry.path] : []))
const files = entries.flatMap((entry) => (entry.type === "file" ? [entry.path] : []))
const targets = [
...directories.map((path) => ({ path, type: "directory" as const, ignore })),
...files
.filter((file) => !directories.some((directory) => FSUtil.contains(directory, file)))
.map((path) => ({ path, type: "file" as const })),
]
for (const target of targets) {
const key = JSON.stringify(target)
if (watched.has(key)) continue
watched.add(key)
const stream = yield* watcher.subscribe(target)
yield* stream.pipe(
Stream.runForEach((update) => PubSub.publish(updates, update)),
Effect.forkScoped({ startImmediately: true }),
)
}
})
const reload = Effect.fn("Config.reload")(() =>
reloadLock.withPermit(
Effect.gen(function* () {
const next = yield* discover()
if (isDeepStrictEqual(configs, next)) return
configs = next
yield* reconcile(next)
yield* bus.publish(Event.Updated, {})
}),
),
)
yield* Stream.fromPubSub(updates).pipe(
Stream.debounce("100 millis"),
Stream.runForEach((update) =>
reload().pipe(
Effect.catchCause((cause) => Effect.logError("failed to reload config", { path: update.path, cause })),
),
),
Effect.forkScoped({ startImmediately: true }),
)
yield* bus.subscribe(Integration.Event.ConnectionUpdated).pipe(
Stream.filterEffect((event) =>
wellknown.entries().pipe(
Effect.map((entries) => entries.some((entry) => entry.integrationID === event.data.integrationID)),
Effect.catch(() => Effect.succeed(false)),
),
),
Stream.runForEach(() =>
reload().pipe(Effect.catchCause((cause) => Effect.logError("failed to reload wellknown config", { cause }))),
),
Effect.forkScoped({ startImmediately: true }),
)
yield* bus.subscribe(WellKnown.Event.Updated).pipe(
Stream.runForEach(() =>
reload().pipe(Effect.catchCause((cause) => Effect.logError("failed to reload wellknown sources", { cause }))),
),
Effect.forkScoped({ startImmediately: true }),
)
yield* Effect.sleep("10 minutes").pipe(
Effect.andThen(
Effect.suspend(() => {
if (!wellknown.snapshot().length) return Effect.void
return Effect.gen(function* () {
const changed = yield* wellknown
.refresh()
.pipe(
Effect.catch((error) =>
Effect.logWarning("failed to refresh wellknown manifests", { error }).pipe(Effect.as(false)),
),
)
if (!changed) yield* reload()
}).pipe(Effect.catchCause((cause) => Effect.logWarning("failed to refresh wellknown config", { cause })))
}),
),
Effect.forever,
Effect.forkScoped({ startImmediately: true }),
)
yield* reconcile(initial)
return Service.of({
entries: Effect.fn("Config.entries")(function* () {
return configs
}),
changes: () => Stream.fromPubSub(updates),
})
}),
)
export function configured(options?: Options) { export function configured(options?: Options) {
return makeLocationNode({ return makeLocationNode({
+1 -3
View File
@@ -63,9 +63,7 @@ export const Plugin = define({
Effect.catch(() => Effect.succeed(undefined)), Effect.catch(() => Effect.succeed(undefined)),
), ),
).pipe( ).pipe(
Effect.map((documents) => Effect.map((documents) => documents.filter((document): document is Document => document !== undefined)),
documents.filter((document): document is Document => document !== undefined),
),
) )
}) })
}).pipe(Effect.map((documents) => documents.flat())) }).pipe(Effect.map((documents) => documents.flat()))
+13 -9
View File
@@ -92,9 +92,7 @@ type Options = {
readonly nextDatabasePath?: string readonly nextDatabasePath?: string
} }
type MigrationState = type MigrationState = { readonly phase: "sessions"; readonly cursor?: string } | { readonly phase: "completed" }
| { readonly phase: "sessions"; readonly cursor?: string }
| { readonly phase: "completed" }
type RuntimeState = type RuntimeState =
| { readonly status: "idle" } | { readonly status: "idle" }
@@ -487,7 +485,10 @@ export function run(options: Options = {}): Effect.Effect<RunResult, never, Data
.transaction((tx) => .transaction((tx) =>
Effect.gen(function* () { Effect.gen(function* () {
yield* tx.delete(EventTable).run() yield* tx.delete(EventTable).run()
yield* tx.insert(KVTable).values({ key: MIGRATION_STATE_KEY, value: { phase: "sessions" } }).run() yield* tx
.insert(KVTable)
.values({ key: MIGRATION_STATE_KEY, value: { phase: "sessions" } })
.run()
}), }),
) )
.pipe(Effect.orDie) .pipe(Effect.orDie)
@@ -505,7 +506,9 @@ export function run(options: Options = {}): Effect.Effect<RunResult, never, Data
updateProgress({ label: "Migrating sessions", numerator: migrated + completed, denominator }) updateProgress({ label: "Migrating sessions", numerator: migrated + completed, denominator })
}) })
updateProgress({ label: "Migrating sessions", numerator: migrated + sourceTotal, denominator }) updateProgress({ label: "Migrating sessions", numerator: migrated + sourceTotal, denominator })
const projects = new Set((yield* db.all<{ id: string }>(sql`SELECT id FROM project`)).map((project) => project.id)) const projects = new Set(
(yield* db.all<{ id: string }>(sql`SELECT id FROM project`)).map((project) => project.id),
)
while (true) { while (true) {
const state = yield* readState(db) const state = yield* readState(db)
const cursorValue = state?.phase === "sessions" ? state.cursor : undefined const cursorValue = state?.phase === "sessions" ? state.cursor : undefined
@@ -594,7 +597,7 @@ export function run(options: Options = {}): Effect.Effect<RunResult, never, Data
numerator: (runtimeState.progress.numerator ?? 0) + 1, numerator: (runtimeState.progress.numerator ?? 0) + 1,
denominator, denominator,
}, },
} }
yield* Effect.yieldNow yield* Effect.yieldNow
} }
yield* db yield* db
@@ -681,9 +684,10 @@ function importNextDatabase(
}) })
} }
const messages = source const messages = source
.query<NextMessage, [string]>( .query<
"SELECT id, session_id, type, seq, time_created, time_updated, data FROM session_message WHERE session_id = ? ORDER BY seq", NextMessage,
) [string]
>("SELECT id, session_id, type, seq, time_created, time_updated, data FROM session_message WHERE session_id = ? ORDER BY seq")
.all(session.id) .all(session.id)
yield* db yield* db
.transaction((tx) => .transaction((tx) =>
+1 -6
View File
@@ -4,12 +4,7 @@ import { Effect, Layer } from "effect"
import { makeGlobalNode } from "@opencode-ai/util/effect/app-node" import { makeGlobalNode } from "@opencode-ai/util/effect/app-node"
import { Bus } from "./bus" import { Bus } from "./bus"
const Types = new Set([ const Types = new Set(["agent.updated", "catalog.updated", "command.updated", "config.updated"])
"agent.updated",
"catalog.updated",
"command.updated",
"config.updated",
])
export const layer = Layer.effectDiscard( export const layer = Layer.effectDiscard(
Effect.gen(function* () { Effect.gen(function* () {
+1 -3
View File
@@ -92,9 +92,7 @@ export const layer = (options?: Options) =>
const watchers = yield* RcMap.make({ const watchers = yield* RcMap.make({
lookup: (key: Key) => lookup: (key: Key) =>
Effect.gen(function* () { Effect.gen(function* () {
const pubsub = yield* Effect.acquireRelease(PubSub.unbounded<Update>(), (pubsub) => const pubsub = yield* Effect.acquireRelease(PubSub.unbounded<Update>(), (pubsub) => PubSub.shutdown(pubsub))
PubSub.shutdown(pubsub),
)
const subscription = yield* Effect.acquireRelease( const subscription = yield* Effect.acquireRelease(
native.subscribe({ native.subscribe({
type: key.type, type: key.type,
+5 -1
View File
@@ -183,7 +183,11 @@ export const layer = Layer.effect(
const invalid = validateAnswer(entry.form, input.answer) const invalid = validateAnswer(entry.form, input.answer)
if (invalid) return yield* new InvalidAnswerError({ id: input.id, message: invalid }) if (invalid) return yield* new InvalidAnswerError({ id: input.id, message: invalid })
const next: TerminalState = { status: "answered", answer: input.answer } const next: TerminalState = { status: "answered", answer: input.answer }
yield* bus.publish(Form.Event.Replied, { id: input.id, sessionID: entry.form.sessionID, answer: input.answer }) yield* bus.publish(Form.Event.Replied, {
id: input.id,
sessionID: entry.form.sessionID,
answer: input.answer,
})
yield* Cache.set(forms, input.id, { ...entry, state: next }) yield* Cache.set(forms, input.id, { ...entry, state: next })
yield* Deferred.succeed(entry.deferred, next) yield* Deferred.succeed(entry.deferred, next)
}), }),

Some files were not shown because too many files have changed in this diff Show More