Compare commits

..
Author SHA1 Message Date
Kit Langton cbbb83ec1c refactor(core): reuse Markdown chunk byte counts 2026-08-28 23:51:02 -04:00
290 changed files with 1584 additions and 11915 deletions
+1 -3
View File
@@ -5,7 +5,6 @@ on:
branches:
- dev
- production
- beta
workflow_dispatch:
concurrency: ${{ github.workflow }}-${{ github.ref }}
@@ -16,7 +15,7 @@ permissions:
jobs:
deploy:
if: github.repository == 'anomalyco/opencode' && (github.ref_name == 'dev' || github.ref_name == 'production' || github.ref_name == 'beta')
if: github.repository == 'anomalyco/opencode' && (github.ref_name == 'dev' || github.ref_name == 'production')
runs-on: ubuntu-latest
environment: ${{ github.ref_name }}
steps:
@@ -29,7 +28,6 @@ jobs:
node-version: "24"
- uses: aws-actions/configure-aws-credentials@7474bc4690e29a8392af63c5b98e7449536d5c3a # v4.3.1
if: github.ref_name != 'beta'
with:
role-to-assume: ${{ vars.AWS_DEPLOY_ROLE_ARN }}
role-session-name: opencode-${{ github.run_id }}
-1
View File
@@ -46,7 +46,6 @@ Examples: `fix(tui): simplify thinking toggle styling`, `docs: update contributi
### General Principles
- Keep things in one function unless composable or reusable
- Validate unknown values once at the boundary that owns them. Pass typed values inward instead of repeating `typeof value === "object"` and property-existence checks. Do not defensively revalidate values already guaranteed by a schema, constructor, or internal type.
- Do not extract single-use helpers preemptively. Inline the logic at the call site unless the helper is reused, hides a genuinely complex boundary, or has a clear independent name that improves the caller.
- Before adding complexity for a speculative or vanishingly unlikely race or security edge case, explain the concrete failure mode, likelihood, and complexity cost to the user and get their buy-in. Do not silently expand scope for theoretical robustness.
- Avoid `try`/`catch` where possible
+33 -57
View File
@@ -183,7 +183,6 @@
"@typescript/native-preview": "catalog:",
"effect": "catalog:",
"solid-js": "catalog:",
"zod": "catalog:",
},
"peerDependencies": {
"effect": "4.0.0-rc.112",
@@ -431,7 +430,6 @@
"@types/bun": "catalog:",
"@types/node": "catalog:",
"@typescript/native-preview": "catalog:",
"app-builder-lib": "26.15.7",
"drizzle-orm": "catalog:",
"effect": "catalog:",
"electron": "42.10.1",
@@ -1680,7 +1678,7 @@
"@electron/fuses": ["@electron/fuses@1.8.0", "", { "dependencies": { "chalk": "^4.1.1", "fs-extra": "^9.0.1", "minimist": "^1.2.5" }, "bin": { "electron-fuses": "dist/bin.js" } }, "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw=="],
"@electron/get": ["@electron/get@3.1.0", "", { "dependencies": { "debug": "^4.1.1", "env-paths": "^2.2.0", "fs-extra": "^8.1.0", "got": "^11.8.5", "progress": "^2.0.3", "semver": "^6.2.0", "sumchecker": "^3.0.1" }, "optionalDependencies": { "global-agent": "^3.0.0" } }, "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ=="],
"@electron/get": ["@electron/get@5.1.0", "", { "dependencies": { "debug": "^4.1.1", "env-paths": "^3.0.0", "graceful-fs": "^4.2.11", "progress": "^2.0.3", "semver": "^7.6.3", "sumchecker": "^3.0.1" }, "optionalDependencies": { "undici": "^7.24.4" } }, "sha512-3kSBtG8ObcTVfXanm5vVJ6UnBLEVmVsRk1M+vGqCuMBV+XLCbJYuWQful+yIy0GQDsSlK0kHEriEHn7SPk4EnA=="],
"@electron/notarize": ["@electron/notarize@2.5.0", "", { "dependencies": { "debug": "^4.1.1", "fs-extra": "^9.0.1", "promise-retry": "^2.0.1" } }, "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A=="],
@@ -2776,7 +2774,7 @@
"@silvia-odwyer/photon-node": ["@silvia-odwyer/photon-node@0.3.4", "", {}, "sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA=="],
"@sindresorhus/is": ["@sindresorhus/is@4.6.0", "", {}, "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw=="],
"@sindresorhus/is": ["@sindresorhus/is@7.2.0", "", {}, "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw=="],
"@smithy/config-resolver": ["@smithy/config-resolver@4.7.2", "", { "dependencies": { "@smithy/core": "^3.33.2", "tslib": "^2.6.2" } }, "sha512-Y1XfSefHIOub9762qm3ShafdlEE/Va8h3kLUeMq765fNeWeNLcOP2YUPr86H1SlyGwZTOqQ67RlBZPZ3k9Djgg=="],
@@ -3534,7 +3532,7 @@
"chromium-pickle-js": ["chromium-pickle-js@0.2.0", "", {}, "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw=="],
"ci-info": ["ci-info@4.3.1", "", {}, "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA=="],
"ci-info": ["ci-info@4.4.0", "", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="],
"classnames": ["classnames@2.3.2", "", {}, "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw=="],
@@ -3814,7 +3812,7 @@
"dot-prop": ["dot-prop@8.0.2", "", { "dependencies": { "type-fest": "^3.8.0" } }, "sha512-xaBe6ZT4DHPkg0k4Ytbvn5xoxgpG0jOS1dYxSOwAHPuNLjP3/OzN0gH55SrLqpx8cBfSaVt91lXYkApjb+nYdQ=="],
"dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
"dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="],
"dotenv-expand": ["dotenv-expand@11.0.7", "", { "dependencies": { "dotenv": "^16.4.5" } }, "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA=="],
@@ -3886,7 +3884,7 @@
"entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
"env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="],
"env-paths": ["env-paths@3.0.0", "", {}, "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A=="],
"err-code": ["err-code@2.0.3", "", {}, "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA=="],
@@ -4208,7 +4206,7 @@
"hono-openapi": ["hono-openapi@1.1.2", "", { "peerDependencies": { "@hono/standard-validator": "^0.2.0", "@standard-community/standard-json": "^0.3.5", "@standard-community/standard-openapi": "^0.2.9", "@types/json-schema": "^7.0.15", "hono": "^4.8.3", "openapi-types": "^12.1.3" }, "optionalPeers": ["@hono/standard-validator", "hono"] }, "sha512-toUcO60MftRBxqcVyxsHNYs2m4vf4xkQaiARAucQx3TiBPDtMNNkoh+C4I1vAretQZiGyaLOZNWn1YxfSyUA5g=="],
"hosted-git-info": ["hosted-git-info@4.1.0", "", { "dependencies": { "lru-cache": "^6.0.0" } }, "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA=="],
"hosted-git-info": ["hosted-git-info@9.0.3", "", { "dependencies": { "lru-cache": "^11.1.0" } }, "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg=="],
"html-entities": ["html-entities@2.3.3", "", {}, "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA=="],
@@ -5814,7 +5812,7 @@
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
"yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
"yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
"yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="],
@@ -5962,8 +5960,6 @@
"@astrojs/starlight/js-yaml": ["js-yaml@4.3.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ=="],
"@astrojs/telemetry/ci-info": ["ci-info@4.4.0", "", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="],
"@astrojs/telemetry/is-docker": ["is-docker@4.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-LHE+wROyG/Y/0ZnbktRCoTix2c1RhgWaZraMZ8o1Q7zCh0VSrICJQO5oqIIISrcSBtrXv0o233w1IYwsWCjTzA=="],
"@aws-crypto/crc32/@aws-sdk/types": ["@aws-sdk/types@3.974.4", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-dSFDNG00MEz0/xl5gxL62giLd1iYyJsTxZ1I1DOj6lC+bbgLB4TRsYClJg3b62dhXT1uATzsTNXPnC+33EJV3A=="],
@@ -6118,9 +6114,7 @@
"@electron/fuses/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="],
"@electron/get/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="],
"@electron/get/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"@electron/get/undici": ["undici@7.29.0", "", {}, "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw=="],
"@electron/notarize/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="],
@@ -6156,12 +6150,6 @@
"@modelcontextprotocol/sdk/jose": ["jose@6.2.9", "", {}, "sha512-XrchZOFZUl/T3vTwRe8XK+cJrGtMF4th1ARnDfwbBXFKThGhlsxEE4Zu03AD/bjJSt/9jT/mxrOCkJWOg77aPA=="],
"@npmcli/arborist/hosted-git-info": ["hosted-git-info@9.0.3", "", { "dependencies": { "lru-cache": "^11.1.0" } }, "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg=="],
"@npmcli/config/ci-info": ["ci-info@4.4.0", "", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="],
"@npmcli/package-json/hosted-git-info": ["hosted-git-info@9.0.3", "", { "dependencies": { "lru-cache": "^11.1.0" } }, "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg=="],
"@npmcli/query/postcss-selector-parser": ["postcss-selector-parser@7.1.5", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw=="],
"@octokit/auth-app/@octokit/request": ["@octokit/request@10.0.14", "", { "dependencies": { "@octokit/endpoint": "^11.0.3", "@octokit/request-error": "^7.1.1", "@octokit/types": "^17.0.0", "content-type": "^2.0.0", "json-with-bigint": "^3.5.12", "universal-user-agent": "^7.0.2" } }, "sha512-bgWgiSfFS689/AxQDarU+b3Qu1FYewfVr5vI/jhV84s7GQ3C2OsdRxukGGYKB37MCgwcS50UrfBLlHzhiG13sw=="],
@@ -6302,16 +6290,12 @@
"@pierre/trees/react-dom": ["react-dom@19.2.8", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.8" } }, "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ=="],
"@poppinss/dumper/@sindresorhus/is": ["@sindresorhus/is@7.2.0", "", {}, "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw=="],
"@poppinss/dumper/supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="],
"@protobuf-ts/plugin/typescript": ["typescript@3.9.10", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q=="],
"@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
"@sentry/bundler-plugins/dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="],
"@sentry/bundler-plugins/glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="],
"@sentry/cli/https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="],
@@ -6384,6 +6368,14 @@
"anymatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
"app-builder-lib/@electron/get": ["@electron/get@3.1.0", "", { "dependencies": { "debug": "^4.1.1", "env-paths": "^2.2.0", "fs-extra": "^8.1.0", "got": "^11.8.5", "progress": "^2.0.3", "semver": "^6.2.0", "sumchecker": "^3.0.1" }, "optionalDependencies": { "global-agent": "^3.0.0" } }, "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ=="],
"app-builder-lib/ci-info": ["ci-info@4.3.1", "", {}, "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA=="],
"app-builder-lib/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
"app-builder-lib/hosted-git-info": ["hosted-git-info@4.1.0", "", { "dependencies": { "lru-cache": "^6.0.0" } }, "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA=="],
"app-builder-lib/js-yaml": ["js-yaml@4.3.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ=="],
"app-builder-lib/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
@@ -6394,8 +6386,6 @@
"astro/@clack/prompts": ["@clack/prompts@1.7.0", "", { "dependencies": { "@clack/core": "1.4.3", "fast-string-width": "^3.0.2", "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-y7/yvZ2TPAnR9+jnc00klvNNLkJiXFFrQA/hlLCcxA9a2A4zQIOimyFQ9XfwYKiGD1fb5GY8vbKIIgO8d5Tb2A=="],
"astro/ci-info": ["ci-info@4.4.0", "", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="],
"astro/diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="],
"astro/es-module-lexer": ["es-module-lexer@2.3.2", "", {}, "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw=="],
@@ -6440,8 +6430,6 @@
"conf/dot-prop": ["dot-prop@10.2.0", "", { "dependencies": { "type-fest": "^5.0.0" } }, "sha512-BTJ9aZYL3vCfZlZOBLy9v8TUqWGQ0pzFnygKwFZt5udj6viBoFIBviKPUoZLDCPn1FoXffv6McQFDenrm5Krfw=="],
"conf/env-paths": ["env-paths@3.0.0", "", {}, "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A=="],
"config-chain/ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="],
"cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
@@ -6468,16 +6456,14 @@
"dot-prop/type-fest": ["type-fest@3.13.1", "", {}, "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g=="],
"dotenv-expand/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
"duplexer2/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
"editorconfig/commander": ["commander@10.0.1", "", {}, "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug=="],
"editorconfig/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="],
"electron/@electron/get": ["@electron/get@5.1.0", "", { "dependencies": { "debug": "^4.1.1", "env-paths": "^3.0.0", "graceful-fs": "^4.2.11", "progress": "^2.0.3", "semver": "^7.6.3", "sumchecker": "^3.0.1" }, "optionalDependencies": { "undici": "^7.24.4" } }, "sha512-3kSBtG8ObcTVfXanm5vVJ6UnBLEVmVsRk1M+vGqCuMBV+XLCbJYuWQful+yIy0GQDsSlK0kHEriEHn7SPk4EnA=="],
"electron-builder/ci-info": ["ci-info@4.4.0", "", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="],
"electron-publish/mime": ["mime@2.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg=="],
"electron-store/type-fest": ["type-fest@5.8.0", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-YGYEVz3Fm5iy/AybuA0oyNFq7H4CgQNfRp/qfe8nurE1kuCeNm3/vfm9X4Mtl+qLyaKJUh5xrFZwogr41SMjYA=="],
@@ -6524,7 +6510,7 @@
"globby/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
"hosted-git-info/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="],
"got/@sindresorhus/is": ["@sindresorhus/is@4.6.0", "", {}, "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw=="],
"html-minifier-terser/commander": ["commander@10.0.1", "", {}, "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug=="],
@@ -6570,9 +6556,9 @@
"nitro/undici": ["undici@7.29.0", "", {}, "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw=="],
"node-gyp/undici": ["undici@6.28.0", "", {}, "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA=="],
"node-gyp/env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="],
"npm-package-arg/hosted-git-info": ["hosted-git-info@9.0.3", "", { "dependencies": { "lru-cache": "^11.1.0" } }, "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg=="],
"node-gyp/undici": ["undici@6.28.0", "", {}, "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA=="],
"openid-client/jose": ["jose@4.15.9", "", {}, "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA=="],
@@ -6656,6 +6642,8 @@
"svgo/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="],
"tar/yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
"temp/rimraf": ["rimraf@2.6.3", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "./bin.js" } }, "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA=="],
"tempy/type-fest": ["type-fest@0.16.0", "", {}, "sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg=="],
@@ -6798,8 +6786,6 @@
"@astrojs/starlight/astro/acorn": ["acorn@8.18.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="],
"@astrojs/starlight/astro/ci-info": ["ci-info@4.4.0", "", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="],
"@astrojs/starlight/astro/common-ancestor-path": ["common-ancestor-path@1.0.1", "", {}, "sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w=="],
"@astrojs/starlight/astro/cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
@@ -6922,8 +6908,6 @@
"@electron/fuses/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="],
"@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="],
"@electron/notarize/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="],
"@electron/universal/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="],
@@ -7146,8 +7130,6 @@
"@opencode-ai/web/astro/acorn": ["acorn@8.18.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="],
"@opencode-ai/web/astro/ci-info": ["ci-info@4.4.0", "", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="],
"@opencode-ai/web/astro/common-ancestor-path": ["common-ancestor-path@1.0.1", "", {}, "sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w=="],
"@opencode-ai/web/astro/cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
@@ -7252,6 +7234,14 @@
"ansi-align/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"app-builder-lib/@electron/get/env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="],
"app-builder-lib/@electron/get/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="],
"app-builder-lib/@electron/get/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"app-builder-lib/hosted-git-info/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="],
"app-builder-lib/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
"app-builder-lib/which/isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="],
@@ -7270,8 +7260,6 @@
"astro-expressive-code/astro/acorn": ["acorn@8.18.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="],
"astro-expressive-code/astro/ci-info": ["ci-info@4.4.0", "", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="],
"astro-expressive-code/astro/common-ancestor-path": ["common-ancestor-path@1.0.1", "", {}, "sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w=="],
"astro-expressive-code/astro/cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
@@ -7406,10 +7394,6 @@
"electron-winstaller/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="],
"electron/@electron/get/env-paths": ["env-paths@3.0.0", "", {}, "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A=="],
"electron/@electron/get/undici": ["undici@7.29.0", "", {}, "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw=="],
"esbuild-plugin-copy/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
"filelist/minimatch/brace-expansion": ["brace-expansion@2.1.4", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg=="],
@@ -7418,8 +7402,6 @@
"gcp-metadata/gaxios/node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
"hosted-git-info/lru-cache/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
"js-beautify/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
"js-beautify/glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="],
@@ -7468,12 +7450,6 @@
"miniflare/sharp/@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.33.5", "", { "os": "win32", "cpu": "x64" }, "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg=="],
"minipass-flush/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
"minipass-pipeline/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
"openid-client/lru-cache/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
"p-locate/p-limit/yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
"pkg-dir/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="],
@@ -7500,8 +7476,6 @@
"toolbeam-docs-theme/astro/acorn": ["acorn@8.18.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="],
"toolbeam-docs-theme/astro/ci-info": ["ci-info@4.4.0", "", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="],
"toolbeam-docs-theme/astro/common-ancestor-path": ["common-ancestor-path@1.0.1", "", {}, "sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w=="],
"toolbeam-docs-theme/astro/cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
@@ -8180,6 +8154,8 @@
"ansi-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"app-builder-lib/@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="],
"archiver-utils/glob/jackspeak/@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="],
"archiver-utils/glob/minimatch/brace-expansion": ["brace-expansion@2.1.4", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg=="],
+9 -2
View File
@@ -1,5 +1,4 @@
import { domain } from "./stage"
import { createWebApp } from "./webapp"
const GITHUB_APP_ID = new sst.Secret("GITHUB_APP_ID")
const GITHUB_APP_PRIVATE_KEY = new sst.Secret("GITHUB_APP_PRIVATE_KEY")
@@ -60,4 +59,12 @@ new sst.cloudflare.x.Astro("Web", {
},
})
createWebApp("app." + domain)
new sst.cloudflare.StaticSite("WebApp", {
domain: "app." + domain,
path: "packages/app",
build: {
// Preserve Sentry credentials and run source-map uploads on every deployment.
command: "bun run build",
output: "./dist",
},
})
-18
View File
@@ -1,18 +0,0 @@
export function createWebApp(domain: string) {
return new sst.cloudflare.StaticSite("WebApp", {
domain,
path: "packages/app",
environment:
$app.stage === "beta"
? {
OPENCODE_CHANNEL: "beta",
VITE_SENTRY_ENVIRONMENT: "beta",
}
: undefined,
build: {
// Preserve Sentry credentials and run source-map uploads on every deployment.
command: "bun run build",
output: "./dist",
},
})
}
+4 -4
View File
@@ -1,8 +1,8 @@
{
"nodeModules": {
"x86_64-linux": "sha256-fG6VYtNC0pce4VM9po7vVucPuJul42yuuijTjNSr7rk=",
"aarch64-linux": "sha256-3TznrmNqdt25cOxia6vcdi/5qKaeyLPIsNXGYBSJNrs=",
"aarch64-darwin": "sha256-8Kmagb5tfECSWZNsIJgrRP1d3X5tuEoWLEWkV3UENZo=",
"x86_64-darwin": "sha256-mIV+mDwIGD02BNYZVi37sY4ls1T01N6z76eBtH0sKiA="
"x86_64-linux": "sha256-No3mCuG2tGQauX1HUpO+rebiWh+rrpSHUCC7rtXFu1s=",
"aarch64-linux": "sha256-8joWv1iDkc6TejEukGBEX0wW8DPs55wJEPY7+9+HDdM=",
"aarch64-darwin": "sha256-4MWGFQUIP1Ae4dujztTb4G8/uDJx2wiOoypcPSUWDbw=",
"x86_64-darwin": "sha256-BtvnraCJmVagtA3Iv+EbWodjFG74sTd2Purqgo7Wkr4="
}
}
+15 -36
View File
@@ -831,7 +831,6 @@ const lowerMessages = Effect.fn("AnthropicMessages.lowerMessages")(function* (
const content: AnthropicUserBlock[] = []
for (const part of message.content) {
if (part.type === "text") {
if (part.text.trim().length === 0) continue
content.push({ type: "text", text: part.text, cache_control: cacheControl(breakpoints, part.cache) })
continue
}
@@ -841,7 +840,7 @@ const lowerMessages = Effect.fn("AnthropicMessages.lowerMessages")(function* (
}
return yield* ProviderShared.unsupportedContent("Anthropic Messages", "user", ["text", "media"])
}
if (content.length > 0) messages.push({ role: "user", content })
messages.push({ role: "user", content })
continue
}
@@ -849,7 +848,6 @@ const lowerMessages = Effect.fn("AnthropicMessages.lowerMessages")(function* (
const content: AnthropicAssistantBlock[] = []
for (const part of message.content) {
if (part.type === "text") {
if (part.text.trim().length === 0) continue
content.push({ type: "text", text: part.text, cache_control: cacheControl(breakpoints, part.cache) })
continue
}
@@ -893,7 +891,7 @@ const lowerMessages = Effect.fn("AnthropicMessages.lowerMessages")(function* (
`Anthropic Messages assistant messages only support text, reasoning, and tool-call content for now`,
)
}
if (content.length > 0) messages.push({ role: "assistant", content })
messages.push({ role: "assistant", content })
continue
}
@@ -1021,11 +1019,10 @@ const fromRequest = Effect.fn("AnthropicMessages.fromRequest")(function* (reques
)
// Anthropic rejects tool_choice when tools are absent; "none" is only meaningful with tools present.
const toolChoice = tools === undefined || !request.toolChoice ? undefined : yield* lowerToolChoice(request.toolChoice)
const systemParts = request.system.filter((part) => part.text.length > 0)
const system =
systemParts.length === 0
request.system.length === 0
? undefined
: systemParts.map((part) => ({
: request.system.map((part) => ({
type: "text" as const,
text: part.text,
cache_control: cacheControl(breakpoints, part.cache),
@@ -1346,28 +1343,20 @@ const onMessageDelta = (
event: AnthropicEvent & { readonly delta?: AnthropicStreamDelta },
): StepResult => {
const usage = mergeUsage(state.usage, mapUsage(event.usage, state.providerMetadataKey), state.providerMetadataKey)
const pendingFinish = (() => {
const stopReason = event.delta?.stop_reason
if (stopReason === null || stopReason === undefined) return state.pendingFinish
const stopSequence = event.delta?.stop_sequence
const finishMetadata =
stopSequence === null || stopSequence === undefined
? state.pendingFinish?.providerMetadata
: providerMetadata(state.providerMetadataKey, { stopSequence })
return {
reason: {
normalized: mapFinishReason(stopReason),
raw: stopReason,
},
providerMetadata: finishMetadata,
}
})()
return [
{
...state,
usage,
pendingFinish,
pendingFinish: {
reason: {
normalized: mapFinishReason(event.delta?.stop_reason),
raw: event.delta?.stop_reason ?? undefined,
},
providerMetadata:
event.delta?.stop_sequence === null || event.delta?.stop_sequence === undefined
? undefined
: providerMetadata(state.providerMetadataKey, { stopSequence: event.delta.stop_sequence }),
},
},
NO_EVENTS,
]
@@ -1378,17 +1367,7 @@ const onMessageStop = Effect.fn("AnthropicMessages.onMessageStop")(function* (st
const events: LLMEvent[] = []
const lifecycle = result.events.length ? Lifecycle.stepStart(state.lifecycle, events) : state.lifecycle
events.push(...result.events)
const closed = Object.entries(state.reasoningSignatures).reduce(
(current, [index, signature]) =>
Lifecycle.reasoningEnd(
current,
events,
`reasoning-${index}`,
providerMetadata(state.providerMetadataKey, { signature }),
),
lifecycle,
)
const finished = Lifecycle.finish(closed, events, {
const finished = Lifecycle.finish(lifecycle, events, {
reason: state.pendingFinish?.reason ?? {
normalized: "unknown",
raw: undefined,
+73 -138
View File
@@ -1,4 +1,4 @@
import { Effect, Encoding, Schema } from "effect"
import { Effect, Schema } from "effect"
import { Route } from "../route/client.js"
import { Endpoint } from "../route/endpoint.js"
import { Protocol } from "../route/protocol.js"
@@ -262,28 +262,24 @@ const providerMetadata = (key: string, metadata: Record<string, unknown>): Provi
const reasoningSignature = (part: ReasoningPart, providerMetadataKey: string) => {
const metadata = part.providerMetadata?.[providerMetadataKey]
if (part.encrypted !== undefined) return part.encrypted
if (ProviderShared.isRecord(metadata) && typeof metadata.signature === "string") return metadata.signature
return (
part.encrypted ??
(ProviderShared.isRecord(metadata) && typeof metadata.signature === "string" ? metadata.signature : undefined)
)
}
const reasoningRedactedData = (part: ReasoningPart, providerMetadataKey: string) => {
const metadata = part.providerMetadata?.[providerMetadataKey]
if (ProviderShared.isRecord(metadata) && typeof metadata.redactedData === "string") return metadata.redactedData
}
const removeEmptyToolInputKeys = (input: unknown): unknown => {
if (Array.isArray(input)) return input.map(removeEmptyToolInputKeys)
if (!ProviderShared.isRecord(input)) return input
return Object.fromEntries(
Object.entries(input).flatMap(([key, value]) => (key === "" ? [] : [[key, removeEmptyToolInputKeys(value)]])),
)
return ProviderShared.isRecord(metadata) && typeof metadata.redactedData === "string"
? metadata.redactedData
: undefined
}
const lowerToolCall = (part: ToolCallPart): BedrockToolUseBlock => ({
toolUse: {
toolUseId: part.id,
name: part.name,
input: removeEmptyToolInputKeys(part.input),
input: part.input,
},
})
@@ -418,12 +414,7 @@ const lowerMessages = Effect.fn("BedrockConverse.lowerMessages")(function* (
const lowerSystem = (
breakpoints: BedrockCache.Breakpoints,
system: ReadonlyArray<LLMRequest["system"][number]>,
) => {
const content = system
.filter((part) => part.text.length > 0)
.flatMap((part) => textWithCache(breakpoints, part.text, part.cache))
return content.length === 0 ? undefined : content
}
): BedrockSystemBlock[] => system.flatMap((part) => textWithCache(breakpoints, part.text, part.cache))
const fromRequest = Effect.fn("BedrockConverse.fromRequest")(function* (request: LLMRequest) {
const toolChoice = request.toolChoice ? yield* lowerToolChoice(request.toolChoice) : undefined
@@ -431,42 +422,38 @@ const fromRequest = Effect.fn("BedrockConverse.fromRequest")(function* (request:
// Bedrock-Claude shares Anthropic's 4-breakpoint cap. Spend the budget in
// tools → system → messages order to favour the highest-impact prefixes.
const breakpoints = BedrockCache.breakpoints()
const toolConfig = (() => {
if (request.tools.length === 0) return undefined
return {
tools: lowerTools(request.model.compatibility?.toolSchema, breakpoints, request.tools),
// Converse has no native "none". Keep definitions stable for prompt
// caching and omit only the unsupported choice.
toolChoice,
}
})()
const system = lowerSystem(breakpoints, request.system)
const toolConfig =
request.tools.length > 0
? {
tools: lowerTools(request.model.compatibility?.toolSchema, breakpoints, request.tools),
// Converse has no native "none". Keep definitions stable for prompt
// caching and omit only the unsupported choice.
toolChoice,
}
: undefined
const system = request.system.length === 0 ? undefined : lowerSystem(breakpoints, request.system)
const messages = yield* lowerMessages(request, breakpoints)
if (breakpoints.dropped > 0) {
yield* Effect.logWarning(
`Bedrock Converse: dropped ${breakpoints.dropped} cache breakpoint(s); the API allows at most ${BedrockCache.BEDROCK_BREAKPOINT_CAP} per request.`,
)
}
const inferenceConfig = (() => {
if (
generation?.maxTokens === undefined &&
generation?.temperature === undefined &&
generation?.topP === undefined &&
(generation?.stop === undefined || generation.stop.length === 0)
)
return undefined
return {
maxTokens: generation?.maxTokens,
temperature: generation?.temperature,
topP: generation?.topP,
stopSequences: generation?.stop,
}
})()
return {
modelId: request.model.id,
messages,
system,
inferenceConfig,
inferenceConfig:
generation?.maxTokens === undefined &&
generation?.temperature === undefined &&
generation?.topP === undefined &&
(generation?.stop === undefined || generation.stop.length === 0)
? undefined
: {
maxTokens: generation?.maxTokens,
temperature: generation?.temperature,
topP: generation?.topP,
stopSequences: generation?.stop,
},
toolConfig,
// Converse's base inferenceConfig has no topK; Anthropic/Nova accept it
// as a model-specific field, so it goes through additionalModelRequestFields.
@@ -482,6 +469,7 @@ const mapFinishReason = (reason: string): FinishReason => {
if (reason === "max_tokens" || reason === "model_context_window_exceeded") return "length"
if (reason === "tool_use") return "tool-calls"
if (reason === "content_filtered" || reason === "guardrail_intervened") return "content-filter"
if (reason === "malformed_model_output" || reason === "malformed_tool_use") return "error"
return "unknown"
}
@@ -516,16 +504,6 @@ interface ParserState {
readonly hasToolCalls: boolean
readonly lifecycle: Lifecycle.State
readonly reasoningSignatures: Readonly<Record<number, string>>
readonly reasoningRedactedContent: Readonly<Record<number, ReadonlyArray<Uint8Array>>>
}
const encodeRedactedContent = (chunks: ReadonlyArray<Uint8Array>) => {
const bytes = new Uint8Array(chunks.reduce((total, chunk) => total + chunk.length, 0))
chunks.reduce((offset, chunk) => {
bytes.set(chunk, offset)
return offset + chunk.length
}, 0)
return Encoding.encodeBase64(bytes)
}
const step = (state: ParserState, event: BedrockEvent) =>
@@ -573,46 +551,23 @@ const step = (state: ParserState, event: BedrockEvent) =>
const index = event.contentBlockDelta.contentBlockIndex
const reasoning = event.contentBlockDelta.delta.reasoningContent
const events: LLMEvent[] = []
const redactedChunks = yield* (() => {
if (reasoning.redactedContent === undefined) return Effect.succeed(undefined)
return Effect.fromResult(Encoding.decodeBase64(reasoning.redactedContent)).pipe(
Effect.map((chunk) => [...(state.reasoningRedactedContent[index] ?? []), chunk]),
Effect.mapError((cause) =>
ProviderShared.eventError(
ADAPTER,
"Bedrock Converse reasoningContent.redactedContent contains invalid base64 data",
undefined,
cause,
),
),
)
})()
const redactedData = redactedChunks === undefined ? reasoning.data : encodeRedactedContent(redactedChunks)
const metadata = (() => {
if (reasoning.signature) return providerMetadata(state.providerMetadataKey, { signature: reasoning.signature })
if (redactedData !== undefined) return providerMetadata(state.providerMetadataKey, { redactedData })
})()
const lifecycle = (() => {
if (reasoning.text === undefined && metadata === undefined) return state.lifecycle
return Lifecycle.reasoningDelta(state.lifecycle, events, `reasoning-${index}`, reasoning.text ?? "", metadata)
})()
const reasoningRedactedContent = (() => {
if (redactedChunks !== undefined) return { ...state.reasoningRedactedContent, [index]: redactedChunks }
if (reasoning.data === undefined) return state.reasoningRedactedContent
return Object.fromEntries(
Object.entries(state.reasoningRedactedContent).filter(([key]) => key !== String(index)),
)
})()
const reasoningSignatures = (() => {
if (!reasoning.signature) return state.reasoningSignatures
return { ...state.reasoningSignatures, [index]: reasoning.signature }
})()
const redactedData = reasoning.redactedContent ?? reasoning.data
const metadata = reasoning.signature
? providerMetadata(state.providerMetadataKey, { signature: reasoning.signature })
: redactedData !== undefined
? providerMetadata(state.providerMetadataKey, { redactedData })
: undefined
const lifecycle =
reasoning.text !== undefined || metadata !== undefined
? Lifecycle.reasoningDelta(state.lifecycle, events, `reasoning-${index}`, reasoning.text ?? "", metadata)
: state.lifecycle
return [
{
...state,
lifecycle,
reasoningSignatures,
reasoningRedactedContent,
reasoningSignatures: reasoning.signature
? { ...state.reasoningSignatures, [index]: reasoning.signature }
: state.reasoningSignatures,
},
events,
] as const
@@ -640,24 +595,16 @@ const step = (state: ParserState, event: BedrockEvent) =>
const result = yield* ToolStream.finish(ADAPTER, state.tools, index)
const events: LLMEvent[] = []
const resultEvents = result.events ?? []
const lifecycle = (() => {
if (resultEvents.length) return Lifecycle.stepStart(state.lifecycle, events)
const metadata = (() => {
const signature = state.reasoningSignatures[index]
if (signature) return providerMetadata(state.providerMetadataKey, { signature })
const redactedContent = state.reasoningRedactedContent[index]
if (redactedContent)
return providerMetadata(state.providerMetadataKey, {
redactedData: encodeRedactedContent(redactedContent),
})
})()
return Lifecycle.reasoningEnd(
Lifecycle.textEnd(state.lifecycle, events, `text-${index}`),
events,
`reasoning-${index}`,
metadata,
)
})()
const lifecycle = resultEvents.length
? Lifecycle.stepStart(state.lifecycle, events)
: Lifecycle.reasoningEnd(
Lifecycle.textEnd(state.lifecycle, events, `text-${index}`),
events,
`reasoning-${index}`,
state.reasoningSignatures[index]
? providerMetadata(state.providerMetadataKey, { signature: state.reasoningSignatures[index] })
: undefined,
)
events.push(...resultEvents)
return [
{
@@ -671,24 +618,12 @@ const step = (state: ParserState, event: BedrockEvent) =>
reasoningSignatures: Object.fromEntries(
Object.entries(state.reasoningSignatures).filter(([key]) => key !== String(index)),
),
reasoningRedactedContent: Object.fromEntries(
Object.entries(state.reasoningRedactedContent).filter(([key]) => key !== String(index)),
),
},
events,
] as const
}
if (event.messageStop) {
if (
event.messageStop.stopReason === "malformed_model_output" ||
event.messageStop.stopReason === "malformed_tool_use"
)
return yield* ProviderShared.eventError(
ADAPTER,
`Bedrock Converse stopped with ${event.messageStop.stopReason}`,
ProviderShared.encodeJson(event),
)
return [
{
...state,
@@ -735,22 +670,23 @@ const step = (state: ParserState, event: BedrockEvent) =>
const framing = BedrockEventStream.framing(ADAPTER)
const onHalt = (state: ParserState): ReadonlyArray<LLMEvent> => {
if (!state.pendingFinish) return []
const normalized = (() => {
if (state.pendingFinish.reason.normalized === "stop" && state.hasToolCalls) return "tool-calls"
return state.pendingFinish.reason.normalized
})()
const events: LLMEvent[] = []
Lifecycle.finish(state.lifecycle, events, {
reason: {
...state.pendingFinish.reason,
normalized,
},
usage: state.pendingFinish.usage,
})
return events
}
const onHalt = (state: ParserState): ReadonlyArray<LLMEvent> =>
state.pendingFinish
? (() => {
const events: LLMEvent[] = []
Lifecycle.finish(state.lifecycle, events, {
reason: {
...state.pendingFinish.reason,
normalized:
state.pendingFinish.reason.normalized === "stop" && state.hasToolCalls
? "tool-calls"
: state.pendingFinish.reason.normalized,
},
usage: state.pendingFinish.usage,
})
return events
})()
: []
// =============================================================================
// Protocol And Bedrock Route
@@ -775,7 +711,6 @@ export const protocol = Protocol.make({
hasToolCalls: false,
lifecycle: Lifecycle.initial(),
reasoningSignatures: {},
reasoningRedactedContent: {},
}),
step,
onHalt: (state) => Effect.succeed(onHalt(state)),
@@ -1,7 +1,7 @@
import { EventStreamCodec } from "@smithy/eventstream-codec"
import { fromUtf8, toUtf8 } from "@smithy/util-utf8"
import { Effect, Encoding, Stream } from "effect"
import { AIError, AIErrorReason, InvalidProviderOutputError } from "../schema/index.js"
import { AIError, AIErrorReason } from "../schema/index.js"
import { Framing } from "../route/framing.js"
import { ProviderShared } from "./shared.js"
@@ -22,10 +22,6 @@ interface FrameBufferState {
const initialFrameBuffer: FrameBufferState = { buffer: new Uint8Array(0), offset: 0 }
type FrameInput = { readonly _tag: "Chunk"; readonly bytes: Uint8Array } | { readonly _tag: "End" }
const endOfStream: FrameInput = { _tag: "End" }
const appendChunk = (state: FrameBufferState, chunk: Uint8Array): FrameBufferState => {
const remaining = state.buffer.length - state.offset
// Compact: drop the consumed prefix and append the new chunk in one alloc.
@@ -37,23 +33,9 @@ const appendChunk = (state: FrameBufferState, chunk: Uint8Array): FrameBufferSta
return { buffer: next, offset: 0 }
}
const consumeFrames = (route: string) => (state: FrameBufferState, input: FrameInput) =>
const consumeFrames = (route: string) => (state: FrameBufferState, chunk: Uint8Array) =>
Effect.gen(function* () {
if (input._tag === "End") {
const remaining = state.buffer.subarray(state.offset)
if (remaining.length > 0)
return yield* new AIError({
reason: new InvalidProviderOutputError({
route,
classification: "incomplete-stream",
message: `Incomplete Bedrock Converse event-stream frame: ${remaining.length} buffered bytes remain at end of stream`,
body: Encoding.encodeBase64(remaining),
}),
})
return [state, []] as const
}
let cursor = appendChunk(state, input.bytes)
let cursor = appendChunk(state, chunk)
const out: object[] = []
while (cursor.buffer.length - cursor.offset >= 4) {
const view = cursor.buffer.subarray(cursor.offset)
@@ -131,12 +113,7 @@ const consumeFrames = (route: string) => (state: FrameBufferState, input: FrameI
export const framing = (route: string): Framing.Definition<object> => ({
id: "aws-event-stream",
body: (frame) => ("rawBody" in frame && typeof frame.rawBody === "string" ? frame.rawBody : undefined),
frame: (bytes) =>
bytes.pipe(
Stream.map((bytes): FrameInput => ({ _tag: "Chunk", bytes })),
Stream.concat(Stream.succeed(endOfStream)),
Stream.mapAccumEffect(() => initialFrameBuffer, consumeFrames(route)),
),
frame: (bytes) => bytes.pipe(Stream.mapAccumEffect(() => initialFrameBuffer, consumeFrames(route))),
})
export * as BedrockEventStream from "./bedrock-event-stream.js"
+24 -80
View File
@@ -240,10 +240,6 @@ interface ParserState {
readonly lifecycle: Lifecycle.State
readonly reasoningSignature?: string
readonly textSignature?: string
readonly reasoningId?: string
readonly textId?: string
readonly nextReasoningId: number
readonly nextTextId: number
readonly seenCallIds?: ReadonlySet<string>
}
@@ -575,23 +571,19 @@ const finish = (state: ParserState): ReadonlyArray<LLMEvent> => {
const events: LLMEvent[] = []
let lifecycle = state.lifecycle
if (state.reasoningId !== undefined)
if (state.reasoningSignature !== undefined)
lifecycle = Lifecycle.reasoningEnd(
lifecycle,
events,
state.reasoningId,
state.reasoningSignature === undefined
? undefined
: providerMetadata(state.providerMetadataKey, { thoughtSignature: state.reasoningSignature }),
"reasoning-0",
providerMetadata(state.providerMetadataKey, { thoughtSignature: state.reasoningSignature }),
)
if (state.textId !== undefined)
if (state.textSignature !== undefined)
lifecycle = Lifecycle.textEnd(
lifecycle,
events,
state.textId,
state.textSignature === undefined
? undefined
: providerMetadata(state.providerMetadataKey, { thoughtSignature: state.textSignature }),
"text-0",
providerMetadata(state.providerMetadataKey, { thoughtSignature: state.textSignature }),
)
Lifecycle.finish(lifecycle, events, {
reason: {
@@ -640,10 +632,6 @@ const step = (state: ParserState, event: GeminiEvent) => {
let lifecycle = nextState.lifecycle
let reasoningSignature = nextState.reasoningSignature
let textSignature = nextState.textSignature
let reasoningId = nextState.reasoningId
let textId = nextState.textId
let nextReasoningId = nextState.nextReasoningId
let nextTextId = nextState.nextTextId
// Supplier ids must be tracked across chunks of the same response, not just within one event's parts.
const seenCallIds = new Set(nextState.seenCallIds)
@@ -669,51 +657,27 @@ const step = (state: ParserState, event: GeminiEvent) => {
else if (signature !== undefined && "text" in part) textSignature = signature
if ("text" in part && part.text.length > 0) {
if (part.thought) {
if (textId !== undefined) {
lifecycle = Lifecycle.textEnd(
lifecycle,
events,
textId,
textSignature
? providerMetadata(state.providerMetadataKey, { thoughtSignature: textSignature })
: undefined,
)
textId = undefined
textSignature = undefined
}
if (reasoningId === undefined) {
reasoningId = `reasoning-${nextReasoningId}`
nextReasoningId += 1
}
lifecycle = Lifecycle.reasoningDelta(
lifecycle,
events,
reasoningId,
"reasoning-0",
part.text,
signature ? providerMetadata(state.providerMetadataKey, { thoughtSignature: signature }) : undefined,
)
continue
}
if (reasoningId !== undefined) {
lifecycle = Lifecycle.reasoningEnd(
lifecycle,
events,
reasoningId,
reasoningSignature
? providerMetadata(state.providerMetadataKey, { thoughtSignature: reasoningSignature })
: undefined,
)
reasoningId = undefined
reasoningSignature = undefined
}
if (textId === undefined) {
textId = `text-${nextTextId}`
nextTextId += 1
}
lifecycle = Lifecycle.reasoningEnd(
lifecycle,
events,
"reasoning-0",
reasoningSignature
? providerMetadata(state.providerMetadataKey, { thoughtSignature: reasoningSignature })
: undefined,
)
lifecycle = Lifecycle.textDelta(
lifecycle,
events,
textId,
"text-0",
part.text,
textSignature ? providerMetadata(state.providerMetadataKey, { thoughtSignature: textSignature }) : undefined,
)
@@ -731,28 +695,14 @@ const step = (state: ParserState, event: GeminiEvent) => {
const duplicate = supplied !== undefined && seenCallIds.has(supplied)
if (supplied !== undefined) seenCallIds.add(supplied)
const id = supplied !== undefined && !duplicate ? supplied : `tool_${crypto.randomUUID().replaceAll("-", "")}`
if (reasoningId !== undefined) {
lifecycle = Lifecycle.reasoningEnd(
lifecycle,
events,
reasoningId,
reasoningSignature
? providerMetadata(state.providerMetadataKey, { thoughtSignature: reasoningSignature })
: undefined,
)
reasoningId = undefined
reasoningSignature = undefined
}
if (textId !== undefined) {
lifecycle = Lifecycle.textEnd(
lifecycle,
events,
textId,
textSignature ? providerMetadata(state.providerMetadataKey, { thoughtSignature: textSignature }) : undefined,
)
textId = undefined
textSignature = undefined
}
lifecycle = Lifecycle.reasoningEnd(
lifecycle,
events,
"reasoning-0",
reasoningSignature
? providerMetadata(state.providerMetadataKey, { thoughtSignature: reasoningSignature })
: undefined,
)
lifecycle = Lifecycle.stepStart(lifecycle, events)
events.push(
LLMEvent.toolCall({
@@ -775,10 +725,6 @@ const step = (state: ParserState, event: GeminiEvent) => {
lifecycle,
reasoningSignature,
textSignature,
reasoningId,
textId,
nextReasoningId,
nextTextId,
seenCallIds,
finishReason: candidate.finishReason ?? nextState.finishReason,
},
@@ -806,8 +752,6 @@ export const protocol = Protocol.make({
providerMetadataKey: request.model.route.providerMetadataKey ?? String(request.model.provider),
hasToolCalls: false,
lifecycle: Lifecycle.initial(),
nextReasoningId: 0,
nextTextId: 0,
}),
step,
onHalt: (state) => Effect.succeed(finish(state)),
-1
View File
@@ -1,7 +1,6 @@
export * as AnthropicMessages from "./anthropic-messages.js"
export * as BedrockConverse from "./bedrock-converse.js"
export * as Gemini from "./gemini.js"
export * as MistralChat from "./mistral-chat.js"
export * as OpenAIChat from "./openai-chat.js"
export * as OpenAIImages from "./openai-images.js"
export * as OpenAICompatibleChat from "./openai-compatible-chat.js"
-780
View File
@@ -1,780 +0,0 @@
import { Effect, Schema } from "effect"
import { Auth } from "../route/auth.js"
import { Route } from "../route/client.js"
import { Endpoint } from "../route/endpoint.js"
import { Framing } from "../route/framing.js"
import { Protocol } from "../route/protocol.js"
import { HttpTransport } from "../route/transport/index.js"
import {
AIError,
InvalidProviderOutputError,
LLMEvent,
Usage,
type FinishReasonDetails,
type LLMRequest,
type MediaPart,
type ToolCallPart,
type ToolDefinition,
} from "../schema/index.js"
import { classifyProviderFailure } from "../provider-error.js"
import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared.js"
import { Lifecycle } from "./utils/lifecycle.js"
import { ToolStream } from "./utils/tool-stream.js"
const ADAPTER = "mistral-chat"
const DONE = "[DONE]" as const
const TOOL_ID = /^[A-Za-z0-9]{9}$/
export const DEFAULT_BASE_URL = "https://api.mistral.ai/v1"
export const PATH = "/chat/completions"
const MistralTextContent = Schema.Struct({
type: Schema.Literal("text"),
text: Schema.String,
})
const MistralThinkingUnit = Schema.StructWithRest(
Schema.Struct({
type: Schema.optional(Schema.String),
text: Schema.optional(Schema.String),
}),
[Schema.Record(Schema.String, Schema.Unknown)],
)
type MistralThinkingUnit = Schema.Schema.Type<typeof MistralThinkingUnit>
const MistralThinkingContent = Schema.StructWithRest(
Schema.Struct({
type: Schema.Literal("thinking"),
thinking: Schema.Array(MistralThinkingUnit),
}),
[Schema.Record(Schema.String, Schema.Unknown)],
)
type MistralThinkingContent = Schema.Schema.Type<typeof MistralThinkingContent>
const isMistralThinkingContent = Schema.is(MistralThinkingContent)
const MistralUserContent = Schema.Union([
MistralTextContent,
Schema.Struct({ type: Schema.Literal("image_url"), image_url: Schema.String }),
Schema.Struct({ type: Schema.Literal("document_url"), document_url: Schema.String }),
])
type MistralUserContent = Schema.Schema.Type<typeof MistralUserContent>
const MistralAssistantToolCall = Schema.Struct({
id: Schema.String,
type: Schema.Literal("function"),
function: Schema.Struct({ name: Schema.String, arguments: Schema.String }),
})
type MistralAssistantToolCall = Schema.Schema.Type<typeof MistralAssistantToolCall>
const MistralMessage = Schema.Union([
Schema.Struct({ role: Schema.Literal("system"), content: Schema.String }),
Schema.Struct({
role: Schema.Literal("user"),
content: Schema.Union([Schema.String, Schema.Array(MistralUserContent)]),
}),
Schema.Struct({
role: Schema.Literal("assistant"),
content: Schema.Union([Schema.String, Schema.Array(Schema.Union([MistralTextContent, MistralThinkingContent]))]),
tool_calls: optionalArray(MistralAssistantToolCall),
prefix: Schema.optional(Schema.Literal(true)),
}),
Schema.Struct({
role: Schema.Literal("tool"),
tool_call_id: Schema.String,
name: Schema.String,
content: Schema.Union([Schema.String, Schema.Array(MistralUserContent)]),
}),
]).pipe(Schema.toTaggedUnion("role"))
type MistralMessage = Schema.Schema.Type<typeof MistralMessage>
const MistralTool = Schema.Struct({
type: Schema.Literal("function"),
function: Schema.Struct({
name: Schema.String,
description: Schema.String,
parameters: JsonObject,
strict: Schema.Literal(false),
}),
})
type MistralTool = Schema.Schema.Type<typeof MistralTool>
const MistralOptions = Schema.Struct({
safePrompt: Schema.optional(Schema.Boolean),
documentImageLimit: Schema.optional(Schema.Number),
documentPageLimit: Schema.optional(Schema.Number),
parallelToolCalls: Schema.optional(Schema.Boolean),
reasoningEffort: Schema.optional(Schema.String),
promptMode: Schema.optional(Schema.Literal("reasoning")),
promptCacheKey: Schema.optional(Schema.String),
})
export type ReasoningEffort = "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | (string & {})
export type ProviderOptionsInput = {
readonly safePrompt?: boolean
readonly documentImageLimit?: number
readonly documentPageLimit?: number
readonly parallelToolCalls?: boolean
readonly reasoningEffort?: ReasoningEffort
readonly promptMode?: "reasoning"
readonly promptCacheKey?: string
readonly [key: string]: unknown
}
const MistralBody = Schema.Struct({
model: Schema.String,
messages: Schema.Array(MistralMessage),
tools: optionalArray(MistralTool),
tool_choice: Schema.optional(
Schema.Union([
Schema.Literals(["auto", "none", "any"]),
Schema.Struct({ type: Schema.Literal("function"), function: Schema.Struct({ name: Schema.String }) }),
]),
),
stream: Schema.Literal(true),
max_tokens: Schema.optional(Schema.Number),
random_seed: Schema.optional(Schema.Number),
temperature: Schema.optional(Schema.Number),
top_p: Schema.optional(Schema.Number),
frequency_penalty: Schema.optional(Schema.Number),
presence_penalty: Schema.optional(Schema.Number),
stop: optionalArray(Schema.String),
prompt_cache_key: Schema.optional(Schema.String),
safe_prompt: Schema.optional(Schema.Boolean),
document_image_limit: Schema.optional(Schema.Number),
document_page_limit: Schema.optional(Schema.Number),
parallel_tool_calls: Schema.optional(Schema.Boolean),
reasoning_effort: Schema.optional(Schema.String),
prompt_mode: Schema.optional(Schema.Literal("reasoning")),
})
export type MistralBody = Schema.Schema.Type<typeof MistralBody>
const MistralUsageDetails = Schema.StructWithRest(Schema.Struct({ cached_tokens: optionalNull(Schema.Number) }), [
Schema.Record(Schema.String, Schema.Unknown),
])
const MistralUsage = Schema.StructWithRest(
Schema.Struct({
prompt_tokens: optionalNull(Schema.Number),
completion_tokens: optionalNull(Schema.Number),
total_tokens: optionalNull(Schema.Number),
num_cached_tokens: optionalNull(Schema.Number),
prompt_token_details: optionalNull(MistralUsageDetails),
prompt_tokens_details: optionalNull(MistralUsageDetails),
}),
[Schema.Record(Schema.String, Schema.Unknown)],
)
const MistralOutputContent = Schema.StructWithRest(
Schema.Struct({
type: Schema.String,
text: optionalNull(Schema.String),
thinking: optionalNull(Schema.Unknown),
}),
[Schema.Record(Schema.String, Schema.Unknown)],
)
type MistralOutputContent = Schema.Schema.Type<typeof MistralOutputContent>
const MistralToolDelta = Schema.Struct({
index: optionalNull(Schema.Number),
id: optionalNull(Schema.String),
function: optionalNull(
Schema.Struct({
name: optionalNull(Schema.String),
arguments: optionalNull(Schema.Union([Schema.String, JsonObject])),
}),
),
})
type MistralToolDelta = Schema.Schema.Type<typeof MistralToolDelta>
const MistralChoice = Schema.StructWithRest(
Schema.Struct({
delta: optionalNull(
Schema.StructWithRest(
Schema.Struct({
content: optionalNull(Schema.Union([Schema.String, Schema.Array(MistralOutputContent)])),
tool_calls: optionalNull(Schema.Array(MistralToolDelta)),
}),
[Schema.Record(Schema.String, Schema.Unknown)],
),
),
finish_reason: optionalNull(Schema.String),
}),
[Schema.Record(Schema.String, Schema.Unknown)],
)
const MistralError = Schema.StructWithRest(
Schema.Struct({
message: Schema.String,
code: optionalNull(Schema.Union([Schema.String, Schema.Number])),
}),
[Schema.Record(Schema.String, Schema.Unknown)],
)
const MistralEvent = Schema.StructWithRest(
Schema.Struct({
choices: optionalNull(Schema.Array(MistralChoice)),
usage: optionalNull(MistralUsage),
error: optionalNull(MistralError),
}),
[Schema.Record(Schema.String, Schema.Unknown)],
)
type MistralEvent = Schema.Schema.Type<typeof MistralEvent>
const MistralStreamEvent = Schema.Union([Schema.Literal(DONE), Protocol.jsonEvent(MistralEvent)])
const hashID = (value: string) => {
const hash = (seed: number) => {
let result = seed
for (const char of value) result = Math.imul(result ^ char.charCodeAt(0), 16777619)
return (result >>> 0).toString(36)
}
return `${hash(2166136261).padStart(7, "0")}${hash(2246822519).padStart(7, "0")}`.slice(-9)
}
const toolIDNormalizer = (request: LLMRequest) => {
const ids = request.messages.flatMap((message) =>
message.content.flatMap((part) => (part.type === "tool-call" || part.type === "tool-result" ? [part.id] : [])),
)
const used = new Set(ids.filter((id) => TOOL_ID.test(id)))
const normalized = new Map<string, string>()
return (id: string) => {
if (TOOL_ID.test(id)) return id
const previous = normalized.get(id)
if (previous) return previous
let attempt = 0
let candidate = hashID(id)
while (used.has(candidate)) candidate = hashID(`${id}:${++attempt}`)
used.add(candidate)
normalized.set(id, candidate)
return candidate
}
}
const lowerMedia = Effect.fn("MistralChat.lowerMedia")(function* (part: MediaPart) {
const media = ProviderShared.normalizeMedia(part)
const url = typeof part.data === "string" && /^(?:https?:|data:)/.test(part.data) ? part.data : media.dataUrl
if (media.mime.startsWith("image/")) return { type: "image_url" as const, image_url: url }
if (media.mime === "application/pdf") return { type: "document_url" as const, document_url: url }
return yield* ProviderShared.invalidRequest(`Mistral Chat does not support media type ${part.mediaType}`)
})
const lowerUser = Effect.fn("MistralChat.lowerUser")(function* (message: LLMRequest["messages"][number]) {
const content: MistralUserContent[] = []
for (const part of message.content) {
if (part.type === "text") {
content.push({ type: "text", text: part.text })
continue
}
if (part.type === "media") {
content.push(yield* lowerMedia(part))
continue
}
return yield* ProviderShared.unsupportedContent("Mistral Chat", "user", ["text", "media"])
}
if (content.every((part) => part.type === "text"))
return { role: "user" as const, content: content.map((part) => part.text).join("") }
return { role: "user" as const, content }
})
const lowerToolCall = (part: ToolCallPart, normalizeID: (id: string) => string): MistralAssistantToolCall => ({
id: normalizeID(part.id),
type: "function",
function: { name: part.name, arguments: ProviderShared.encodeJson(part.input) },
})
const lowerAssistant = Effect.fn("MistralChat.lowerAssistant")(function* (
message: LLMRequest["messages"][number],
normalizeID: (id: string) => string,
prefix: boolean,
) {
const structured = message.content.some(
(part) => part.type === "reasoning" && isMistralThinkingContent(part.providerMetadata?.mistral?.thinking),
)
const content: Array<Schema.Schema.Type<typeof MistralTextContent> | MistralThinkingContent> = []
const text: string[] = []
const toolCalls: MistralAssistantToolCall[] = []
for (const part of message.content) {
if (part.type === "text") {
if (structured) content.push({ type: "text", text: part.text })
else text.push(part.text)
continue
}
if (part.type === "reasoning") {
const native = part.providerMetadata?.mistral?.thinking
if (structured && isMistralThinkingContent(native)) content.push(native)
else if (structured) content.push({ type: "text", text: part.text })
else text.push(part.text)
continue
}
if (part.type === "tool-call") {
toolCalls.push(lowerToolCall(part, normalizeID))
continue
}
return yield* ProviderShared.unsupportedContent("Mistral Chat", "assistant", ["text", "reasoning", "tool-call"])
}
return {
role: "assistant" as const,
content: structured ? content : text.join(""),
...(toolCalls.length > 0 ? { tool_calls: toolCalls } : {}),
...(prefix ? { prefix: true as const } : {}),
}
})
const lowerToolResults = Effect.fn("MistralChat.lowerToolResults")(function* (
message: LLMRequest["messages"][number],
normalizeID: (id: string) => string,
) {
const output: MistralMessage[] = []
for (const part of message.content) {
if (part.type !== "tool-result")
return yield* ProviderShared.unsupportedContent("Mistral Chat", "tool", ["tool-result"])
if (part.result.type !== "content") {
output.push({
role: "tool",
tool_call_id: normalizeID(part.id),
name: part.name,
content: ProviderShared.toolResultText(part),
})
continue
}
const content: MistralUserContent[] = []
for (const item of part.result.value) {
if (item.type === "text") {
content.push({ type: "text", text: item.text })
continue
}
content.push(yield* lowerMedia({ type: "media", mediaType: item.mime, data: item.uri, filename: item.name }))
}
output.push({
role: "tool",
tool_call_id: normalizeID(part.id),
name: part.name,
content: content.some((item) => item.type !== "text")
? content
: content.map((item) => (item.type === "text" ? item.text : "")).join(""),
})
}
return output
})
const lowerMessages = Effect.fn("MistralChat.lowerMessages")(function* (request: LLMRequest) {
const normalizeID = toolIDNormalizer(request)
const messages: MistralMessage[] =
request.system.length === 0 ? [] : [{ role: "system", content: ProviderShared.joinText(request.system) }]
for (const message of request.messages) {
if (message.role === "system") {
const update = yield* ProviderShared.wrappedSystemUpdate("Mistral Chat", message)
messages.push({
role: "user",
content: update.text,
})
continue
}
if (message.role === "user") {
messages.push(yield* lowerUser(message))
continue
}
if (message.role === "assistant") {
const hasToolCalls = message.content.some((part) => part.type === "tool-call")
const hasNativeThinking = message.content.some(
(part) => part.type === "reasoning" && isMistralThinkingContent(part.providerMetadata?.mistral?.thinking),
)
const text = message.content
.flatMap((part) => (part.type === "text" || part.type === "reasoning" ? [part.text] : []))
.join("")
if (!hasToolCalls && !hasNativeThinking && text.trim() === "") continue
messages.push(yield* lowerAssistant(message, normalizeID, !hasToolCalls && message === request.messages.at(-1)))
continue
}
messages.push(...(yield* lowerToolResults(message, normalizeID)))
}
return messages
})
const lowerTool = (tool: ToolDefinition): MistralTool => ({
type: "function",
function: { name: tool.name, description: tool.description, parameters: tool.inputSchema, strict: false },
})
export const fromRequest = Effect.fn("MistralChat.fromRequest")(function* (request: LLMRequest) {
const options = yield* ProviderShared.validateWith(Schema.decodeUnknownEffect(MistralOptions))(
request.providerOptions ?? {},
)
const selected = request.toolChoice?.type === "tool" ? request.toolChoice.name : undefined
if (request.toolChoice?.type === "tool" && !selected)
return yield* ProviderShared.invalidRequest("Mistral Chat tool choice requires a tool name")
if (options.reasoningEffort !== undefined && options.promptMode !== undefined)
return yield* ProviderShared.invalidRequest(
"Mistral Chat reasoningEffort and promptMode provider options are mutually exclusive",
)
const toolChoice = request.toolChoice
? yield* ProviderShared.matchToolChoice("Mistral Chat", request.toolChoice, {
auto: () => "auto" as const,
none: () => "none" as const,
required: () => "any" as const,
tool: (name) => ({ type: "function" as const, function: { name } }),
})
: undefined
return {
model: request.model.id,
messages: yield* lowerMessages(request),
tools: request.tools.length > 0 ? request.tools.map(lowerTool) : undefined,
tool_choice: toolChoice,
stream: true as const,
max_tokens: request.generation?.maxTokens,
random_seed: request.generation?.seed,
temperature: request.generation?.temperature,
top_p: request.generation?.topP,
frequency_penalty: request.generation?.frequencyPenalty,
presence_penalty: request.generation?.presencePenalty,
stop: request.generation?.stop,
prompt_cache_key: request.cache === "none" ? undefined : (options.promptCacheKey ?? request.promptCacheKey),
safe_prompt: options.safePrompt,
document_image_limit: options.documentImageLimit,
document_page_limit: options.documentPageLimit,
parallel_tool_calls:
options.parallelToolCalls ?? (request.toolChoice?.disableParallelToolUse === true ? false : undefined),
reasoning_effort: options.reasoningEffort,
prompt_mode: options.promptMode,
}
})
type ToolKey = string | number
interface PendingTool {
readonly id: string
readonly name?: string
readonly input: string
}
interface ActiveContent {
readonly type: "text" | "reasoning"
readonly id: string
readonly thinking?: MistralThinkingContent
}
export interface ParserState {
readonly tools: ToolStream.State<ToolKey>
readonly pendingTools: Partial<Record<ToolKey, PendingTool>>
readonly toolIDs: ReadonlyMap<string, string>
readonly usedToolIDs: ReadonlySet<string>
readonly completedTools: ReadonlyArray<LLMEvent>
readonly latestToolKey?: ToolKey
readonly generatedTools: number
readonly lifecycle: Lifecycle.State
readonly active?: ActiveContent
readonly nextContent: number
readonly usage?: Usage
readonly finishReason?: FinishReasonDetails
}
const mapUsage = (usage: MistralEvent["usage"]): Usage | undefined => {
if (!usage) return undefined
const input = usage.prompt_tokens ?? undefined
const reported =
usage.num_cached_tokens ??
usage.prompt_tokens_details?.cached_tokens ??
usage.prompt_token_details?.cached_tokens ??
undefined
const cached = input === undefined || reported === undefined ? undefined : Math.max(0, Math.min(input, reported))
const output = usage.completion_tokens ?? undefined
return new Usage({
inputTokens: input,
outputTokens: output,
nonCachedInputTokens: ProviderShared.subtractTokens(input, cached),
cacheReadInputTokens: cached,
totalTokens: ProviderShared.totalTokens(input, output, usage.total_tokens ?? undefined),
providerMetadata: { mistral: usage },
})
}
const mapFinishReason = (reason: string) => {
switch (reason) {
case "stop":
return "stop" as const
case "length":
case "model_length":
return "length" as const
case "tool_calls":
return "tool-calls" as const
case "content_filter":
return "content-filter" as const
case "error":
case "network_error":
return "error" as const
default:
return "unknown" as const
}
}
const thinkingUnits = (value: unknown): ReadonlyArray<MistralThinkingUnit> => {
if (typeof value === "string") return [{ type: "text", text: value }]
if (!Array.isArray(value)) return []
return value.filter(Schema.is(MistralThinkingUnit))
}
const thinkingText = (thinking: ReadonlyArray<MistralThinkingUnit>) =>
thinking.flatMap((unit) => (typeof unit.text === "string" ? [unit.text] : [])).join("")
const thinkingMetadata = (thinking: MistralThinkingContent) => ({ mistral: { thinking } })
const closeActive = (state: ParserState, events: LLMEvent[]) => {
if (!state.active) return state
const lifecycle =
state.active.type === "text"
? Lifecycle.textEnd(state.lifecycle, events, state.active.id)
: Lifecycle.reasoningEnd(
state.lifecycle,
events,
state.active.id,
thinkingMetadata(state.active.thinking ?? { type: "thinking", thinking: [] }),
thinkingText(state.active.thinking?.thinking ?? []),
)
return { ...state, lifecycle, active: undefined }
}
const appendText = (state: ParserState, events: LLMEvent[], text: string) => {
if (text.length === 0) return state
const current = state.active?.type === "text" ? state : closeActive(state, events)
const active = current.active ?? { type: "text" as const, id: `text-${current.nextContent}` }
return {
...current,
lifecycle: Lifecycle.textDelta(current.lifecycle, events, active.id, text),
active,
nextContent: current.active ? current.nextContent : current.nextContent + 1,
}
}
const appendThinking = (state: ParserState, events: LLMEvent[], part: MistralOutputContent) => {
const current = state.active?.type === "reasoning" ? state : closeActive(state, events)
const units = thinkingUnits(part.thinking)
const active = current.active ?? { type: "reasoning" as const, id: `reasoning-${current.nextContent}` }
const thinking = {
...active.thinking,
...part,
type: "thinking" as const,
thinking: [...(active.thinking?.thinking ?? []), ...units],
}
const text = thinkingText(units)
return {
...current,
lifecycle:
text.length > 0
? Lifecycle.reasoningDelta(current.lifecycle, events, active.id, text, thinkingMetadata(thinking))
: Lifecycle.reasoningStart(current.lifecycle, events, active.id, thinkingMetadata(thinking)),
active: { ...active, thinking },
nextContent: current.active ? current.nextContent : current.nextContent + 1,
}
}
const appendContent = (
state: ParserState,
events: LLMEvent[],
content: string | ReadonlyArray<MistralOutputContent>,
) => {
if (typeof content === "string") return appendText(state, events, content)
return content.reduce((current, part) => {
if (part.type === "text") return appendText(current, events, part.text ?? "")
if (part.type === "thinking") return appendThinking(current, events, part)
return closeActive(current, events)
}, state)
}
const normalizeStreamToolID = (state: ParserState, source: string) => {
if (TOOL_ID.test(source))
return { id: source, state: { ...state, usedToolIDs: new Set([...state.usedToolIDs, source]) } }
const previous = state.toolIDs.get(source)
if (previous) return { id: previous, state }
let attempt = 0
let id = hashID(source)
while (state.usedToolIDs.has(id)) id = hashID(`${source}:${++attempt}`)
return {
id,
state: {
...state,
toolIDs: new Map([...state.toolIDs, [source, id]]),
usedToolIDs: new Set([...state.usedToolIDs, id]),
},
}
}
const toolText = (tool: MistralToolDelta) => {
const value = tool.function?.arguments
if (typeof value === "string") return value
return value === null || value === undefined ? "" : ProviderShared.encodeJson(value)
}
const appendTools = Effect.fn("MistralChat.appendTools")(function* (
initial: ParserState,
events: LLMEvent[],
deltas: ReadonlyArray<MistralToolDelta>,
) {
if (deltas.length === 0) return initial
let state = closeActive(initial, events)
for (const [position, delta] of deltas.entries()) {
const wireID = delta.id?.trim() || undefined
const providedID = wireID === "null" ? undefined : wireID
const key =
delta.index ??
(providedID
? `id:${providedID}`
: deltas.length > 1
? `position:${position}`
: (state.latestToolKey ?? `missing:${state.generatedTools}`))
const existing = state.tools[key]
const pending = state.pendingTools[key]
const source = providedID ?? `generated:${String(key)}`
const normalized =
existing || pending ? { id: existing?.id ?? pending?.id ?? "", state } : normalizeStreamToolID(state, source)
state = normalized.state
const name = existing?.name ?? pending?.name ?? (delta.function?.name?.trim() || undefined)
const text = `${pending?.input ?? ""}${toolText(delta)}`
if (!name) {
state = {
...state,
pendingTools: { ...state.pendingTools, [key]: { id: normalized.id, input: text } },
latestToolKey: key,
generatedTools: state.generatedTools + (!providedID && !pending ? 1 : 0),
}
continue
}
const result = ToolStream.appendOrStart(
ADAPTER,
state.tools,
key,
{ id: normalized.id, name, text },
"Mistral Chat tool call delta is missing a name",
)
if (ToolStream.isError(result)) return yield* result
if (result.events.length > 0) state = { ...state, lifecycle: Lifecycle.stepStart(state.lifecycle, events) }
events.push(...result.events)
const pendingTools = { ...state.pendingTools }
delete pendingTools[key]
state = {
...state,
tools: result.tools,
pendingTools,
latestToolKey: key,
generatedTools: state.generatedTools + (!providedID && !existing && !pending ? 1 : 0),
}
}
return state
})
const hasLateContent = (event: MistralEvent) => {
const delta = event.choices?.[0]?.delta
if (typeof delta?.content === "string" && delta.content.length > 0) return true
if (Array.isArray(delta?.content) && delta.content.length > 0) return true
return (delta?.tool_calls ?? []).some(
(tool) => Boolean(tool.id) || Boolean(tool.function?.name) || tool.function?.arguments !== undefined,
)
}
const step = Effect.fn("MistralChat.step")(function* (state: ParserState, event: MistralEvent) {
if (event.error) {
const body = ProviderShared.encodeJson(event)
return yield* new AIError({
reason: classifyProviderFailure({
message: event.error.message,
status: typeof event.error.code === "number" ? event.error.code : undefined,
rawBody: body,
}),
})
}
const events: LLMEvent[] = []
const usage = mapUsage(event.usage) ?? state.usage
if (state.finishReason) {
if (hasLateContent(event))
return yield* ProviderShared.eventError(
ADAPTER,
"Mistral Chat received content after the finish reason",
ProviderShared.encodeJson(event),
)
return [{ ...state, usage }, events] as const
}
const choice = event.choices?.[0]
const withContent = choice?.delta?.content == null ? state : appendContent(state, events, choice.delta.content)
const withTools = yield* appendTools(withContent, events, choice?.delta?.tool_calls ?? [])
if (!choice?.finish_reason) return [{ ...withTools, usage }, events] as const
const finishReason = {
normalized: mapFinishReason(choice.finish_reason),
raw: choice.finish_reason,
}
const incomplete = finishReason.normalized === "length" || finishReason.normalized === "content-filter"
if (!incomplete && Object.keys(withTools.pendingTools).length > 0)
return yield* ProviderShared.eventError(
ADAPTER,
"Mistral Chat tool call delta is missing a name",
ProviderShared.encodeJson(event),
)
const finished =
!incomplete && Object.keys(withTools.tools).length > 0
? yield* ToolStream.finishAll(ADAPTER, withTools.tools)
: undefined
return [
{
...withTools,
tools: finished?.tools ?? withTools.tools,
completedTools: finished?.events ?? withTools.completedTools,
usage,
finishReason,
},
events,
] as const
})
const finishEvents = Effect.fn("MistralChat.finishEvents")(function* (state: ParserState) {
if (!state.finishReason)
return yield* new AIError({
reason: new InvalidProviderOutputError({
message: "Mistral Chat stream ended without finish_reason",
classification: "incomplete-stream",
route: ADAPTER,
}),
})
const events: LLMEvent[] = []
const closed = closeActive(state, events)
const lifecycle = closed.completedTools.length > 0 ? Lifecycle.stepStart(closed.lifecycle, events) : closed.lifecycle
events.push(...closed.completedTools)
const reason =
state.finishReason.normalized === "stop" && closed.completedTools.some(LLMEvent.is.toolCall)
? { ...state.finishReason, normalized: "tool-calls" as const }
: state.finishReason
Lifecycle.finish(lifecycle, events, { reason, usage: closed.usage })
return events
})
export const protocol = Protocol.make({
id: ADAPTER,
body: { schema: MistralBody, from: fromRequest },
stream: {
event: MistralStreamEvent,
initial: (): ParserState => ({
tools: ToolStream.empty<ToolKey>(),
pendingTools: {},
toolIDs: new Map(),
usedToolIDs: new Set(),
completedTools: [],
generatedTools: 0,
lifecycle: Lifecycle.initial(),
nextContent: 0,
}),
step: (state: ParserState, event) => (event === DONE ? Effect.succeed([state, []] as const) : step(state, event)),
terminal: (event) => event === DONE,
onHalt: finishEvents,
},
})
export const framing = Framing.sseWithDone
export const httpTransport = HttpTransport.sseJson.with<MistralBody>().with({ framing })
export const route = Route.make({
id: ADAPTER,
provider: "mistral",
providerMetadataKey: "mistral",
protocol,
endpoint: Endpoint.path(PATH, { baseURL: DEFAULT_BASE_URL }),
auth: Auth.none,
transport: httpTransport,
})
export * as MistralChat from "./mistral-chat.js"
+4 -2
View File
@@ -577,8 +577,10 @@ const lowerMessages = Effect.fn("OpenResponses.lowerMessages")(function* (reques
}
if (message.role === "user") {
const content = yield* Effect.forEach(message.content, (part) => lowerUserContent(part, request, extension))
if (content.length > 0) input.push({ role: "user", content })
input.push({
role: "user",
content: yield* Effect.forEach(message.content, (part) => lowerUserContent(part, request, extension)),
})
continue
}
@@ -1,4 +1,4 @@
import { Effect, Encoding, Schema } from "effect"
import { Effect, Schema } from "effect"
import type { MediaPart } from "../../schema/index.js"
import { ProviderShared } from "../shared.js"
@@ -57,16 +57,6 @@ const documentBlock = (name: string, format: DocumentFormat, bytes: string): Doc
},
})
const mediaBase64 = Effect.fn("BedrockMedia.mediaBase64")(function* (part: MediaPart) {
const media = ProviderShared.normalizeMedia(part)
const bytes = yield* Effect.fromResult(Encoding.decodeBase64(media.base64)).pipe(
Effect.mapError((cause) =>
ProviderShared.invalidRequest("Bedrock Converse media data must be valid base64", cause),
),
)
return Encoding.encodeBase64(bytes)
})
// Route by MIME. Known image/document formats lower into a typed block; anything
// else fails with a clear error instead of silently degrading to a malformed
// document block. Image MIME types not in `IMAGE_FORMATS` (e.g. `image/svg+xml`)
@@ -76,7 +66,8 @@ export const lower = Effect.fn("BedrockMedia.lower")(function* (part: MediaPart)
const mime = part.mediaType.toLowerCase()
const imageFormat = IMAGE_FORMATS[mime as keyof typeof IMAGE_FORMATS]
if (imageFormat) {
return { image: { format: imageFormat, source: { bytes: yield* mediaBase64(part) } } } satisfies ImageBlock
const media = ProviderShared.normalizeMedia(part)
return { image: { format: imageFormat, source: { bytes: media.base64 } } } satisfies ImageBlock
}
if (mime.startsWith("image/"))
return yield* ProviderShared.invalidRequest(`Bedrock Converse does not support image media type ${part.mediaType}`)
@@ -84,7 +75,8 @@ export const lower = Effect.fn("BedrockMedia.lower")(function* (part: MediaPart)
if (documentFormat) {
if (!part.filename)
return yield* ProviderShared.invalidRequest("Bedrock Converse document media requires a filename")
return documentBlock(part.filename, documentFormat, yield* mediaBase64(part))
const media = ProviderShared.normalizeMedia(part)
return documentBlock(part.filename, documentFormat, media.base64)
}
return yield* ProviderShared.invalidRequest(`Bedrock Converse does not support media type ${part.mediaType}`)
})
-1
View File
@@ -13,7 +13,6 @@ export * as GoogleVertexChat from "./google-vertex-chat.js"
export * as GoogleVertexMessages from "./google-vertex-messages.js"
export * as GoogleVertexResponses from "./google-vertex-responses.js"
export * as Groq from "./groq.js"
export * as Mistral from "./mistral.js"
export * as OpenAI from "./openai.js"
export * as OpenAICompatible from "./openai-compatible.js"
export * as OpenAICompatibleResponses from "./openai-compatible-responses.js"
-51
View File
@@ -1,51 +0,0 @@
import type { ProviderPackage } from "../provider-package.js"
import { MistralChat } from "../protocols/mistral-chat.js"
import { AuthOptions, type ProviderAuthOption } from "../route/auth-options.js"
import type { RouteDefaultsInput } from "../route/client.js"
import { ProviderID, type ModelID } from "../schema/index.js"
export const id = ProviderID.make("mistral")
export type ProviderOptions = MistralChat.ProviderOptionsInput
export type LanguageModelOptions = Omit<RouteDefaultsInput, "providerOptions"> &
ProviderAuthOption<"optional"> & {
readonly baseURL?: string
readonly providerOptions?: ProviderOptions
}
export interface Settings extends ProviderPackage.Settings {
readonly apiKey?: string
readonly baseURL?: string
readonly providerOptions?: ProviderOptions
}
export const route = MistralChat.route
export const routes = [route]
export const configure = (input: LanguageModelOptions = {}) => {
const { apiKey: _apiKey, auth: _auth, baseURL, ...defaults } = input
const configured = route.with({
...defaults,
endpoint: { baseURL: baseURL ?? MistralChat.DEFAULT_BASE_URL },
auth: AuthOptions.bearer(input, "MISTRAL_API_KEY"),
})
return {
id,
model: (modelID: string | ModelID) => configured.model<ProviderOptions>({ id: modelID }),
configure,
}
}
export const provider = configure()
export const model: ProviderPackage.Definition<Settings, ProviderOptions>["model"] = (modelID, settings) =>
configure({
apiKey: settings.apiKey,
baseURL: settings.baseURL,
headers: settings.headers === undefined ? undefined : { ...settings.headers },
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
providerOptions: settings.providerOptions,
}).model(modelID)
export * as Mistral from "./mistral.js"
+7 -12
View File
@@ -7,7 +7,6 @@ import { HttpTransport } from "./transport/index.js"
import type { HttpMiddleware, Transport, TransportRuntime, WebSocketChannelExecutor } from "./transport/index.js"
import type { Protocol } from "./protocol.js"
import { applyCachePolicy } from "../cache-policy.js"
import { normalizeToolHistory } from "../tool-history.js"
import { sanitizeSurrogates } from "../utils/sanitize.js"
import * as ProviderShared from "../protocols/shared.js"
import type { ProtocolID, ProviderOptions } from "../schema/index.js"
@@ -170,19 +169,17 @@ export interface GenerateMethod {
export class Service extends Context.Service<Service, Interface>()("@opencode/LLMClient") {}
const resolveRequestOptions = (request: LLMRequest) => {
const messages = normalizeToolHistory(request.messages)
const normalized = messages === request.messages ? request : LLMRequest.update(request, { messages })
const routeDefaults = normalized.model.route.defaults
const modelDefaults = normalized.model.defaults
const generation = mergeGenerationOptions(routeDefaults.generation, modelDefaults?.generation, normalized.generation)
return LLMRequest.update(normalized, {
const routeDefaults = request.model.route.defaults
const modelDefaults = request.model.defaults
const generation = mergeGenerationOptions(routeDefaults.generation, modelDefaults?.generation, request.generation)
return LLMRequest.update(request, {
generation: generation ?? new GenerationOptions({}),
providerOptions: mergeProviderOptions(
routeDefaults.providerOptions,
modelDefaults?.providerOptions,
normalized.providerOptions,
request.providerOptions,
),
http: mergeHttpOptions(routeDefaults.http, modelDefaults?.http, normalized.http),
http: mergeHttpOptions(routeDefaults.http, modelDefaults?.http, request.http),
})
}
@@ -449,9 +446,7 @@ export function make<Body, Prepared, Frame, Event, State>(
const compile = Effect.fn("LLM.compile")(function* (request: LLMRequest, options?: StreamOptions) {
const original = applyCachePolicy(resolveRequestOptions(request))
const sanitized = LLMRequest.update(original, sanitizeSurrogates({ ...LLMRequest.input(original), model: undefined }))
const tools = [...new Map(sanitized.tools.map((tool) => [tool.name, tool])).values()]
const resolved = tools.length === sanitized.tools.length ? sanitized : LLMRequest.update(sanitized, { tools })
const resolved = LLMRequest.update(original, sanitizeSurrogates({ ...LLMRequest.input(original), model: undefined }))
const route = resolved.model.route
const body = yield* route.body
-74
View File
@@ -1,74 +0,0 @@
import { Message, ToolResultPart, type ToolCallPart } from "./schema/messages.js"
const EMPTY_TOOL_OUTPUT = "(no tool output)"
const MISSING_TOOL_RESULT = "Tool result missing"
export function normalizeToolHistory(messages: ReadonlyArray<Message>) {
const normalized: Message[] = []
const pending = new Map<string, ToolCallPart>()
const appendMissingResults = () => {
if (pending.size === 0) return
normalized.push(missingToolResults(pending.values()))
pending.clear()
}
for (const message of messages) {
if (message.role === "user" || message.role === "assistant") appendMissingResults()
if (message.role === "tool") {
const tool = normalizeToolMessage(message, pending)
if (tool) normalized.push(tool)
continue
}
normalized.push(message)
if (message.role !== "assistant") continue
for (const part of message.content) {
if (part.type === "tool-call" && part.providerExecuted !== true) pending.set(part.id, part)
}
}
return normalized.length === messages.length && normalized.every((message, index) => message === messages[index])
? messages
: normalized
}
function missingToolResults(calls: Iterable<ToolCallPart>) {
return new Message({
role: "tool",
content: [...calls].map((call) =>
ToolResultPart.make({ id: call.id, name: call.name, result: MISSING_TOOL_RESULT, resultType: "error" }),
),
})
}
function normalizeToolMessage(message: Message, pending: Map<string, ToolCallPart>): Message | undefined {
const content = message.content.map((part) => {
if (part.type !== "tool-result" || part.providerExecuted === true) return part
const call = pending.get(part.id)
if (call) pending.delete(part.id)
return normalizeToolResult(part, call?.name ?? part.name)
})
if (content.length === 0) return undefined
if (content.every((part, index) => part === message.content[index])) return message
return new Message({
id: message.id,
role: message.role,
content,
metadata: message.metadata,
native: message.native,
})
}
function normalizeToolResult(part: ToolResultPart, name: string): ToolResultPart {
const named = part.name === name ? part : { ...part, name }
if (named.result.type === "text" && named.result.value === "")
return { ...named, result: { type: "text", value: EMPTY_TOOL_OUTPUT } }
if (named.result.type === "error" && named.result.value === "")
return { ...named, result: { type: "error", value: EMPTY_TOOL_OUTPUT } }
if (named.result.type !== "content") return named
const value = named.result.value.filter((item) => item.type !== "text" || item.text !== "")
if (value.length === 0) return { ...named, result: { type: "text", value: EMPTY_TOOL_OUTPUT } }
if (value.length === named.result.value.length) return named
return { ...named, result: { type: "content", value } }
}
+1 -50
View File
@@ -1,7 +1,7 @@
import { describe, expect, test } from "bun:test"
import { Effect, Ref, Schema } from "effect"
import { HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
import { LLM, LLMRequest, Message, ToolCallPart, ToolDefinition, mergeProviderOptions } from "../src/index.js"
import { LLM, Message, ToolCallPart, mergeProviderOptions } from "../src/index.js"
import { AnthropicMessages, OpenAIChat } from "../src/protocols.js"
import { Auth, LLMClient } from "../src/route.js"
import { compileRequest } from "../src/route/client.js"
@@ -77,55 +77,6 @@ describe("request option precedence", () => {
}),
)
it.effect("keeps the last tool definition for duplicate names", () =>
Effect.gen(function* () {
const request = LLM.request({
model: OpenAIChat.route.model({ id: "gpt-4o-mini" }),
prompt: "Use a tool.",
})
const prepared = yield* compileRequest(
LLMRequest.update(request, {
tools: [
ToolDefinition.make({ name: "lookup", description: "old", inputSchema: { type: "object" } }),
ToolDefinition.make({ name: "search", description: "search", inputSchema: { type: "object" } }),
ToolDefinition.make({ name: "lookup", description: "new", inputSchema: { type: "object" } }),
],
}),
)
expect(prepared.body.tools).toEqual([
{
type: "function",
function: { name: "lookup", description: "new", parameters: { type: "object" }, strict: false },
},
{
type: "function",
function: { name: "search", description: "search", parameters: { type: "object" }, strict: false },
},
])
}),
)
it.effect("normalizes tool history before protocol lowering", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({
model: OpenAIChat.route.model({ id: "gpt-4o-mini" }),
messages: [
Message.assistant(ToolCallPart.make({ id: "call_1", name: "lookup", input: {} })),
Message.user("Continue."),
],
}),
)
expect(prepared.body.messages).toMatchObject([
{ role: "assistant", tool_calls: [{ id: "call_1", function: { name: "lookup" } }] },
{ role: "tool", tool_call_id: "call_1", content: "Tool result missing" },
{ role: "user", content: "Continue." },
])
}),
)
it.effect("applies model HTTP defaults before request HTTP overlays", () =>
LLMClient.generate(
LLM.request({
@@ -1,36 +0,0 @@
{
"version": 1,
"metadata": {
"model": "zai-glm-5-2",
"tags": [
"prefix:mistral-chat-glm",
"provider:mistral",
"protocol:mistral-chat",
"hosted-model",
"tool",
"tool-call"
],
"name": "mistral-chat-glm/streams-an-indexed-tool-call",
"recordedAt": "2026-08-30T17:38:02.921Z"
},
"interactions": [
{
"transport": "http",
"request": {
"method": "POST",
"url": "https://api.mistral.ai/v1/chat/completions",
"headers": {
"content-type": "application/json"
},
"body": "{\"model\":\"zai-glm-5-2\",\"messages\":[{\"role\":\"system\",\"content\":\"Call lookup_weather exactly once with Paris.\"},{\"role\":\"user\",\"content\":\"What is the weather?\"}],\"tools\":[{\"type\":\"function\",\"function\":{\"name\":\"lookup_weather\",\"description\":\"Look up the current weather for a city\",\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\",\"enum\":[\"Paris\"]}},\"required\":[\"city\"],\"additionalProperties\":false},\"strict\":false}}],\"tool_choice\":{\"type\":\"function\",\"function\":{\"name\":\"lookup_weather\"}},\"stream\":true,\"max_tokens\":256,\"temperature\":0}"
},
"response": {
"status": 200,
"headers": {
"content-type": "text/event-stream; charset=utf-8"
},
"body": "data: {\"id\":\"f139bf0e4b984e51aabf6a83c237674d\",\"object\":\"chat.completion.chunk\",\"created\":1788111482,\"model\":\"zai-glm-5-2\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"f139bf0e4b984e51aabf6a83c237674d\",\"object\":\"chat.completion.chunk\",\"created\":1788111482,\"model\":\"zai-glm-5-2\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"index\":0,\"content\":\"\"},\"finish_reason\":null,\"logprobs\":null}]}\n\ndata: {\"id\":\"f139bf0e4b984e51aabf6a83c237674d\",\"object\":\"chat.completion.chunk\",\"created\":1788111482,\"model\":\"zai-glm-5-2\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"id\":\"chatcmpl-tool-8cc4d8f9f07b298a\",\"type\":\"function\",\"function\":{\"name\":\"lookup_weather\",\"arguments\":\"{\\\"city\\\": \\\"\"},\"index\":0}],\"index\":0,\"content\":\"\"},\"finish_reason\":null,\"logprobs\":null}]}\n\ndata: {\"id\":\"f139bf0e4b984e51aabf6a83c237674d\",\"object\":\"chat.completion.chunk\",\"created\":1788111482,\"model\":\"zai-glm-5-2\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"type\":\"function\",\"function\":{\"name\":\"\",\"arguments\":\"Paris\\\"}\"},\"index\":0}],\"index\":0,\"content\":\"\"},\"finish_reason\":null,\"logprobs\":null}]}\n\ndata: {\"id\":\"f139bf0e4b984e51aabf6a83c237674d\",\"object\":\"chat.completion.chunk\",\"created\":1788111482,\"model\":\"zai-glm-5-2\",\"choices\":[{\"index\":0,\"delta\":{\"index\":0,\"content\":\"\"},\"finish_reason\":\"stop\",\"logprobs\":null}],\"usage\":{\"prompt_tokens\":171,\"total_tokens\":182,\"completion_tokens\":11,\"prompt_tokens_details\":{\"cached_tokens\":0}}}\n\ndata: [DONE]\n\n"
}
}
]
}
@@ -1,47 +0,0 @@
{
"version": 1,
"metadata": {
"model": "mistral-small-latest",
"tags": ["prefix:mistral-chat", "provider:mistral", "protocol:mistral-chat", "tool", "tool-loop", "usage"],
"name": "mistral-chat/drives-a-tool-loop",
"recordedAt": "2026-08-30T17:18:49.552Z"
},
"interactions": [
{
"transport": "http",
"request": {
"method": "POST",
"url": "https://api.mistral.ai/v1/chat/completions",
"headers": {
"content-type": "application/json"
},
"body": "{\"model\":\"mistral-small-latest\",\"messages\":[{\"role\":\"system\",\"content\":\"Call lookup_weather exactly once with Paris.\"},{\"role\":\"user\",\"content\":\"What is the weather?\"}],\"tools\":[{\"type\":\"function\",\"function\":{\"name\":\"lookup_weather\",\"description\":\"Look up the current weather for a city\",\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\",\"enum\":[\"Paris\"]}},\"required\":[\"city\"],\"additionalProperties\":false},\"strict\":false}}],\"tool_choice\":{\"type\":\"function\",\"function\":{\"name\":\"lookup_weather\"}},\"stream\":true,\"max_tokens\":160,\"temperature\":0,\"reasoning_effort\":\"none\"}"
},
"response": {
"status": 200,
"headers": {
"content-type": "text/event-stream; charset=utf-8"
},
"body": "data: {\"id\":\"07491e37a5ed48f9987f1583753a466b\",\"object\":\"chat.completion.chunk\",\"created\":1788110328,\"model\":\"mistral-small-latest\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"07491e37a5ed48f9987f1583753a466b\",\"object\":\"chat.completion.chunk\",\"created\":1788110328,\"model\":\"mistral-small-latest\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"id\":\"ffJovBNqY\",\"type\":\"function\",\"function\":{\"name\":\"lookup_weather\",\"arguments\":\"{\\\"city\\\": \\\"Paris\\\"}\"},\"index\":0}]},\"finish_reason\":\"tool_calls\"}],\"usage\":{\"prompt_tokens\":110,\"total_tokens\":122,\"completion_tokens\":12,\"prompt_tokens_details\":{\"cached_tokens\":0},\"service_tier\":\"standard\"},\"p\":\"abcdefghijklm\"}\n\ndata: [DONE]\n\n"
}
},
{
"transport": "http",
"request": {
"method": "POST",
"url": "https://api.mistral.ai/v1/chat/completions",
"headers": {
"content-type": "application/json"
},
"body": "{\"model\":\"mistral-small-latest\",\"messages\":[{\"role\":\"system\",\"content\":\"Call lookup_weather exactly once with Paris.\"},{\"role\":\"user\",\"content\":\"What is the weather?\"},{\"role\":\"assistant\",\"content\":\"\",\"tool_calls\":[{\"id\":\"ffJovBNqY\",\"type\":\"function\",\"function\":{\"name\":\"lookup_weather\",\"arguments\":\"{\\\"city\\\":\\\"Paris\\\"}\"}}]},{\"role\":\"tool\",\"tool_call_id\":\"ffJovBNqY\",\"name\":\"lookup_weather\",\"content\":\"{\\\"condition\\\":\\\"sunny\\\",\\\"temperature\\\":\\\"18C\\\"}\"}],\"tools\":[{\"type\":\"function\",\"function\":{\"name\":\"lookup_weather\",\"description\":\"Look up the current weather for a city\",\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\",\"enum\":[\"Paris\"]}},\"required\":[\"city\"],\"additionalProperties\":false},\"strict\":false}}],\"tool_choice\":\"none\",\"stream\":true,\"max_tokens\":160,\"temperature\":0,\"reasoning_effort\":\"none\"}"
},
"response": {
"status": 200,
"headers": {
"content-type": "text/event-stream; charset=utf-8"
},
"body": "data: {\"id\":\"8fcd293093b849139fc0893a48bbc7ce\",\"object\":\"chat.completion.chunk\",\"created\":1788110328,\"model\":\"mistral-small-latest\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"8fcd293093b849139fc0893a48bbc7ce\",\"object\":\"chat.completion.chunk\",\"created\":1788110328,\"model\":\"mistral-small-latest\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"The\"},\"finish_reason\":null}],\"p\":\"abcdefghijklmn\"}\n\ndata: {\"id\":\"8fcd293093b849139fc0893a48bbc7ce\",\"object\":\"chat.completion.chunk\",\"created\":1788110328,\"model\":\"mistral-small-latest\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" weather in Paris is\"},\"finish_reason\":null}],\"p\":\"abcdefghijklmn\"}\n\ndata: {\"id\":\"8fcd293093b849139fc0893a48bbc7ce\",\"object\":\"chat.completion.chunk\",\"created\":1788110328,\"model\":\"mistral-small-latest\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" currently sunny with\"},\"finish_reason\":null}],\"p\":\"abcdefghijklmnopqrstu\"}\n\ndata: {\"id\":\"8fcd293093b849139fc0893a48bbc7ce\",\"object\":\"chat.completion.chunk\",\"created\":1788110328,\"model\":\"mistral-small-latest\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" a temperature of \"},\"finish_reason\":null}],\"p\":\"abcdef\"}\n\ndata: {\"id\":\"8fcd293093b849139fc0893a48bbc7ce\",\"object\":\"chat.completion.chunk\",\"created\":1788110328,\"model\":\"mistral-small-latest\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"18°C\"},\"finish_reason\":null}],\"p\":\"abcdefghijklmnopqr\"}\n\ndata: {\"id\":\"8fcd293093b849139fc0893a48bbc7ce\",\"object\":\"chat.completion.chunk\",\"created\":1788110328,\"model\":\"mistral-small-latest\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\".\"},\"finish_reason\":\"stop\"}],\"usage\":{\"prompt_tokens\":57,\"total_tokens\":74,\"completion_tokens\":17,\"prompt_tokens_details\":{\"cached_tokens\":0},\"service_tier\":\"standard\"},\"p\":\"abcdefghijklmnopqrstuvwxyz\"}\n\ndata: [DONE]\n\n"
}
}
]
}
File diff suppressed because one or more lines are too long
@@ -1,29 +0,0 @@
{
"version": 1,
"metadata": {
"model": "mistral-small-latest",
"tags": ["prefix:mistral-chat", "provider:mistral", "protocol:mistral-chat", "text", "usage"],
"name": "mistral-chat/streams-text-with-usage",
"recordedAt": "2026-08-30T17:18:45.432Z"
},
"interactions": [
{
"transport": "http",
"request": {
"method": "POST",
"url": "https://api.mistral.ai/v1/chat/completions",
"headers": {
"content-type": "application/json"
},
"body": "{\"model\":\"mistral-small-latest\",\"messages\":[{\"role\":\"user\",\"content\":\"Reply with exactly one word: hello\"}],\"stream\":true,\"max_tokens\":40,\"temperature\":0,\"reasoning_effort\":\"none\"}"
},
"response": {
"status": 200,
"headers": {
"content-type": "text/event-stream; charset=utf-8"
},
"body": "data: {\"id\":\"9a4d16bdddb74e5e89c2cf9e9b91e065\",\"object\":\"chat.completion.chunk\",\"created\":1788110325,\"model\":\"mistral-small-latest\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"\"},\"finish_reason\":null}]}\n\ndata: {\"id\":\"9a4d16bdddb74e5e89c2cf9e9b91e065\",\"object\":\"chat.completion.chunk\",\"created\":1788110325,\"model\":\"mistral-small-latest\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"Hi\"},\"finish_reason\":null}],\"p\":\"abcdefghijklmnopqrs\"}\n\ndata: {\"id\":\"9a4d16bdddb74e5e89c2cf9e9b91e065\",\"object\":\"chat.completion.chunk\",\"created\":1788110325,\"model\":\"mistral-small-latest\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\"},\"finish_reason\":\"stop\"}],\"usage\":{\"prompt_tokens\":22,\"total_tokens\":24,\"completion_tokens\":2,\"prompt_tokens_details\":{\"cached_tokens\":0},\"service_tier\":\"standard\"},\"p\":\"abcdefghijklmnopqrstuvwxyz0\"}\n\ndata: [DONE]\n\n"
}
}
]
}
@@ -1,24 +0,0 @@
import { LLM } from "../../src/index.js"
import { Mistral } from "../../src/providers.js"
const selected = Mistral.provider.model("mistral-small-latest")
LLM.request({ model: selected, prompt: "Hello", providerOptions: { reasoningEffort: "high" } })
LLM.request({ model: selected, prompt: "Hello", providerOptions: { reasoningEffort: "future-effort" } })
LLM.request({ model: selected, prompt: "Hello", providerOptions: { promptMode: "reasoning" } })
LLM.request({ model: selected, prompt: "Hello", providerOptions: { parallelToolCalls: false } })
LLM.request({ model: selected, prompt: "Hello", providerOptions: { promptCacheKey: "session-1" } })
LLM.request({
model: selected,
prompt: "Hello",
// @ts-expect-error Mistral reasoning effort must be a string.
providerOptions: { reasoningEffort: 1 },
})
LLM.request({
model: selected,
prompt: "Hello",
// @ts-expect-error Mistral prompt mode only supports reasoning.
providerOptions: { promptMode: "standard" },
})
@@ -74,48 +74,6 @@ describe("Anthropic Messages route", () => {
}),
)
it.effect("omits empty system text while preserving whitespace", () =>
Effect.gen(function* () {
const empty = yield* compileRequest(LLMRequest.update(request, { system: [{ type: "text", text: "" }] }))
const whitespace = yield* compileRequest(LLMRequest.update(request, { system: [{ type: "text", text: " " }] }))
expect(empty.body.system).toBeUndefined()
expect(whitespace.body.system).toEqual([{ type: "text", text: " " }])
}),
)
it.effect("filters whitespace-only text and removes empty messages", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({
model,
messages: [
Message.user(" \n\t"),
Message.user([]),
Message.user([
{ type: "text", text: "" },
{ type: "text", text: " Keep this spacing. " },
{ type: "text", text: " \n\t" },
]),
Message.assistant(" \n\t"),
Message.assistant([]),
Message.assistant([{ type: "reasoning", text: "" }]),
Message.assistant([
{ type: "text", text: "" },
{ type: "reasoning", text: "", providerMetadata: { anthropic: { signature: "sig_1" } } },
]),
],
cache: "none",
}),
)
expect(prepared.body.messages).toEqual([
{ role: "user", content: [{ type: "text", text: " Keep this spacing. " }] },
{ role: "assistant", content: [{ type: "thinking", thinking: "", signature: "sig_1" }] },
])
}),
)
it.effect("lowers adaptive thinking settings with effort", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
@@ -945,54 +903,6 @@ describe("Anthropic Messages route", () => {
}),
)
it.effect("preserves a reasoning signature when message_stop closes the block", () =>
Effect.gen(function* () {
const compatible = Route.make({
id: "custom-anthropic-messages",
provider: "custom-anthropic",
protocol: AnthropicMessages.protocol,
endpoint: Endpoint.path("/messages", { baseURL: "https://compatible.test/v1" }),
auth: Auth.header("x-api-key", "test"),
framing: AnthropicMessages.framing,
}).model({ id: "custom-model" })
const body = sseEvents(
{ type: "message_start", message: { usage: { input_tokens: 5 } } },
{
type: "content_block_start",
index: 0,
content_block: { type: "thinking", thinking: "", signature: "" },
},
{ type: "content_block_delta", index: 0, delta: { type: "thinking_delta", thinking: "Reasoning." } },
{ type: "content_block_delta", index: 0, delta: { type: "signature_delta", signature: "sig_1" } },
{ type: "message_delta", delta: { stop_reason: "end_turn" }, usage: { output_tokens: 2 } },
{ type: "message_stop" },
)
const response = yield* LLMClient.generate(LLM.request({ model: compatible, prompt: "Think." })).pipe(
Effect.provide(fixedResponse(body)),
)
const reasoningEnds = response.events.filter((event) => event.type === "reasoning-end")
expect(reasoningEnds).toHaveLength(1)
expect(reasoningEnds[0]).toMatchObject({
providerMetadata: { "custom-anthropic": { signature: "sig_1" } },
})
expect(response.message.content).toEqual([
{
type: "reasoning",
text: "Reasoning.",
providerMetadata: { "custom-anthropic": { signature: "sig_1" } },
},
])
const prepared = yield* compileRequest(
LLM.request({ model: compatible, messages: [response.message], cache: "none" }),
)
expect(prepared.body.messages).toEqual([
{ role: "assistant", content: [{ type: "thinking", thinking: "Reasoning.", signature: "sig_1" }] },
])
}),
)
it.effect("parses text, reasoning, and usage stream fixtures", () =>
Effect.gen(function* () {
const body = sseEvents(
@@ -1026,7 +936,6 @@ describe("Anthropic Messages route", () => {
expect(response.events.find((event) => event.type === "reasoning-end")).toMatchObject({
providerMetadata: { anthropic: { signature: "sig_1" } },
})
expect(response.events.filter((event) => event.type === "reasoning-end")).toHaveLength(1)
expect(response.events.find((event) => event.type === "reasoning-delta" && event.text === "")).toBeUndefined()
expect(response.message.content).toEqual([
{ type: "text", text: "Hello!" },
@@ -1040,41 +949,6 @@ describe("Anthropic Messages route", () => {
}),
)
it.effect("preserves terminal state across usage-only message deltas", () =>
Effect.gen(function* () {
const response = yield* LLMClient.generate(request).pipe(
Effect.provide(
fixedResponse(
sseEvents(
{ type: "message_start", message: { usage: { input_tokens: 5 } } },
{
type: "message_delta",
delta: { stop_reason: "end_turn", stop_sequence: "X" },
usage: { output_tokens: 8 },
},
{ type: "message_delta", delta: {}, usage: { output_tokens: 10 } },
{ type: "message_stop" },
),
),
),
)
expect(response.usage).toMatchObject({ inputTokens: 5, outputTokens: 10, totalTokens: 15 })
expect(response.finishReason).toEqual({ normalized: "stop", raw: "end_turn" })
expect(response.events.find((event) => event.type === "step-finish")).toMatchObject({
reason: { normalized: "stop", raw: "end_turn" },
usage: { inputTokens: 5, outputTokens: 10, totalTokens: 15 },
providerMetadata: { anthropic: { stopSequence: "X" } },
})
expect(response.events.at(-1)).toMatchObject({
type: "finish",
reason: { normalized: "stop", raw: "end_turn" },
usage: { inputTokens: 5, outputTokens: 10, totalTokens: 15 },
providerMetadata: { anthropic: { stopSequence: "X" } },
})
}),
)
it.effect("requires message_stop before completing a streamed message", () =>
Effect.gen(function* () {
const error = yield* LLMClient.generate(request).pipe(
@@ -1,12 +1,11 @@
import { EventStreamCodec } from "@smithy/eventstream-codec"
import { fromUtf8, toUtf8 } from "@smithy/util-utf8"
import { describe, expect } from "bun:test"
import { Effect, Encoding, Ref, Stream } from "effect"
import { Effect } from "effect"
import {
CacheHint,
GenerationOptions,
LLM,
LLMEvent,
LLMRequest,
Message,
ToolCallPart,
@@ -84,17 +83,6 @@ const eventStreamBody = (...payloads: ReadonlyArray<readonly [string, object]>)
const fixedBytes = (bytes: Uint8Array) =>
fixedResponse(bytes.slice().buffer, { headers: { "content-type": "application/vnd.amazon.eventstream" } })
const fixedByteChunks = (...chunks: ReadonlyArray<Uint8Array>) =>
fixedResponse(
new ReadableStream<Uint8Array>({
start(controller) {
chunks.forEach((chunk) => controller.enqueue(chunk))
controller.close()
},
}),
{ headers: { "content-type": "application/vnd.amazon.eventstream" } },
)
const model = AmazonBedrock.configure({
baseURL: "https://bedrock-runtime.test",
apiKey: "test-bearer",
@@ -125,50 +113,6 @@ describe("Bedrock Converse route", () => {
}),
)
it.effect("omits empty initial system blocks", () =>
Effect.gen(function* () {
const empty = yield* compileRequest(LLM.request({ model, system: "", prompt: "hello" }))
const cachedEmpty = yield* compileRequest(
LLM.request({
model,
system: [{ type: "text", text: "", cache: new CacheHint({ type: "ephemeral" }) }],
prompt: "hello",
cache: "none",
}),
)
expect(empty.body.system).toBeUndefined()
expect(cachedEmpty.body.system).toBeUndefined()
}),
)
it.effect("omits empty system blocks while preserving order and cache hints", () =>
Effect.gen(function* () {
const cache = new CacheHint({ type: "ephemeral" })
const prepared = yield* compileRequest(
LLM.request({
model,
system: [
{ type: "text", text: "", cache },
{ type: "text", text: "First." },
{ type: "text", text: " " },
{ type: "text", text: "" },
{ type: "text", text: "Second.", cache },
],
prompt: "hello",
cache: "none",
}),
)
expect(prepared.body.system).toEqual([
{ text: "First." },
{ text: " " },
{ text: "Second." },
{ cachePoint: { type: "default" } },
])
}),
)
it.effect("passes topK through additionalModelRequestFields as top_k", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
@@ -311,79 +255,6 @@ describe("Bedrock Converse route", () => {
}),
)
it.effect("removes empty keys recursively from outbound tool inputs without mutating history", () =>
Effect.gen(function* () {
const input = {
path: "file.ts",
edits: [
{ oldText: "a", newText: "b", "": "" },
null,
true,
7,
"text",
["kept", { "": false, nested: { "": null, value: "ok" } }],
],
nested: { "": "drop", empty: {}, onlyEmpty: { "": 1 } },
" ": "preserve whitespace key",
"": "drop",
}
const original = structuredClone(input)
const call = ToolCallPart.make({ id: "tool_1", name: "edit", input })
const prepared = yield* compileRequest(
LLM.request({ model, messages: [Message.assistant([call])], cache: "none" }),
)
expect(prepared.body.messages).toEqual([
{
role: "assistant",
content: [
{
toolUse: {
toolUseId: "tool_1",
name: "edit",
input: {
path: "file.ts",
edits: [{ oldText: "a", newText: "b" }, null, true, 7, "text", ["kept", { nested: { value: "ok" } }]],
nested: { empty: {}, onlyEmpty: {} },
" ": "preserve whitespace key",
},
},
},
],
},
])
expect(input).toEqual(original)
expect(call.input).toBe(input)
}),
)
it.effect("keeps empty tool inputs and empties inputs containing only empty keys", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({
model,
messages: [
Message.assistant([
ToolCallPart.make({ id: "tool_empty_key", name: "first", input: { "": { value: true } } }),
ToolCallPart.make({ id: "tool_empty_object", name: "second", input: {} }),
]),
],
cache: "none",
}),
)
expect(prepared.body.messages).toEqual([
{
role: "assistant",
content: [
{ toolUse: { toolUseId: "tool_empty_key", name: "first", input: {} } },
{ toolUse: { toolUseId: "tool_empty_object", name: "second", input: {} } },
],
},
])
}),
)
it.effect("merges parallel tool results into one user message", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
@@ -514,77 +385,19 @@ describe("Bedrock Converse route", () => {
}),
)
it.effect("rejects truncated event-stream frames after message stop", () =>
it.effect("maps truncation and malformed output stop reasons", () =>
Effect.gen(function* () {
const partialFrames = [
eventFrame("metadata", { usage: { inputTokens: 5, outputTokens: 2, totalTokens: 7 } }).subarray(0, 3),
exceptionFrame("modelStreamErrorException", { originalMessage: "Upstream model failed" }).subarray(0, -1),
]
const reasons = [
["model_context_window_exceeded", "length"],
["malformed_model_output", "error"],
["malformed_tool_use", "error"],
] as const
for (const partial of partialFrames) {
const error = yield* LLMClient.generate(baseRequest).pipe(
Effect.provide(fixedBytes(concat([eventFrame("messageStop", { stopReason: "end_turn" }), partial]))),
Effect.flip,
for (const [raw, normalized] of reasons) {
const response = yield* LLMClient.generate(baseRequest).pipe(
Effect.provide(fixedBytes(eventStreamBody(["messageStop", { stopReason: raw }]))),
)
expect(error).toMatchObject({
reason: { _tag: "InvalidProviderOutput", classification: "incomplete-stream" },
message: `Incomplete Bedrock Converse event-stream frame: ${partial.length} buffered bytes remain at end of stream`,
})
expect(error.reason.body).toBe(Encoding.encodeBase64(partial))
}
}),
)
it.effect("decodes frames split across transport chunks through exact-boundary EOF", () =>
Effect.gen(function* () {
const body = eventStreamBody(
["messageStart", { role: "assistant" }],
["contentBlockDelta", { contentBlockIndex: 0, delta: { text: "Hello" } }],
["messageStop", { stopReason: "end_turn" }],
)
const response = yield* LLMClient.generate(baseRequest).pipe(
Effect.provide(fixedByteChunks(body.subarray(0, 2), body.subarray(2, 17), body.subarray(17))),
)
expect(response.text).toBe("Hello")
expect(response.finishReason).toEqual({ normalized: "stop", raw: "end_turn" })
}),
)
it.effect("maps model context window exhaustion to length", () =>
Effect.gen(function* () {
const response = yield* LLMClient.generate(baseRequest).pipe(
Effect.provide(fixedBytes(eventStreamBody(["messageStop", { stopReason: "model_context_window_exceeded" }]))),
)
expect(response.finishReason).toEqual({
normalized: "length",
raw: "model_context_window_exceeded",
})
}),
)
it.effect("fails malformed output stop reasons", () =>
Effect.gen(function* () {
for (const reason of ["malformed_model_output", "malformed_tool_use"] as const) {
const events = yield* Ref.make<ReadonlyArray<LLMEvent>>([])
const error = yield* LLMClient.stream(baseRequest).pipe(
Stream.tap((event) => Ref.update(events, (current) => [...current, event])),
Stream.runDrain,
Effect.provide(fixedBytes(eventStreamBody(["messageStop", { stopReason: reason }]))),
Effect.flip,
)
expect(error).toMatchObject({
reason: { _tag: "InvalidProviderOutput" },
message: `Bedrock Converse stopped with ${reason}`,
})
expect(JSON.parse(error.reason.body ?? "")).toMatchObject({
headers: { ":event-type": { value: "messageStop" } },
body: JSON.stringify({ stopReason: reason }),
})
expect((yield* Ref.get(events)).some((event) => event.type === "finish")).toBeFalse()
expect(response.finishReason).toEqual({ normalized, raw })
}
}),
)
@@ -1040,7 +853,7 @@ describe("Bedrock Converse route", () => {
Effect.gen(function* () {
// Bedrock represents redactedContent blobs as base64 strings on its JSON
// wire. The provider owns the payload and requires byte-exact replay.
const redactedData = "AQID"
const redactedData = "cmVkYWN0ZWQtdGhpbmtpbmc="
const response = yield* LLMClient.generate(
LLMRequest.update(baseRequest, {
tools: [ToolDefinition.make({ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } })],
@@ -1050,8 +863,10 @@ describe("Bedrock Converse route", () => {
fixedBytes(
eventStreamBody(
["messageStart", { role: "assistant" }],
["contentBlockDelta", { contentBlockIndex: 0, delta: { reasoningContent: { redactedContent: "AQ==" } } }],
["contentBlockDelta", { contentBlockIndex: 0, delta: { reasoningContent: { redactedContent: "AgM=" } } }],
[
"contentBlockDelta",
{ contentBlockIndex: 0, delta: { reasoningContent: { redactedContent: redactedData } } },
],
["contentBlockStop", { contentBlockIndex: 0 }],
[
"contentBlockStart",
@@ -1067,17 +882,12 @@ describe("Bedrock Converse route", () => {
),
),
)
expect(response.events.filter((event) => event.type === "reasoning-delta" && event.text === "").at(-1)).toEqual({
expect(response.events.find((event) => event.type === "reasoning-delta" && event.text === "")).toEqual({
type: "reasoning-delta",
id: "reasoning-0",
text: "",
providerMetadata: { bedrock: { redactedData } },
})
expect(response.events.find((event) => event.type === "reasoning-end")).toEqual({
type: "reasoning-end",
id: "reasoning-0",
providerMetadata: { bedrock: { redactedData } },
})
const prepared = yield* compileRequest(
LLM.request({
model,
@@ -1108,73 +918,6 @@ describe("Bedrock Converse route", () => {
}),
)
it.effect("keeps redacted reasoning accumulation separate by content block index", () =>
Effect.gen(function* () {
const response = yield* LLMClient.generate(baseRequest).pipe(
Effect.provide(
fixedBytes(
eventStreamBody(
["messageStart", { role: "assistant" }],
["contentBlockDelta", { contentBlockIndex: 2, delta: { reasoningContent: { redactedContent: "AQ==" } } }],
["contentBlockDelta", { contentBlockIndex: 2, delta: { reasoningContent: { redactedContent: "Ag==" } } }],
["contentBlockStop", { contentBlockIndex: 2 }],
["contentBlockDelta", { contentBlockIndex: 7, delta: { reasoningContent: { redactedContent: "Aw==" } } }],
["contentBlockDelta", { contentBlockIndex: 7, delta: { reasoningContent: { redactedContent: "BA==" } } }],
["contentBlockStop", { contentBlockIndex: 7 }],
["messageStop", { stopReason: "end_turn" }],
),
),
),
)
expect(response.message.content).toEqual([
{ type: "reasoning", text: "", providerMetadata: { bedrock: { redactedData: "AQI=" } } },
{ type: "reasoning", text: "", providerMetadata: { bedrock: { redactedData: "AwQ=" } } },
])
}),
)
it.effect("preserves split redacted reasoning when contentBlockStop is missing", () =>
Effect.gen(function* () {
const response = yield* LLMClient.generate(baseRequest).pipe(
Effect.provide(
fixedBytes(
eventStreamBody(
["messageStart", { role: "assistant" }],
["contentBlockDelta", { contentBlockIndex: 0, delta: { reasoningContent: { redactedContent: "AQ==" } } }],
["contentBlockDelta", { contentBlockIndex: 0, delta: { reasoningContent: { redactedContent: "AgM=" } } }],
["messageStop", { stopReason: "end_turn" }],
),
),
),
)
expect(response.message.content).toEqual([
{ type: "reasoning", text: "", providerMetadata: { bedrock: { redactedData: "AQID" } } },
])
}),
)
it.effect("rejects invalid redacted reasoning base64 with the triggering event", () =>
Effect.gen(function* () {
const payload = { contentBlockIndex: 0, delta: { reasoningContent: { redactedContent: "%%==" } } }
const error = yield* LLMClient.generate(baseRequest).pipe(
Effect.provide(fixedBytes(eventStreamBody(["contentBlockDelta", payload]))),
Effect.flip,
)
expect(error).toMatchObject({
reason: { _tag: "InvalidProviderOutput" },
message: "Bedrock Converse reasoningContent.redactedContent contains invalid base64 data",
})
expect(JSON.parse(error.reason.body ?? "")).toMatchObject({
headers: { ":event-type": { value: "contentBlockDelta" } },
body: JSON.stringify(payload),
})
expect(error.reason.cause).toBeInstanceOf(Error)
}),
)
it.effect("ignores unknown normal stream events", () =>
Effect.gen(function* () {
const body = concat([
@@ -1429,20 +1172,6 @@ describe("Bedrock Converse route", () => {
}),
)
it.effect("rejects image media that is not valid base64", () =>
Effect.gen(function* () {
const error = yield* compileRequest(
LLM.request({
model,
messages: [Message.user({ type: "media", mediaType: "image/png", data: "https://example.test/image.png" })],
}),
).pipe(Effect.flip)
expect(error).toMatchObject({ reason: { _tag: "InvalidRequest" } })
expect(error.message).toContain("Bedrock Converse media data must be valid base64")
}),
)
it.effect("lowers document media into Bedrock document blocks with format and name", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
@@ -1566,37 +1295,6 @@ describe("Bedrock Converse route", () => {
}),
)
it.effect("rejects remote media URLs in tool results", () =>
Effect.gen(function* () {
const error = yield* compileRequest(
LLM.request({
model,
messages: [
Message.assistant([ToolCallPart.make({ id: "call_1", name: "read", input: {} })]),
Message.tool({
id: "call_1",
name: "read",
result: {
type: "content",
value: [
{
type: "file",
uri: "https://example.test/report.pdf",
mime: "application/pdf",
name: "report.pdf",
},
],
},
}),
],
}),
).pipe(Effect.flip)
expect(error).toMatchObject({ reason: { _tag: "InvalidRequest" } })
expect(error.message).toContain("Bedrock Converse media data must be valid base64")
}),
)
it.effect("rejects unsupported image media types", () =>
Effect.gen(function* () {
const error = yield* compileRequest(
-138
View File
@@ -906,94 +906,6 @@ describe("Gemini route", () => {
}),
)
it.effect("assigns unique ids to separated reasoning blocks", () =>
Effect.gen(function* () {
const body = sseEvents(
{
candidates: [
{
content: {
role: "model",
parts: [{ text: "A", thought: true, thoughtSignature: "reasoning_sig_a" }],
},
},
],
},
{
candidates: [
{
content: { role: "model", parts: [{ text: "X", thoughtSignature: "text_sig_x" }] },
},
],
},
{
candidates: [
{
content: {
role: "model",
parts: [{ text: "B", thought: true, thoughtSignature: "reasoning_sig_b" }],
},
},
],
},
{
candidates: [
{
content: { role: "model", parts: [{ text: "Y", thoughtSignature: "text_sig_y" }] },
finishReason: "STOP",
},
],
},
)
const response = yield* LLMClient.generate(request).pipe(Effect.provide(fixedResponse(body)))
const starts = response.events.filter((event) => event.type === "reasoning-start")
const deltas = response.events.filter((event) => event.type === "reasoning-delta")
const ends = response.events.filter((event) => event.type === "reasoning-end")
expect(starts.map((event) => event.id)).toEqual(["reasoning-0", "reasoning-1"])
expect(starts[0]?.id).not.toBe(starts[1]?.id)
expect(deltas.map((event) => ({ id: event.id, text: event.text }))).toEqual([
{ id: "reasoning-0", text: "A" },
{ id: "reasoning-1", text: "B" },
])
expect(ends.map((event) => event.id)).toEqual(["reasoning-0", "reasoning-1"])
expect(response.events.filter((event) => event.type === "text-start").map((event) => event.id)).toEqual([
"text-0",
"text-1",
])
expect(response.events.filter((event) => event.type === "text-delta").map((event) => event.id)).toEqual([
"text-0",
"text-1",
])
expect(response.events.filter((event) => event.type === "text-end").map((event) => event.id)).toEqual([
"text-0",
"text-1",
])
expect(response.message.content).toEqual([
{
type: "reasoning",
text: "A",
providerMetadata: { google: { thoughtSignature: "reasoning_sig_a" } },
},
{
type: "text",
text: "X",
providerMetadata: { google: { thoughtSignature: "text_sig_x" } },
},
{
type: "reasoning",
text: "B",
providerMetadata: { google: { thoughtSignature: "reasoning_sig_b" } },
},
{
type: "text",
text: "Y",
providerMetadata: { google: { thoughtSignature: "text_sig_y" } },
},
])
}),
)
it.effect("ignores unknown response parts", () =>
Effect.gen(function* () {
const response = yield* LLMClient.generate(request).pipe(
@@ -1368,56 +1280,6 @@ describe("Gemini route", () => {
}),
)
it.effect("separates text blocks around streamed tool calls", () =>
Effect.gen(function* () {
const body = sseEvents({
candidates: [
{
content: {
role: "model",
parts: [
{ text: "before" },
{ functionCall: { id: "call_1", name: "lookup", args: { query: "weather" } } },
{ text: "after" },
],
},
finishReason: "STOP",
},
],
})
const response = yield* LLMClient.generate(
LLMRequest.update(request, {
tools: [ToolDefinition.make({ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } })],
}),
).pipe(Effect.provide(fixedResponse(body)))
expect(response.events.slice(1, 8)).toEqual([
{ type: "text-start", id: "text-0" },
{ type: "text-delta", id: "text-0", text: "before" },
{ type: "text-end", id: "text-0" },
{
type: "tool-call",
id: "call_1",
name: "lookup",
input: { query: "weather" },
providerExecuted: undefined,
providerMetadata: undefined,
},
{ type: "text-start", id: "text-1" },
{ type: "text-delta", id: "text-1", text: "after" },
{ type: "text-end", id: "text-1" },
])
const textStarts = response.events.filter((event) => event.type === "text-start")
expect(textStarts[0]?.id).not.toBe(textStarts[1]?.id)
expect(response.message.content).toEqual([
{ type: "text", text: "before" },
{ type: "tool-call", id: "call_1", name: "lookup", input: { query: "weather" } },
{ type: "text", text: "after" },
])
expect(response.finishReason).toEqual({ normalized: "tool-calls", raw: "STOP" })
}),
)
it.effect("defaults omitted function call args to an empty object", () =>
Effect.gen(function* () {
const response = yield* LLMClient.generate(
@@ -1,694 +0,0 @@
import { describe, expect, test } from "bun:test"
import { ConfigProvider, Effect } from "effect"
import { HttpClientRequest } from "effect/unstable/http"
import { LLM, LLMEvent, Message, ToolDefinition } from "../../src/index.js"
import { Mistral } from "../../src/providers/index.js"
import { MistralChat } from "../../src/protocols/index.js"
import { LLMClient } from "../../src/route.js"
import { compileRequest } from "../../src/route/client.js"
import { it } from "../lib/effect.js"
import { dynamicResponse, fixedResponse } from "../lib/http.js"
import { sseEvents } from "../lib/sse.js"
const model = Mistral.configure({ apiKey: "fixture" }).model("mistral-large-latest")
const request = LLM.request({ model, prompt: "Hello" })
const chunk = (delta: object, finishReason: string | null = null, usage?: object) => ({
choices: [{ delta, finish_reason: finishReason }],
usage,
})
describe("Mistral Chat", () => {
test("exposes native provider and protocol identities", async () => {
const entrypoint = await import("@opencode-ai/ai/providers/mistral")
expect(Mistral.id).toBe("mistral")
expect(MistralChat.protocol.id).toBe("mistral-chat")
expect(Mistral.route).toMatchObject({
id: "mistral-chat",
provider: "mistral",
providerMetadataKey: "mistral",
protocol: "mistral-chat",
})
expect(Mistral.route.endpoint).toMatchObject({
baseURL: "https://api.mistral.ai/v1",
path: "/chat/completions",
})
expect(entrypoint.model).toBeFunction()
})
it.effect("lowers native messages, media, tool choice, options, and replay IDs", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({
model,
system: "Initial",
messages: [
Message.system("Updated"),
Message.user([
{ type: "text", text: "Inspect" },
{ type: "media", mediaType: "image/png", data: "aW1hZ2U=" },
{ type: "media", mediaType: "application/pdf", data: "cGRm" },
]),
Message.assistant([
{ type: "reasoning", text: "Think" },
{ type: "text", text: "Calling" },
{ type: "tool-call", id: "call.same-prefix-1", name: "lookup", input: { city: "Paris" } },
{ type: "tool-call", id: "call.same-prefix-2", name: "other", input: {} },
]),
Message.tool({ id: "call.same-prefix-1", name: "lookup", result: { ok: true } }),
],
tools: [
ToolDefinition.make({ name: "lookup", description: "Look up a city", inputSchema: { type: "object" } }),
ToolDefinition.make({ name: "other", description: "Other operation", inputSchema: { type: "object" } }),
],
toolChoice: "lookup",
promptCacheKey: "session-1",
generation: {
maxTokens: 64,
seed: 7,
temperature: 0.2,
topP: 0.8,
frequencyPenalty: 0.1,
presencePenalty: 0.3,
stop: ["done"],
},
providerOptions: {
safePrompt: true,
documentImageLimit: 3,
documentPageLimit: 8,
parallelToolCalls: false,
reasoningEffort: "high",
},
}),
)
expect(prepared.body).toMatchObject({
model: "mistral-large-latest",
tools: [{ function: { name: "lookup", strict: false } }, { function: { name: "other", strict: false } }],
tool_choice: { type: "function", function: { name: "lookup" } },
stream: true,
max_tokens: 64,
random_seed: 7,
temperature: 0.2,
top_p: 0.8,
frequency_penalty: 0.1,
presence_penalty: 0.3,
stop: ["done"],
prompt_cache_key: "session-1",
safe_prompt: true,
document_image_limit: 3,
document_page_limit: 8,
parallel_tool_calls: false,
reasoning_effort: "high",
})
expect(prepared.body.messages.slice(0, 4)).toMatchObject([
{ role: "system", content: "Initial" },
{ role: "user", content: "<system-update>\nUpdated\n</system-update>" },
{
role: "user",
content: [
{ type: "text", text: "Inspect" },
{ type: "image_url", image_url: "data:image/png;base64,aW1hZ2U=" },
{ type: "document_url", document_url: "data:application/pdf;base64,cGRm" },
],
},
{
role: "assistant",
content: "ThinkCalling",
},
])
const assistant = prepared.body.messages[3]
const toolResult = prepared.body.messages[4]
expect(assistant?.role).toBe("assistant")
expect(toolResult?.role).toBe("tool")
if (assistant?.role !== "assistant" || toolResult?.role !== "tool") return
const ids = assistant.tool_calls?.map((tool) => tool.id) ?? []
expect(ids).toHaveLength(2)
expect(ids[0]).toMatch(/^[A-Za-z0-9]{9}$/)
expect(ids[1]).toMatch(/^[A-Za-z0-9]{9}$/)
expect(ids[0]).not.toBe(ids[1])
expect(toolResult.tool_call_id).toBe(ids[0])
expect(toolResult.name).toBe("lookup")
}),
)
it.effect("preserves valid replay IDs", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({
model,
messages: [
Message.assistant({ type: "tool-call", id: "Ab12Cd34E", name: "lookup", input: {} }),
Message.tool({ id: "Ab12Cd34E", name: "lookup", result: "ok" }),
],
}),
)
expect(prepared.body.messages).toMatchObject([
{ tool_calls: [{ id: "Ab12Cd34E" }] },
{ tool_call_id: "Ab12Cd34E" },
])
}),
)
it.effect("applies trailing prefix, cache, and reasoning options without changing earlier assistants", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({
model,
promptCacheKey: "common-key",
messages: [Message.assistant("Earlier"), Message.user("Continue"), Message.assistant("Prefix")],
providerOptions: { promptCacheKey: "native-key", promptMode: "reasoning" },
}),
)
expect(prepared.body.prompt_cache_key).toBe("native-key")
expect(prepared.body.prompt_mode).toBe("reasoning")
expect(prepared.body.messages).toEqual([
{ role: "assistant", content: "Earlier" },
{ role: "user", content: "Continue" },
{ role: "assistant", content: "Prefix", prefix: true },
])
const uncached = yield* compileRequest(
LLM.request({
model,
prompt: "Hello",
promptCacheKey: "common-key",
cache: "none",
providerOptions: { promptCacheKey: "native-key" },
}),
)
expect(uncached.body.prompt_cache_key).toBeUndefined()
const longKey = "cache-key-".repeat(10)
const unbounded = yield* compileRequest(
LLM.request({
model,
prompt: "Hello",
promptCacheKey: longKey,
}),
)
expect(unbounded.body.prompt_cache_key).toBe(longKey)
const conflict = yield* compileRequest(
LLM.request({
model,
prompt: "Hello",
providerOptions: { reasoningEffort: "high", promptMode: "reasoning" },
}),
).pipe(Effect.flip)
expect(conflict.message).toContain("mutually exclusive")
}),
)
it.effect("omits empty assistant history unless it carries a tool call", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({
model,
messages: [
Message.assistant(" \n "),
Message.assistant({ type: "reasoning", text: "\t" }),
Message.assistant({ type: "tool-call", id: "Ab12Cd34E", name: "lookup", input: {} }),
],
}),
)
expect(prepared.body.messages).toEqual([
{
role: "assistant",
content: "",
tool_calls: [{ id: "Ab12Cd34E", type: "function", function: { name: "lookup", arguments: "{}" } }],
},
])
}),
)
it.effect("preserves remote media URLs and structured tool-result media", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({
model,
messages: [
Message.user({
type: "media",
mediaType: "image/png",
data: "https://assets.example.test/input.png",
}),
Message.tool({
id: "Ab12Cd34E",
name: "inspect",
resultType: "content",
result: [
{ type: "text", text: "Result" },
{ type: "file", mime: "image/jpeg", uri: "https://assets.example.test/output.jpg" },
{ type: "file", mime: "application/pdf", uri: "cGRm" },
],
}),
],
}),
)
expect(prepared.body.messages).toEqual([
{
role: "user",
content: [{ type: "image_url", image_url: "https://assets.example.test/input.png" }],
},
{
role: "tool",
tool_call_id: "Ab12Cd34E",
name: "inspect",
content: [
{ type: "text", text: "Result" },
{ type: "image_url", image_url: "https://assets.example.test/output.jpg" },
{ type: "document_url", document_url: "data:application/pdf;base64,cGRm" },
],
},
])
}),
)
it.effect("concatenates text-only user and tool content without separators", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({
model,
messages: [
Message.user([
{ type: "text", text: "first" },
{ type: "text", text: "second" },
]),
Message.tool({
id: "Ab12Cd34E",
name: "lookup",
resultType: "content",
result: [
{ type: "text", text: "third" },
{ type: "text", text: "fourth" },
],
}),
],
}),
)
expect(prepared.body.messages).toEqual([
{ role: "user", content: "firstsecond" },
{ role: "tool", tool_call_id: "Ab12Cd34E", name: "lookup", content: "thirdfourth" },
])
}),
)
it.effect("streams ordered thinking and text and replays native thinking metadata", () =>
Effect.gen(function* () {
const response = yield* LLMClient.generate(request).pipe(
Effect.provide(
fixedResponse(
sseEvents(
chunk({ content: [{ type: "thinking", thinking: [], marker: "empty" }] }),
chunk({ content: [{ type: "thinking", thinking: [{ type: "text", text: "Consider" }] }] }),
chunk({ content: [{ type: "text", text: "Answer" }] }),
chunk({}, "stop"),
),
),
),
)
expect(response.reasoning).toBe("Consider")
expect(response.text).toBe("Answer")
expect(response.message.content).toEqual([
{
type: "reasoning",
text: "Consider",
providerMetadata: {
mistral: {
thinking: {
type: "thinking",
thinking: [{ type: "text", text: "Consider" }],
marker: "empty",
},
},
},
},
{ type: "text", text: "Answer" },
])
const replay = yield* compileRequest(LLM.request({ model, messages: [response.message] }))
expect(replay.body.messages).toEqual([
{
role: "assistant",
content: [
{
type: "thinking",
thinking: [{ type: "text", text: "Consider" }],
marker: "empty",
},
{ type: "text", text: "Answer" },
],
prefix: true,
},
])
}),
)
it.effect("replays metadata-only native thinking", () =>
Effect.gen(function* () {
const response = yield* LLMClient.generate(request).pipe(
Effect.provide(
fixedResponse(
sseEvents(chunk({ content: [{ type: "thinking", thinking: [], marker: "opaque" }] }), chunk({}, "stop")),
),
),
)
expect(response.message.content).toEqual([
{
type: "reasoning",
text: "",
providerMetadata: {
mistral: { thinking: { type: "thinking", thinking: [], marker: "opaque" } },
},
},
])
const replay = yield* compileRequest(LLM.request({ model, messages: [response.message] }))
expect(replay.body.messages).toEqual([
{
role: "assistant",
content: [{ type: "thinking", thinking: [], marker: "opaque" }],
prefix: true,
},
])
}),
)
it.effect("merges indexed argument fragments with missing continuation identity", () =>
Effect.gen(function* () {
const response = yield* LLMClient.generate(request).pipe(
Effect.provide(
fixedResponse(
sseEvents(
chunk({
tool_calls: [{ index: 0, id: "Ab12Cd34E", function: { name: "lookup", arguments: '{"city":' } }],
}),
chunk({ tool_calls: [{ index: 0, function: { name: "", arguments: '"Paris"}' } }] }),
chunk({}, "tool_calls"),
),
),
),
)
expect(response.message.content).toContainEqual({
type: "tool-call",
id: "Ab12Cd34E",
name: "lookup",
input: { city: "Paris" },
})
expect(
response.events.filter(
(event) =>
LLMEvent.is.toolInputStart(event) ||
LLMEvent.is.toolInputDelta(event) ||
LLMEvent.is.toolInputEnd(event) ||
LLMEvent.is.toolCall(event),
),
).toEqual([
{ type: "tool-input-start", id: "Ab12Cd34E", name: "lookup", providerMetadata: undefined },
{
type: "tool-input-delta",
id: "Ab12Cd34E",
name: "lookup",
text: '{"city":',
input: {},
},
{
type: "tool-input-delta",
id: "Ab12Cd34E",
name: "lookup",
text: '"Paris"}',
input: { city: "Paris" },
},
{ type: "tool-input-end", id: "Ab12Cd34E", name: "lookup", providerMetadata: undefined },
{
type: "tool-call",
id: "Ab12Cd34E",
name: "lookup",
input: { city: "Paris" },
providerExecuted: undefined,
providerMetadata: undefined,
},
])
expect(response.events.filter(LLMEvent.is.toolCall)).toHaveLength(1)
}),
)
it.effect("normalizes stop to tool calls when a hosted model emits indexed tool fragments", () =>
Effect.gen(function* () {
const response = yield* LLMClient.generate(request).pipe(
Effect.provide(
fixedResponse(
sseEvents(
chunk({
tool_calls: [
{
index: 0,
id: "chatcmpl-tool-8cc4d8f9f07b298a",
function: { name: "lookup", arguments: '{"city":"' },
},
],
}),
chunk({ tool_calls: [{ index: 0, function: { name: "", arguments: 'Paris"}' } }] }),
chunk({}, "stop"),
),
),
),
)
expect(response.finishReason).toEqual({ normalized: "tool-calls", raw: "stop" })
expect(response.toolCalls).toMatchObject([{ name: "lookup", input: { city: "Paris" } }])
expect(response.events.filter(LLMEvent.is.toolCall)).toHaveLength(1)
}),
)
it.effect("generates a stable ID when the first indexed fragment has null identity", () =>
Effect.gen(function* () {
const response = yield* LLMClient.generate(request).pipe(
Effect.provide(
fixedResponse(
sseEvents(
chunk({
tool_calls: [{ index: 0, id: null, function: { name: "lookup", arguments: { city: "Paris" } } }],
}),
chunk({}, "tool_calls"),
),
),
),
)
const call = response.message.content.find((part) => part.type === "tool-call")
expect(call?.id).toMatch(/^[A-Za-z0-9]{9}$/)
expect(call).toMatchObject({ name: "lookup", input: { city: "Paris" } })
}),
)
it.effect("generates distinct IDs for parallel null and literal-null identities", () =>
Effect.gen(function* () {
const response = yield* LLMClient.generate(request).pipe(
Effect.provide(
fixedResponse(
sseEvents(
chunk({
tool_calls: [
{ index: 0, id: null, function: { name: "first", arguments: {} } },
{ index: 1, id: "null", function: { name: "second", arguments: {} } },
],
}),
chunk({}, "tool_calls"),
),
),
),
)
const calls = response.message.content.filter((part) => part.type === "tool-call")
expect(calls).toHaveLength(2)
expect(calls[0]?.id).toMatch(/^[A-Za-z0-9]{9}$/)
expect(calls[1]?.id).toMatch(/^[A-Za-z0-9]{9}$/)
expect(calls[0]?.id).not.toBe(calls[1]?.id)
}),
)
it.effect("keeps parallel indexed calls independent", () =>
Effect.gen(function* () {
const response = yield* LLMClient.generate(request).pipe(
Effect.provide(
fixedResponse(
sseEvents(
chunk({
tool_calls: [
{ index: 0, id: "Ab12Cd34E", function: { name: "first", arguments: '{"n":' } },
{ index: 1, id: "Fg56Hi78J", function: { name: "second", arguments: '{"n":' } },
],
}),
chunk({
tool_calls: [
{ index: 0, function: { arguments: "1}" } },
{ index: 1, function: { arguments: "2}" } },
],
}),
chunk({}, "tool_calls"),
),
),
),
)
expect(response.message.content.filter((part) => part.type === "tool-call")).toEqual([
{ type: "tool-call", id: "Ab12Cd34E", name: "first", input: { n: 1 } },
{ type: "tool-call", id: "Fg56Hi78J", name: "second", input: { n: 2 } },
])
}),
)
it.effect("correlates parallel identity-less fragments by batch position", () =>
Effect.gen(function* () {
const response = yield* LLMClient.generate(request).pipe(
Effect.provide(
fixedResponse(
sseEvents(
chunk({
tool_calls: [
{ function: { name: "first", arguments: '{"n":' } },
{ function: { name: "second", arguments: '{"n":' } },
],
}),
chunk({
tool_calls: [{ function: { arguments: "1}" } }, { function: { arguments: "2}" } }],
}),
chunk({}, "tool_calls"),
),
),
),
)
expect(response.message.content.filter((part) => part.type === "tool-call")).toMatchObject([
{ name: "first", input: { n: 1 } },
{ name: "second", input: { n: 2 } },
])
}),
)
it.effect("maps usage variants and clamps cache reads", () =>
Effect.gen(function* () {
for (const usage of [
{ prompt_tokens: 5, completion_tokens: 2, total_tokens: 7, num_cached_tokens: 9 },
{ prompt_tokens: 5, completion_tokens: 2, prompt_token_details: { cached_tokens: 2 } },
{ prompt_tokens: 5, completion_tokens: 2, prompt_tokens_details: { cached_tokens: 3 } },
]) {
const response = yield* LLMClient.generate(request).pipe(
Effect.provide(fixedResponse(sseEvents(chunk({}, "stop", usage)))),
)
expect(response.usage).toMatchObject({
inputTokens: 5,
outputTokens: 2,
totalTokens: 7,
})
expect(response.usage?.cacheReadInputTokens).toBe(
Math.min(
5,
usage.num_cached_tokens ??
usage.prompt_token_details?.cached_tokens ??
usage.prompt_tokens_details?.cached_tokens ??
0,
),
)
}
}),
)
it.effect("maps finish reasons and does not finalize truncated tool calls", () =>
Effect.gen(function* () {
for (const [raw, normalized] of [
["stop", "stop"],
["model_length", "length"],
["tool_calls", "tool-calls"],
["error", "error"],
["future_reason", "unknown"],
] as const) {
const response = yield* LLMClient.generate(request).pipe(
Effect.provide(fixedResponse(sseEvents(chunk({}, raw)))),
)
expect(response.finishReason).toEqual({ normalized, raw })
}
const truncated = yield* LLMClient.generate(request).pipe(
Effect.provide(
fixedResponse(
sseEvents(
chunk({
tool_calls: [{ index: 0, id: "Ab12Cd34E", function: { name: "lookup", arguments: '{"city":' } }],
}),
chunk({}, "length"),
),
),
),
)
expect(truncated.finishReason).toEqual({ normalized: "length", raw: "length" })
expect(truncated.events.some(LLMEvent.is.toolCall)).toBe(false)
expect(truncated.events.some(LLMEvent.is.toolInputEnd)).toBe(false)
}),
)
it.effect("ignores non-text output parts and rejects invalid stream endings", () =>
Effect.gen(function* () {
const response = yield* LLMClient.generate(request).pipe(
Effect.provide(
fixedResponse(
sseEvents(
chunk({ content: null }),
chunk({
content: [
{ type: "reference", reference_ids: [1] },
{ type: "image_url", image_url: "https://example.test/image.png" },
{ type: "text", text: "Answer" },
],
}),
chunk({}, "stop"),
),
),
),
)
expect(response.text).toBe("Answer")
const missingFinish = yield* LLMClient.generate(request).pipe(
Effect.provide(fixedResponse(sseEvents(chunk({ content: "partial" })))),
Effect.flip,
)
expect(missingFinish.message).toContain("without finish_reason")
const lateContent = yield* LLMClient.generate(request).pipe(
Effect.provide(
fixedResponse(sseEvents(chunk({}, "stop"), chunk({ content: [{ type: "text", text: "late" }] }))),
),
Effect.flip,
)
expect(lateContent.message).toContain("content after the finish reason")
}),
)
it.effect("uses environment bearer auth and custom package settings", () =>
LLMClient.generate(
LLM.request({
model: Mistral.model("fixture-model", {
baseURL: "https://mistral.test/v1",
headers: { "x-app": "test" },
body: { service_tier: "priority" },
providerOptions: { safePrompt: true },
}),
prompt: "Hello",
}),
).pipe(
Effect.provide(
dynamicResponse((input) =>
Effect.gen(function* () {
const web = yield* HttpClientRequest.toWeb(input.request).pipe(Effect.orDie)
expect(web.url).toBe("https://mistral.test/v1/chat/completions")
expect(web.headers.get("authorization")).toBe("Bearer secret")
expect(web.headers.get("x-app")).toBe("test")
expect(input.text).toContain('"service_tier":"priority"')
return input.respond(sseEvents(chunk({}, "stop")), { headers: { "content-type": "text/event-stream" } })
}),
),
),
Effect.provide(ConfigProvider.layer(ConfigProvider.fromEnv({ env: { MISTRAL_API_KEY: "secret" } }))),
),
)
})
@@ -1,159 +0,0 @@
import { configure } from "@opencode-ai/ai/providers/mistral"
import { describe, expect } from "bun:test"
import { Effect } from "effect"
import { LLM, LLMEvent, LLMRequest, Message, ToolChoice, ToolDefinition } from "../../src/index.js"
import { LLMClient } from "../../src/route.js"
import { compileRequest } from "../../src/route/client.js"
import { recordedTests } from "../recorded-test.js"
const apiKey = process.env.MISTRAL_API_KEY ?? "fixture"
const recorded = recordedTests({
prefix: "mistral-chat",
provider: "mistral",
protocol: "mistral-chat",
requires: ["MISTRAL_API_KEY"],
})
const glmRecorded = recordedTests({
prefix: "mistral-chat-glm",
provider: "mistral",
protocol: "mistral-chat",
requires: ["MISTRAL_API_KEY"],
})
const weather = ToolDefinition.make({
name: "lookup_weather",
description: "Look up the current weather for a city",
inputSchema: {
type: "object",
properties: { city: { type: "string", enum: ["Paris"] } },
required: ["city"],
additionalProperties: false,
},
})
describe("Mistral recorded", () => {
recorded.effect.with(
"streams text with usage",
{ tags: ["text", "usage"], metadata: { model: "mistral-small-latest" } },
() =>
Effect.gen(function* () {
const response = yield* LLMClient.generate(
LLM.request({
model: configure({ apiKey, providerOptions: { reasoningEffort: "none" } }).model("mistral-small-latest"),
prompt: "Reply with exactly one word: hello",
generation: { maxTokens: 40, temperature: 0 },
}),
)
expect(response.text.trim()).toMatch(/^(?:hello|hi)[!.]?$/i)
expect(response.finishReason.normalized).toBe("stop")
expect(response.usage?.inputTokens).toBeGreaterThan(0)
expect(response.usage?.outputTokens).toBeGreaterThan(0)
}),
60_000,
)
recorded.effect.with(
"replays native reasoning",
{ tags: ["reasoning", "replay", "usage"], metadata: { model: "mistral-small-latest" } },
() =>
Effect.gen(function* () {
const model = configure({ apiKey, providerOptions: { reasoningEffort: "high" } }).model("mistral-small-latest")
const firstRequest = LLM.request({
model,
prompt: "Calculate 17 multiplied by 23. Think briefly, then reply with only the integer.",
generation: { maxTokens: 512, temperature: 0 },
})
const first = yield* LLMClient.generate(firstRequest)
expect(first.text.trim()).toBe("391")
expect(first.reasoning.length).toBeGreaterThan(0)
expect(first.events.some(LLMEvent.is.reasoningDelta)).toBe(true)
const followUp = LLMRequest.update(firstRequest, {
messages: [...firstRequest.messages, first.message, Message.user("Reply with exactly: Done.")],
generation: { maxTokens: 256, temperature: 0 },
})
const replay = yield* compileRequest(followUp)
expect(replay.body.messages).toContainEqual(
expect.objectContaining({
role: "assistant",
content: expect.arrayContaining([expect.objectContaining({ type: "thinking" })]),
}),
)
const second = yield* LLMClient.generate(followUp)
expect(second.text.trim()).toMatch(/Done\.?$/)
expect(second.finishReason.normalized).toBe("stop")
}),
60_000,
)
recorded.effect.with(
"drives a tool loop",
{ tags: ["tool", "tool-loop", "usage"], metadata: { model: "mistral-small-latest" } },
() =>
Effect.gen(function* () {
const model = configure({ apiKey, providerOptions: { reasoningEffort: "none" } }).model("mistral-small-latest")
const firstRequest = LLM.request({
model,
system: "Call lookup_weather exactly once with Paris.",
prompt: "What is the weather?",
tools: [weather],
toolChoice: weather,
generation: { maxTokens: 160, temperature: 0 },
})
const first = yield* LLMClient.generate(firstRequest)
expect(first.finishReason.normalized).toBe("tool-calls")
expect(first.toolCalls).toMatchObject([{ name: "lookup_weather", input: { city: "Paris" } }])
expect(first.events.filter(LLMEvent.is.toolCall)).toHaveLength(1)
const call = first.toolCalls[0]
if (!call) throw new Error("Mistral did not return a tool call")
const followUp = LLMRequest.update(firstRequest, {
toolChoice: ToolChoice.make("none"),
messages: [
...firstRequest.messages,
first.message,
Message.tool({ id: call.id, name: call.name, result: { condition: "sunny", temperature: "18C" } }),
],
generation: { maxTokens: 160, temperature: 0 },
})
const second = yield* LLMClient.generate(followUp)
expect(second.finishReason.normalized).toBe("stop")
expect(second.toolCalls).toHaveLength(0)
expect(second.text.toLowerCase()).toContain("sunny")
}),
60_000,
)
})
describe("Mistral hosted GLM recorded", () => {
glmRecorded.effect.with(
"streams an indexed tool call",
{ tags: ["hosted-model", "tool", "tool-call"], metadata: { model: "zai-glm-5-2" } },
() =>
Effect.gen(function* () {
const response = yield* LLMClient.generate(
LLM.request({
model: configure({ apiKey }).model("zai-glm-5-2"),
system: "Call lookup_weather exactly once with Paris.",
prompt: "What is the weather?",
tools: [weather],
toolChoice: weather,
generation: { maxTokens: 256, temperature: 0 },
}),
)
expect(response.finishReason.normalized).toBe("tool-calls")
expect(response.toolCalls).toMatchObject([{ name: "lookup_weather", input: { city: "Paris" } }])
expect(response.events.filter(LLMEvent.is.toolInputStart)).toHaveLength(1)
expect(response.events.filter(LLMEvent.is.toolInputDelta).length).toBeGreaterThan(0)
expect(response.events.filter(LLMEvent.is.toolInputEnd)).toHaveLength(1)
expect(response.events.filter(LLMEvent.is.toolCall)).toHaveLength(1)
}),
60_000,
)
})
@@ -96,27 +96,6 @@ describe("Open Responses-compatible route", () => {
}),
)
it.effect("omits user messages with no content", () =>
Effect.gen(function* () {
const model = configure({
apiKey: "test-key",
baseURL: "https://responses.example.test/v1",
provider: "example",
}).model("example-model")
const prepared = yield* compileRequest(
LLM.request({
model,
messages: [Message.user("Before."), Message.user([]), Message.user("After.")],
}),
)
expect(prepared.body.input).toEqual([
{ role: "user", content: [{ type: "input_text", text: "Before." }] },
{ role: "user", content: [{ type: "input_text", text: "After." }] },
])
}),
)
it.effect("uses data URLs for embedded PDF messages and tool results", () =>
Effect.gen(function* () {
const model = configure({
-77
View File
@@ -1,77 +0,0 @@
import { describe, expect, test } from "bun:test"
import { Message, ToolCallPart, ToolResultPart } from "../src/schema/messages.js"
import { normalizeToolHistory } from "../src/tool-history.js"
const toolCall = (id: string, name = id) => ToolCallPart.make({ id, name, input: {} })
const toolResult = (id: string, value: unknown, name = id, resultType?: "text" | "content" | "error") =>
Message.tool(ToolResultPart.make({ id, name, result: value, resultType }))
describe("tool history normalization", () => {
test("fills missing local results before the next step", () => {
const normalized = normalizeToolHistory([
Message.assistant([toolCall("first"), toolCall("second")]),
toolResult("first", "done", "wrong", "text"),
Message.user("Continue."),
Message.assistant(toolCall("trailing")),
])
expect(normalized.map((message) => message.role)).toEqual([
"assistant",
"tool",
"tool",
"user",
"assistant",
])
expect(normalized[1]?.content[0]).toMatchObject({ type: "tool-result", id: "first", name: "first" })
expect(normalized[2]?.content).toEqual([
{ type: "tool-result", id: "second", name: "second", result: { type: "error", value: "Tool result missing" } },
])
expect(normalized[4]?.content).toEqual([toolCall("trailing")])
})
test("normalizes empty results without changing whitespace or media", () => {
const media = { type: "file" as const, uri: "data:image/png;base64,AQID", mime: "image/png" }
const normalized = normalizeToolHistory([
Message.assistant([
toolCall("text"),
toolCall("content"),
toolCall("error"),
toolCall("mixed"),
toolCall("whitespace"),
]),
toolResult("text", "", "text", "text"),
toolResult("content", [], "content", "content"),
toolResult("error", "", "error", "error"),
toolResult("mixed", [{ type: "text", text: "" }, media], "mixed", "content"),
toolResult("whitespace", " ", "whitespace", "text"),
])
expect(normalized.slice(1).map((message) => message.content[0])).toEqual([
{ type: "tool-result", id: "text", name: "text", result: { type: "text", value: "(no tool output)" } },
{ type: "tool-result", id: "content", name: "content", result: { type: "text", value: "(no tool output)" } },
{ type: "tool-result", id: "error", name: "error", result: { type: "error", value: "(no tool output)" } },
{ type: "tool-result", id: "mixed", name: "mixed", result: { type: "content", value: [media] } },
{ type: "tool-result", id: "whitespace", name: "whitespace", result: { type: "text", value: " " } },
])
})
test("leaves unmatched and provider-executed history unchanged", () => {
const hostedCall = ToolCallPart.make({
id: "hosted",
name: "web_search",
input: {},
providerExecuted: true,
})
const hostedResult = ToolResultPart.make({
id: "hosted",
name: "web_search",
result: "",
resultType: "text",
providerExecuted: true,
})
const hosted = Message.assistant([hostedCall, hostedResult])
const orphan = toolResult("orphan", "ignored", "orphan", "text")
expect(normalizeToolHistory([orphan, hosted])).toEqual([orphan, hosted])
})
})
+1 -22
View File
@@ -71,25 +71,4 @@ Environment options:
## Deployment
The `deploy` GitHub Actions workflow uses SST to deploy the web app from these branches in `anomalyco/opencode`:
| Branch | Site |
| ------------ | --------------------- |
| `dev` | `app.dev.opencode.ai` |
| `production` | `app.opencode.ai` |
| `beta` | `beta.opencode.ai` |
Changes merged into `v2` reach the beta site when they are promoted to `beta`. The beta SST stage deploys
only the web app, using the same `WebApp` StaticSite definition as production. It sets the build channel
and Sentry environment to `beta` without deploying the API, console, database, or billing infrastructure.
The hosted app defaults to `http://localhost:49374`, matching the managed V2 service. Saved server selections
override this default. Connecting still requires the service's credentials.
The workflow reuses the repository's `CLOUDFLARE_API_TOKEN` and web Sentry settings. The Cloudflare token
must cover SST's R2 state storage, KV assets, Workers, and custom-domain management in the account that
owns `opencode.ai`. The beta GitHub environment must allow deployments from the `beta` branch; it does not
need AWS credentials.
SST manages the beta site's custom domain. The first deployment creates its DNS record and TLS certificate.
Do not create a CNAME for `beta.opencode.ai` first, because it would conflict with the Workers custom domain.
You can deploy the `dist` folder to any static host provider (netlify, surge, now, etc.)
@@ -1,34 +1,5 @@
import { expect, story } from "../../storybook/playwright/story"
for (const draft of ["empty-draft", "multiline-draft", "mixed-attachments"]) {
story(`select all stays inside the composer with ${draft}`, async ({ mount, page }) => {
const component = await mount(`opencode-composer-flow--${draft}`)
const input = component.getByRole("textbox", { name: "Prompt", exact: true })
const text = await input.textContent()
for (let count = 0; count < 2; count++) {
await input.press("ControlOrMeta+a")
expect(
await input.evaluate((editor) => {
const selection = window.getSelection()
return {
text: selection?.toString(),
inside: editor.contains(selection?.anchorNode ?? null) && editor.contains(selection?.focusNode ?? null),
}
}),
).toEqual({ text, inside: true })
}
await page.keyboard.type("Replacement draft")
await expect(input).toHaveText("Replacement draft")
await expect(component.getByRole("status")).toHaveText("Ready")
if (draft === "mixed-attachments") {
await expect(component.getByAltText("layout.png")).toBeVisible()
await expect(component.getByText("Keep the normal flow flat", { exact: true })).toBeVisible()
}
})
}
story("renders a draft once and supports editing, caret restoration, and failure recovery", async ({ mount, page }) => {
await page.addInitScript(() => {
const replace = Element.prototype.replaceChildren
@@ -1,4 +1,4 @@
import { expect, test, type Locator } from "@playwright/test"
import { expect, test } from "@playwright/test"
import { mockOpenCodeServer } from "../utils/mock-server"
import { expectAppVisible } from "../utils/waits"
@@ -65,64 +65,11 @@ for (const lines of [6000, 25000]) {
await expect.poll(async () => (await input.innerText()) === text).toBe(true)
expect(await events.evaluate((events) => events.count)).toBe(1)
await expect(input).toBeFocused()
await expectCaretVisible(input)
const scroll = page.locator('[data-component="composer-scroll"]')
await expect(scroll.locator(".scroll-view__viewport")).toHaveCSS("scrollbar-width", "none")
await expect(scroll.locator(".scroll-view__thumb")).toBeVisible()
await page.keyboard.type("!")
await expect.poll(async () => (await input.innerText()) === text + "!").toBe(true)
await expectCaretVisible(input)
const thumb = await scroll.locator(".scroll-view__thumb").boundingBox()
const bounds = await scroll.boundingBox()
if (!thumb || !bounds) throw new Error("Missing composer scrollbar bounds")
await page.mouse.move(thumb.x + thumb.width / 2, thumb.y + thumb.height / 2)
await page.mouse.down()
await page.mouse.move(thumb.x + thumb.width / 2, bounds.y + 8 + thumb.height / 2)
await page.mouse.up()
await expect(scroll.locator(".scroll-view__viewport")).toHaveJSProperty("scrollTop", 0)
await expect(input).toBeFocused()
await page.keyboard.press("ControlOrMeta+Home")
await page.keyboard.press("ControlOrMeta+End")
await expectCaretVisible(input)
})
}
async function expectCaretVisible(input: Locator) {
await expect
.poll(() =>
input.evaluate((element) => {
const selection = window.getSelection()
if (!selection?.isCollapsed || !selection.rangeCount || !element.contains(selection.anchorNode)) return false
const caret = selection.getRangeAt(0).getBoundingClientRect()
const viewport = (element.closest("[data-scrollable]") ?? element).getBoundingClientRect()
return caret.height > 0 && caret.top >= viewport.top - 1 && caret.bottom <= viewport.bottom + 1
}),
)
.toBe(true)
}
for (const width of [390, 1280]) {
for (const direction of ["ltr", "rtl"]) {
test(`reveals a multiline paste in the middle at ${width}px in ${direction}`, async ({ page }) => {
await page.setViewportSize({ width, height: 800 })
await page.evaluate((direction) => (document.documentElement.dir = direction), direction)
const input = page.getByRole("textbox", { name: "Prompt", exact: true })
const suffix = "\nExisting trailing content".repeat(100)
await input.fill("Before " + suffix)
await input.press("ControlOrMeta+Home")
await input.press("ArrowRight")
const text = "Pasted line /tmp/example.ts 123 \u0645\u0631\u062d\u0628\u0627\n".repeat(100) + "End of paste"
await page.evaluate((text) => navigator.clipboard.writeText(text), text)
await page.keyboard.press("ControlOrMeta+V")
await expect.poll(() => input.innerText()).toBe("B" + text + "efore " + suffix)
await expectCaretVisible(input)
await page.keyboard.type("!")
await expect.poll(() => input.innerText()).toBe("B" + text + "!efore " + suffix)
await expectCaretVisible(input)
})
}
}
for (const text of [
"single line <b> &amp;",
"first\nsecond",
@@ -65,8 +65,8 @@ test("project Extensions stays inside settings while plugins load", async ({ pag
data: (project ? ["shared-plugin", "project-plugin"] : ["shared-plugin"]).map((id) => ({
id,
source: { type: "package", package: id },
state: { status: "active" },
features: { server: true },
status: "active",
tui: false,
})),
},
})
@@ -32,13 +32,12 @@ test("server dialog keeps focus above fullscreen settings", async ({ page }) =>
const editor = page.getByRole("dialog", { name: "Add server" })
await expect(editor.getByPlaceholder("http://localhost:4096")).toBeFocused()
await expect(editor.getByPlaceholder("username")).toHaveCount(0)
const name = editor.getByPlaceholder("Localhost", { exact: true })
const username = editor.getByPlaceholder("username")
const password = editor.getByPlaceholder("password")
await name.click()
await expect(name).toBeFocused()
await name.fill("Remote")
await expect(name).toHaveValue("Remote")
await username.click()
await expect(username).toBeFocused()
await username.fill("kit")
await expect(username).toHaveValue("kit")
await page.keyboard.press("Tab")
await expect(password).toBeFocused()
await password.fill("secret")
@@ -1,193 +0,0 @@
import { expect, test } from "@playwright/test"
import { base64Encode } from "@opencode-ai/util/encode"
import type { OpenCodeEvent } from "@opencode-ai/client/promise"
import { fixture } from "../smoke/session-timeline.fixture"
import { mockOpenCodeServer } from "../utils/mock-server"
test("keeps history visible and recovers the composer by choosing another directory", async ({ page }) => {
const directory = "/projects/deleted"
const destination = "/projects/restored"
const sessionID = "ses_missing_location"
const session = { id: sessionID, projectID: fixture.project.id, directory, title: "Missing location" }
const events: OpenCodeEvent[] = []
const moves: unknown[] = []
await mockOpenCodeServer(page, {
directory: destination,
project: { ...fixture.project, worktree: destination },
provider: fixture.provider,
sessions: [session],
events: () => events.splice(0),
fileList: () => [],
pageMessages: () => ({
items: [{ id: "msg_saved", type: "user", text: "Keep this session history", time: { created: 1 } }],
}),
})
await page.route("**/api/**", (route) => {
if (new URL(route.request().url()).searchParams.get("location[directory]") !== directory) return route.fallback()
return route.fulfill({ status: 500, body: "", headers: { "access-control-allow-origin": "*" } })
})
await page.goto(`/server/${base64Encode(fixture.serverKey)}/session/${sessionID}`)
await expect(page.getByText("Keep this session history", { exact: true })).toBeVisible()
await expect(page.getByRole("status")).toContainText("Session location unavailable")
await expect(page.getByRole("status")).toContainText(directory)
await expect(page.getByRole("textbox", { name: "Prompt", exact: true })).toHaveCount(0)
const choose = page.getByRole("button", { name: "Choose directory", exact: true })
await expect(choose).toBeEnabled()
await choose.click()
const dialog = page.getByRole("dialog", { name: "Choose directory", exact: true })
await expect(dialog.getByRole("combobox")).toBeFocused()
await dialog.getByRole("combobox").press("Escape")
await expect(dialog).toHaveCount(0)
await expect(choose).toBeFocused()
expect(moves).toEqual([])
// The server can still reject a destination after it was selected in the picker.
await page.route(`**/api/session/${sessionID}/move`, (route) => {
if (route.request().method() !== "POST") return route.fallback()
moves.push(route.request().postDataJSON())
return route.fulfill({
status: 400,
json: { _tag: "InvalidRequestError", message: "Destination is unavailable" },
headers: { "access-control-allow-origin": "*" },
})
})
await choose.press("Enter")
await expect(dialog.getByRole("combobox")).toBeFocused()
await dialog.getByRole("combobox").fill(destination)
await dialog.getByRole("combobox").press("Enter")
await expect(dialog.locator(".directory-picker-selection")).toHaveText(destination)
await dialog.getByRole("button", { name: "Select folder", exact: true }).click()
await expect(page.getByText("Failed to move session", { exact: true })).toBeVisible()
await expect(choose).toBeEnabled()
await expect(page.getByRole("textbox", { name: "Prompt", exact: true })).toHaveCount(0)
await expect(choose).toBeFocused()
expect(moves).toEqual([{ directory: destination }])
await page.route(`**/api/session/${sessionID}/move`, (route) => {
if (route.request().method() !== "POST") return route.fallback()
moves.push(route.request().postDataJSON())
session.directory = destination
events.push({
id: "evt_location_recovered",
type: "session.moved",
created: 2,
durable: { aggregateID: sessionID, seq: 1, version: 1 },
data: { sessionID, location: { directory: destination }, projectID: fixture.project.id },
})
return route.fulfill({ status: 204, headers: { "access-control-allow-origin": "*" } })
})
await choose.click()
await dialog.getByRole("combobox").fill(destination)
await dialog.getByRole("combobox").press("Enter")
await expect(dialog.locator(".directory-picker-selection")).toHaveText(destination)
await dialog.getByRole("button", { name: "Select folder", exact: true }).click()
await expect(page.getByText("Session location unavailable", { exact: true })).toHaveCount(0)
await expect(page.getByRole("textbox", { name: "Prompt", exact: true })).toBeEditable()
await expect(page.locator('[data-action="composer-model"]')).toBeVisible()
await expect(page.getByText("Keep this session history", { exact: true })).toBeVisible()
await expect(page).toHaveURL(new RegExp(`/session/${sessionID}$`))
expect(moves).toEqual([{ directory: destination }, { directory: destination }])
})
for (const create of [false, true]) {
test(`recovers into ${create ? "a new" : "an existing"} worktree without resolving the deleted location`, async ({
page,
}) => {
const canonical = "/projects/repository"
const directory = "/projects/deleted-worktree"
const destination = create ? "/projects/new-worktree" : "/projects/existing-worktree"
const sessionID = "ses_worktree_recovery"
const events: OpenCodeEvent[] = []
const requests: { operation: string; body: unknown }[] = []
let listing = Promise.withResolvers<void>()
let listingRequested = Promise.withResolvers<void>()
const moving = Promise.withResolvers<void>()
const moveRequested = Promise.withResolvers<void>()
await mockOpenCodeServer(page, {
directory: canonical,
project: { ...fixture.project, worktree: canonical },
provider: fixture.provider,
sessions: [{ id: sessionID, projectID: fixture.project.id, directory }],
pageMessages: () => ({
items: [{ id: "msg_worktree", type: "user", text: "Recover my worktree", time: { created: 1 } }],
}),
events: () => events.splice(0),
})
await page.route("**/api/**", async (route) => {
const url = new URL(route.request().url())
const headers = { "access-control-allow-origin": "*" }
if (url.searchParams.get("location[directory]") === directory)
return route.fulfill({ status: 500, body: "", headers })
if (url.pathname === `/api/worktree/${fixture.project.id}`) {
if (route.request().method() === "GET") {
listingRequested.resolve()
await listing.promise
return route.fulfill({
json: [
{ directory: canonical, strategy: null },
{ directory: "/projects/existing-worktree", strategy: "git" },
...Array.from({ length: 20 }, (_, index) => ({
directory: `/projects/worktree-${index}`,
strategy: "git",
})),
],
headers,
})
}
if (route.request().method() === "POST") {
requests.push({ operation: "create", body: route.request().postDataJSON() })
return route.fulfill({
json: { directory: destination, name: "new-worktree", branch: "new-worktree" },
headers,
})
}
}
if (url.pathname === `/api/session/${sessionID}/move` && route.request().method() === "POST") {
requests.push({ operation: "move", body: route.request().postDataJSON() })
moveRequested.resolve()
await moving.promise
events.push({
id: "evt_worktree_recovered",
type: "session.moved",
created: 2,
durable: { aggregateID: sessionID, seq: 1, version: 1 },
data: { sessionID, location: { directory: destination }, projectID: fixture.project.id },
})
return route.fulfill({ status: 204, headers })
}
return route.fallback()
})
await page.goto(`/server/${base64Encode(fixture.serverKey)}/session/${sessionID}`)
await page.getByRole("button", { name: "Choose worktree", exact: true }).click()
await listingRequested.promise
await expect(page.getByRole("menuitem", { name: "Loading", exact: true })).toBeVisible()
await expect(page.getByRole("menuitem", { name: "New workspace", exact: true })).toBeVisible()
await expect(page.getByText("Recover my worktree", { exact: true })).toBeVisible()
await expect(page.getByText("Session location unavailable", { exact: true })).toBeVisible()
listing.resolve()
const existing = page.getByRole("menuitem", { name: "existing-worktree", exact: true })
await expect(existing).toBeVisible()
await expect(existing).toHaveCSS("height", "28px")
await page.keyboard.press("Escape")
await expect(page.getByRole("menu")).toHaveCount(0)
listing = Promise.withResolvers<void>()
listingRequested = Promise.withResolvers<void>()
await page.getByRole("button", { name: "Choose worktree", exact: true }).click()
await listingRequested.promise
await expect(page.getByRole("menuitem", { name: "existing-worktree", exact: true })).toBeVisible()
await expect(page.getByText("Recover my worktree", { exact: true })).toBeVisible()
listing.resolve()
await page.getByRole("menuitem", { name: create ? "New workspace" : "existing-worktree", exact: true }).click()
await moveRequested.promise
await expect(page.getByRole("button", { name: "Moving session…", exact: true })).toBeDisabled()
await expect(page.getByRole("button", { name: "Choose worktree", exact: true })).toBeDisabled()
moving.resolve()
await expect(page.getByText("Session location unavailable", { exact: true })).toHaveCount(0)
await expect(page.getByRole("textbox", { name: "Prompt", exact: true })).toBeEditable()
await expect(page.getByText("Recover my worktree", { exact: true })).toBeVisible()
expect(requests).toEqual([
...(create ? [{ operation: "create", body: { strategy: "git", from: canonical, directory: "/projects/" } }] : []),
{ operation: "move", body: { directory: destination } },
])
})
}
@@ -1,130 +0,0 @@
import { expect, test } from "@playwright/test"
import { fixture, pageMessages } from "../smoke/session-timeline.fixture"
import { mockOpenCodeServer } from "../utils/mock-server"
test.beforeEach(async ({ page }) => {
const sessions = fixture.sessions.map((session) => ({ ...session }))
await mockOpenCodeServer(page, {
sessions,
provider: fixture.provider,
directory: fixture.directory,
project: fixture.project,
pageMessages,
})
await page.route("**/api/session/*/rename", async (route) => {
if (route.request().method() !== "POST") return route.fallback()
const id = new URL(route.request().url()).pathname.split("/").at(-2)
const session = sessions.find((item) => item.id === id)
const payload: unknown = route.request().postDataJSON()
if (
!session ||
!payload ||
typeof payload !== "object" ||
!("title" in payload) ||
typeof payload.title !== "string"
)
throw new Error("Invalid rename request")
session.title = payload.title
await route.fulfill({ status: 204, headers: { "access-control-allow-origin": "*" } })
})
await page.goto("/")
await page.locator('[data-component="home-session-row"]').filter({ hasText: fixture.expected.targetTitle }).click()
await expect(page.getByRole("heading", { name: fixture.expected.targetTitle, exact: true })).toBeVisible()
})
for (const commit of ["Enter", "blur", "click outside"]) {
test(`saves the session heading on ${commit}`, async ({ page }) => {
await page.getByRole("heading", { name: fixture.expected.targetTitle, exact: true }).click()
const input = page.locator('input[data-slot="session-title-child"]')
await expect(input).toBeFocused()
await input.fill("Renamed session")
if (commit === "Enter") await input.press("Enter")
if (commit === "blur") await input.press("Tab")
if (commit === "click outside") await page.locator('[data-component="composer-editor"]').click()
await expect(page.getByRole("heading", { name: "Renamed session", exact: true })).toBeVisible()
await expect(page.locator('[data-slot="titlebar-tabs"] a').filter({ hasText: "Renamed session" })).toBeVisible()
await page.reload()
await expect(page.getByRole("heading", { name: "Renamed session", exact: true })).toBeVisible()
})
}
test("cancels the session heading with Escape", async ({ page }) => {
await page.getByRole("heading", { name: fixture.expected.targetTitle, exact: true }).click()
const input = page.locator('input[data-slot="session-title-child"]')
await input.fill("Discard this title")
await input.press("Escape")
await expect(page.getByRole("heading", { name: fixture.expected.targetTitle, exact: true })).toBeVisible()
await page.reload()
await expect(page.getByRole("heading", { name: fixture.expected.targetTitle, exact: true })).toBeVisible()
})
test("keeps the draft when saving the session heading fails", async ({ page }) => {
await page.route("**/api/session/*/rename", (route) =>
route.fulfill({ status: 500, headers: { "access-control-allow-origin": "*" } }),
)
await page.getByRole("heading", { name: fixture.expected.targetTitle, exact: true }).click()
const input = page.locator('input[data-slot="session-title-child"]')
await input.fill("Retry this title")
await input.press("Tab")
await expect(page.getByText("Request failed", { exact: true })).toBeVisible()
await expect(input).toBeEnabled()
await expect(input).toHaveValue("Retry this title")
await expect(
page.locator('[data-slot="titlebar-tabs"] a').filter({ hasText: fixture.expected.targetTitle }),
).toBeVisible()
})
test("does not save an empty session heading", async ({ page }) => {
await page.getByRole("heading", { name: fixture.expected.targetTitle, exact: true }).click()
const input = page.locator('input[data-slot="session-title-child"]')
await input.fill(" ")
await input.press("Tab")
await expect(page.getByRole("heading", { name: fixture.expected.targetTitle, exact: true })).toBeVisible()
await page.reload()
await expect(page.getByRole("heading", { name: fixture.expected.targetTitle, exact: true })).toBeVisible()
})
test("renames and closes the session tab from its context menu", async ({ page }) => {
const tab = page.locator('[data-slot="titlebar-tabs"] a').filter({ hasText: fixture.expected.targetTitle })
await tab.click({ button: "right" })
await expect(page.getByRole("menuitem", { name: "Rename", exact: true })).toBeVisible()
await page.keyboard.press("Escape")
await expect(page.getByRole("menuitem", { name: "Rename", exact: true })).toBeHidden()
await expect(tab).toBeFocused()
await tab.press("Shift+F10")
await page.getByRole("menuitem", { name: "Rename", exact: true }).click()
const input = page.locator('[data-slot="tab-title"][contenteditable="true"]')
await expect(input).toBeFocused()
await input.fill("Renamed from tab")
await input.press("Enter")
await expect(page.getByRole("heading", { name: "Renamed from tab", exact: true })).toBeVisible()
await page.reload()
await expect(page.getByRole("heading", { name: "Renamed from tab", exact: true })).toBeVisible()
const renamed = page.locator('[data-slot="titlebar-tabs"] a').filter({ hasText: "Renamed from tab" })
await renamed.click({ button: "right" })
await page.getByRole("menuitem", { name: "Close tab", exact: true }).click()
await expect(renamed).toBeHidden()
await page.getByRole("button", { name: "Home", exact: true }).click()
await expect(
page.locator('[data-component="home-session-row"]').filter({ hasText: "Renamed from tab" }),
).toBeVisible()
})
test("renames an inactive tab without switching sessions", async ({ page }) => {
await page.getByRole("button", { name: "Home", exact: true }).click()
await page.locator('[data-component="home-session-row"]').filter({ hasText: fixture.expected.sourceTitle }).click()
await expect(page.getByRole("heading", { name: fixture.expected.sourceTitle, exact: true })).toBeVisible()
const tab = page.locator('[data-slot="titlebar-tabs"] a').filter({ hasText: fixture.expected.targetTitle })
await tab.click({ button: "right" })
await page.getByRole("menuitem", { name: "Rename", exact: true }).click()
const input = page.locator('[data-slot="tab-title"][contenteditable="true"]')
await expect(input).toBeFocused()
await input.fill("Inactive tab renamed")
await input.press("Tab")
await expect(page.getByRole("heading", { name: fixture.expected.sourceTitle, exact: true })).toBeVisible()
await expect(page).toHaveURL(new RegExp(`/session/${fixture.sourceID}$`))
await page.locator('[data-slot="titlebar-tabs"] a').filter({ hasText: "Inactive tab renamed" }).click()
await expect(page.getByRole("heading", { name: "Inactive tab renamed", exact: true })).toBeVisible()
await page.reload()
await expect(page.getByRole("heading", { name: "Inactive tab renamed", exact: true })).toBeVisible()
})
@@ -83,12 +83,7 @@ test("extensions opens without waiting for MCPs or plugins", async ({ page }) =>
json: {
location: { directory },
data: [
{
id: "demo-plugin",
source: { type: "package", package: "demo-plugin" },
state: { status: "active" },
features: { server: true },
},
{ id: "demo-plugin", source: { type: "package", package: "demo-plugin" }, status: "active", tui: false },
],
},
})
@@ -141,7 +141,7 @@ test("shows the not found fallback when the viewed session is deleted", async ({
})
await expect(page.getByText("This session cannot be found")).toBeVisible()
await expect(page.getByRole("button", { name: "Close Tab", exact: true })).toBeVisible()
await expect(page.getByRole("button", { name: "Close Tab" })).toBeVisible()
await expect(page.getByRole("heading", { name: taskDescription })).toHaveCount(0)
})
+5 -29
View File
@@ -9,7 +9,6 @@ import { Button } from "@opencode-ai/ui/button"
import { Keybind } from "@opencode-ai/ui/keybind"
import { Menu } from "@opencode-ai/ui/menu"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { ScrollView } from "@opencode-ai/ui/scroll-view"
import { AttachmentCard } from "@opencode-ai/session-ui/attachment-card"
import { CommentCard } from "@opencode-ai/session-ui/comment-card"
import { typeLabel } from "@opencode-ai/session-ui/message-file"
@@ -54,7 +53,6 @@ export function ComposerEditor(props: ComposerEditorProps) {
const state = props.controller.state
const view = props.controller.view
let editor: HTMLDivElement | undefined
let viewport: HTMLDivElement | undefined
let localInput = false
const updateCursor = () => {
if (!editor || !window.getSelection()?.isCollapsed) return
@@ -114,10 +112,8 @@ export function ComposerEditor(props: ComposerEditorProps) {
<form
data-component="composer"
data-dock-border-underlay={props.borderUnderlay ? "true" : undefined}
class="group/composer relative min-h-[96px] w-full overflow-clip rounded-xl"
class="group/composer relative min-h-[96px] w-full overflow-clip rounded-xl bg-v2-background-bg-base"
classList={{
"bg-v2-background-bg-layer-01": props.borderUnderlay,
"bg-v2-background-bg-base": !props.borderUnderlay,
"shadow-[var(--v2-elevation-raised)]": !props.borderUnderlay,
"border border-v2-icon-icon-info border-dashed": state.drag === "active",
}}
@@ -149,14 +145,7 @@ export function ComposerEditor(props: ComposerEditorProps) {
/>
</Show>
<ScrollView
data-component="composer-scroll"
class="min-h-[60px] max-h-[180px]"
viewportRef={(element) => {
viewport = element
element.tabIndex = -1
}}
>
<div class="relative min-h-[60px]">
<div
ref={(element) => {
editor = element
@@ -173,7 +162,7 @@ export function ComposerEditor(props: ComposerEditorProps) {
spellcheck={state.mode === "normal"}
// @ts-expect-error
autocomplete="off"
class="relative z-10 block min-h-[60px] w-full whitespace-pre-wrap bg-transparent px-4 pt-4 pb-2 text-[13px] font-[440] leading-5 text-v2-text-text-base focus:outline-none [&_[data-mention=file]]:text-syntax-property [&_[data-mention=agent]]:text-syntax-type [&_[data-mention=reference]]:text-syntax-keyword"
class="relative z-10 block min-h-[60px] max-h-[180px] w-full overflow-y-auto whitespace-pre-wrap bg-transparent px-4 pt-4 pb-2 text-[13px] font-[440] leading-5 text-v2-text-text-base focus:outline-none [&_[data-mention=file]]:text-syntax-property [&_[data-mention=agent]]:text-syntax-type [&_[data-mention=reference]]:text-syntax-keyword"
classList={{ "font-mono!": state.mode === "shell", "opacity-50": props.disabled }}
style={{
"unicode-bidi": state.mode === "normal" ? "plaintext" : undefined,
@@ -201,20 +190,7 @@ export function ComposerEditor(props: ComposerEditorProps) {
}}
onKeyUp={updateCursor}
onPointerUp={updateCursor}
onPaste={(event) => {
props.controller.onPaste(event)
// Programmatic multiline insertion does not reliably reveal the caret.
requestAnimationFrame(() => {
const selection = window.getSelection()
if (!editor || !viewport || !selection?.isCollapsed || !selection.rangeCount) return
if (!editor.contains(selection.anchorNode)) return
const caret = selection.getRangeAt(0).getBoundingClientRect()
if (!caret.height) return
const bounds = viewport.getBoundingClientRect()
if (caret.bottom > bounds.bottom - 8) viewport.scrollTop += caret.bottom - bounds.bottom + 8
if (caret.top < bounds.top + 8) viewport.scrollTop += caret.top - bounds.top - 8
})
}}
onPaste={props.controller.onPaste}
onFocus={() => props.controller.dispatch({ type: "focus.editor" })}
/>
<Show when={!props.controller.value()}>
@@ -230,7 +206,7 @@ export function ComposerEditor(props: ComposerEditorProps) {
: i18n.t("ui.promptInput.placeholder.normal", { slash: "/", at: "@" }))}
</div>
</Show>
</ScrollView>
</div>
<div class="flex h-11 items-center px-2">
<div
@@ -5,20 +5,10 @@ import { pluginLabels } from "./plugin"
describe("pluginLabels", () => {
test("omits built-in plugins", () => {
const plugins: PluginInfo[] = [
{ id: "opencode.internal", source: { type: "builtin" }, state: { status: "active" }, features: { server: true } },
{
id: "package-plugin",
source: { type: "package", package: "example" },
state: { status: "active" },
features: { server: true },
},
{
id: "local-plugin",
source: { type: "local", path: "/tmp/plugin.ts" },
state: { status: "active" },
features: { server: true },
},
{ id: "sdk-plugin", source: { type: "sdk" }, state: { status: "active" }, features: { server: true } },
{ id: "opencode.internal", source: { type: "builtin" }, status: "active", tui: false },
{ id: "package-plugin", source: { type: "package", package: "example" }, status: "active", tui: false },
{ id: "local-plugin", source: { type: "local", path: "/tmp/plugin.ts" }, status: "active", tui: false },
{ id: "sdk-plugin", source: { type: "sdk" }, status: "active", tui: false },
]
expect(pluginLabels(plugins)).toEqual(["package-plugin", "local-plugin", "sdk-plugin"])
+3
View File
@@ -414,6 +414,8 @@ export const dict = {
"dialog.server.add.button": "አገልጋይ አክል",
"dialog.server.add.name": "የአገልጋይ ስም (አማራጭ)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "የተጠቃሚ ስም (አማራጭ)",
"dialog.server.add.usernamePlaceholder": "የተጠቃሚ ስም",
"dialog.server.add.password": "የይለፍ ቃል (አማራጭ)",
"dialog.server.add.passwordPlaceholder": "የይለፍ ቃል",
"dialog.server.edit.title": "አገልጋይ አርትዕ",
@@ -496,6 +498,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "ዴስክቶፕ {{version}}",
"wsl.onboarding.versionMismatch": "የተጫነው ሥሪት ከዴስክቶፕ መተግበሪያ ሥሪት ጋር አይዛመድም።",
"wsl.onboarding.adding": "በማከል ላይ...",
"server.row.noUsername": "የተጠቃሚ ስም የለም",
"dialog.project.edit.title": "ፕሮጀክት አርትዕ",
"dialog.project.edit.name": "ስም",
"dialog.project.edit.icon": "አዶ",
+3
View File
@@ -420,6 +420,7 @@ export const dict = {
"dialog.server.add.button": "إضافة خادم",
"dialog.server.add.name": "اسم الخادم (اختياري)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "اسم المستخدم (اختياري)",
"dialog.server.add.password": "كلمة المرور (اختياري)",
"dialog.server.edit.title": "تحرير الخادم",
"dialog.server.default.title": "الخادم الافتراضي",
@@ -1023,7 +1024,9 @@ export const dict = {
"app.server.unreachable": "تعذر الوصول إلى {{server}}",
"app.server.retrying": "جارٍ إعادة المحاولة تلقائيًا...",
"app.server.otherServers": "خوادم أخرى",
"dialog.server.add.usernamePlaceholder": "اسم المستخدم",
"dialog.server.add.passwordPlaceholder": "كلمة المرور",
"server.row.noUsername": "لا يوجد اسم مستخدم",
"session.review.noVcs.createGit.title": "إنشاء مستودع Git",
"session.review.noVcs.createGit.description": "تتبع ومراجعة والتراجع عن التغييرات في هذا المشروع",
"session.review.noVcs.createGit.actionLoading": "جارٍ إنشاء مستودع Git...",
+3
View File
@@ -421,6 +421,8 @@ export const dict = {
"dialog.server.add.button": "Server əlavə et",
"dialog.server.add.name": "Server adı (istəyə bağlı)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "İstifadəçi adı (istəyə bağlı)",
"dialog.server.add.usernamePlaceholder": "istifadəçi adı",
"dialog.server.add.password": "Şifrə (istəyə bağlı)",
"dialog.server.add.passwordPlaceholder": "parol",
"dialog.server.edit.title": "Serveri redaktə et",
@@ -507,6 +509,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "masaüstü {{version}}",
"wsl.onboarding.versionMismatch": "Quraşdırılmış versiya masaüstü proqram versiyasına uyğun gəlmir.",
"wsl.onboarding.adding": "Əlavə edilir...",
"server.row.noUsername": "istifadəçi adı yoxdur",
"dialog.project.edit.title": "Layihəni redaktə et",
"dialog.project.edit.name": "Ad",
"dialog.project.edit.icon": "İkon",
+3
View File
@@ -421,6 +421,8 @@ export const dict = {
"dialog.server.add.button": "Добавете сървър",
"dialog.server.add.name": "Име на сървъра (по избор)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "потребителско име (по избор)",
"dialog.server.add.usernamePlaceholder": "потребителско име",
"dialog.server.add.password": "Парола (по избор)",
"dialog.server.add.passwordPlaceholder": "парола",
"dialog.server.edit.title": "Редактиране на сървъра",
@@ -506,6 +508,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "работен плот {{version}}",
"wsl.onboarding.versionMismatch": "Инсталираната версия не съответства на версията на настолното приложение.",
"wsl.onboarding.adding": "Добавяне...",
"server.row.noUsername": "няма потребителско име",
"dialog.project.edit.title": "Редактиране на проекта",
"dialog.project.edit.name": "Име",
"dialog.project.edit.icon": "Икона",
+3
View File
@@ -418,6 +418,8 @@ export const dict: Record<string, string> = {
"dialog.server.add.button": "সার্ভার যোগ করুন",
"dialog.server.add.name": "সার্ভারের নাম (ঐচ্ছিক)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "ব্যবহারকারীর নাম (ঐচ্ছিক)",
"dialog.server.add.usernamePlaceholder": "ব্যবহারকারীর নাম",
"dialog.server.add.password": "পাসওয়ার্ড (ঐচ্ছিক)",
"dialog.server.add.passwordPlaceholder": "পাসওয়ার্ড",
"dialog.server.edit.title": "সার্ভার সম্পাদনা করুন",
@@ -503,6 +505,7 @@ export const dict: Record<string, string> = {
"wsl.onboarding.desktopVersion": "ডেস্কটপ {{version}}",
"wsl.onboarding.versionMismatch": "ইনস্টল করা সংস্করণ ডেস্কটপ অ্যাপ সংস্করণের সাথে মেলে না।",
"wsl.onboarding.adding": "যোগ করা হচ্ছে...",
"server.row.noUsername": "ব্যবহারকারীর নাম নেই",
"dialog.project.edit.title": "প্রকল্প সম্পাদনা করুন",
"dialog.project.edit.name": "নাম",
"dialog.project.edit.icon": "আইকন",
+3
View File
@@ -422,6 +422,7 @@ export const dict = {
"dialog.server.add.button": "Adicionar servidor",
"dialog.server.add.name": "Nome do servidor (opcional)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "Nome de usuário (opcional)",
"dialog.server.add.password": "Senha (opcional)",
"dialog.server.edit.title": "Editar servidor",
"dialog.server.default.title": "Servidor padrão",
@@ -1034,7 +1035,9 @@ export const dict = {
"app.server.unreachable": "Não foi possível conectar a {{server}}",
"app.server.retrying": "Tentando novamente automaticamente...",
"app.server.otherServers": "Outros servidores",
"dialog.server.add.usernamePlaceholder": "nome de usuário",
"dialog.server.add.passwordPlaceholder": "senha",
"server.row.noUsername": "sem nome de usuário",
"session.review.noVcs.createGit.title": "Criar um repositório Git",
"session.review.noVcs.createGit.description": "Rastreie, revise e desfaça alterações neste projeto",
"session.review.noVcs.createGit.actionLoading": "Criando repositório Git...",
+3
View File
@@ -449,6 +449,7 @@ export const dict = {
"dialog.server.add.button": "Dodaj server",
"dialog.server.add.name": "Ime servera (opcionalno)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "Korisničko ime (opcionalno)",
"dialog.server.add.password": "Lozinka (opcionalno)",
"dialog.server.edit.title": "Uredi server",
"dialog.server.default.title": "Podrazumijevani server",
@@ -1112,7 +1113,9 @@ export const dict = {
"app.server.unreachable": "Nije moguće pristupiti {{server}}",
"app.server.retrying": "Automatski ponovni pokušaj...",
"app.server.otherServers": "Drugi serveri",
"dialog.server.add.usernamePlaceholder": "korisničko ime",
"dialog.server.add.passwordPlaceholder": "lozinka",
"server.row.noUsername": "nema korisničkog imena",
"session.review.noVcs.createGit.title": "Kreiraj Git repozitorij",
"session.review.noVcs.createGit.description": "Prati, pregledaj i poništi promjene u ovom projektu",
"session.review.noVcs.createGit.actionLoading": "Kreiranje Git repozitorija...",
+3
View File
@@ -420,6 +420,8 @@ export const dict = {
"dialog.server.add.button": "Afegeix servidor",
"dialog.server.add.name": "Nom del servidor (opcional)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "Nom d'usuari (opcional)",
"dialog.server.add.usernamePlaceholder": "nom d'usuari",
"dialog.server.add.password": "Contrasenya (opcional)",
"dialog.server.add.passwordPlaceholder": "contrasenya",
"dialog.server.edit.title": "Edita el servidor",
@@ -505,6 +507,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "escriptori {{version}}",
"wsl.onboarding.versionMismatch": "La versió instal·lada no coincideix amb la versió de l'aplicació d'escriptori.",
"wsl.onboarding.adding": "S'està afegint...",
"server.row.noUsername": "sense nom d'usuari",
"dialog.project.edit.title": "Edita el projecte",
"dialog.project.edit.name": "Nom",
"dialog.project.edit.icon": "Icona",
+3
View File
@@ -418,6 +418,8 @@ export const dict = {
"dialog.server.add.button": "Přidat server",
"dialog.server.add.name": "Název serveru (volitelné)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "uživatelské jméno (volitelné)",
"dialog.server.add.usernamePlaceholder": "uživatelské jméno",
"dialog.server.add.password": "Heslo (volitelné)",
"dialog.server.add.passwordPlaceholder": "heslo",
"dialog.server.edit.title": "Upravit server",
@@ -503,6 +505,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "desktop {{version}}",
"wsl.onboarding.versionMismatch": "Nainstalovaná verze neodpovídá verzi aplikace pro stolní počítače.",
"wsl.onboarding.adding": "Přidávání...",
"server.row.noUsername": "žádné uživatelské jméno",
"dialog.project.edit.title": "Upravit projekt",
"dialog.project.edit.name": "Jméno",
"dialog.project.edit.icon": "ikona",
+3
View File
@@ -346,6 +346,7 @@ export const dict = {
"dialog.server.add.button": "Tilføj server",
"dialog.server.add.name": "Servernavn (valgfrit)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "Brugernavn (valgfrit)",
"dialog.server.add.password": "Adgangskode (valgfrit)",
"dialog.server.edit.title": "Rediger server",
"dialog.server.default.title": "Standardserver",
@@ -1002,7 +1003,9 @@ export const dict = {
"app.server.unreachable": "Kunne ikke nå {{server}}",
"app.server.retrying": "Prøver igen automatisk...",
"app.server.otherServers": "Andre servere",
"dialog.server.add.usernamePlaceholder": "brugernavn",
"dialog.server.add.passwordPlaceholder": "adgangskode",
"server.row.noUsername": "intet brugernavn",
"session.review.noVcs.createGit.title": "Opret et Git-repository",
"session.review.noVcs.createGit.description": "Spor, gennemgå og fortryd ændringer i dette projekt",
"session.review.noVcs.createGit.actionLoading": "Opretter Git-repository...",
+3
View File
@@ -327,6 +327,7 @@ export const dict = {
"dialog.server.add.button": "Server hinzufügen",
"dialog.server.add.name": "Servername (optional)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "Benutzername (optional)",
"dialog.server.add.password": "Passwort (optional)",
"dialog.server.edit.title": "Server bearbeiten",
"dialog.server.default.title": "Standardserver",
@@ -944,7 +945,9 @@ export const dict = {
"app.server.unreachable": "Konnte {{server}} nicht erreichen",
"app.server.retrying": "Verbindung wird automatisch erneut hergestellt…",
"app.server.otherServers": "Andere Server",
"dialog.server.add.usernamePlaceholder": "Benutzername",
"dialog.server.add.passwordPlaceholder": "Passwort",
"server.row.noUsername": "Kein Benutzername",
"session.review.noVcs.createGit.title": "Git-Repository erstellen",
"session.review.noVcs.createGit.description":
"Änderungen in diesem Projekt verfolgen, überprüfen und rückgängig machen",
+3
View File
@@ -424,6 +424,8 @@ export const dict = {
"dialog.server.add.button": "ސަރވަރ އިތުރުކުރުން",
"dialog.server.add.name": "ސަރވަރ ނަން (އިޚްތިޔާރީ)",
"dialog.server.add.namePlaceholder": "Localhost އެވެ",
"dialog.server.add.username": "ޔޫޒަރނޭމް (އިޚްތިޔާރީ)",
"dialog.server.add.usernamePlaceholder": "ޔޫޒަރނޭމް",
"dialog.server.add.password": "ޕާސްވޯޑް (އިޚްތިޔާރީ)",
"dialog.server.add.passwordPlaceholder": "ޕާސްވަރޑް",
"dialog.server.edit.title": "އެޑިޓް ސަރވަރ",
@@ -509,6 +511,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "ޑެސްކްޓޮޕް {{version}} އެވެ",
"wsl.onboarding.versionMismatch": "އިންސްޓޯލް ކޮށްފައިވާ ވަރޝަން ޑެސްކްޓޮޕް އެޕް ވަރޝަން އާއި އެއްގޮތެއް ނުވެއެވެ.",
"wsl.onboarding.adding": "އިތުރުކުރަނީ...",
"server.row.noUsername": "ޔޫޒަރނޭމް އެއް ނެތެވެ",
"dialog.project.edit.title": "އެޑިޓް ޕްރޮޖެކްޓް",
"dialog.project.edit.name": "ނަން",
"dialog.project.edit.icon": "އައިކޮން",
+3
View File
@@ -423,6 +423,8 @@ export const dict: Record<string, string> = {
"dialog.server.add.button": "སར་བར་ཁ་སྐོང་འབད།",
"dialog.server.add.name": "སར་བར་གྱི་མིང་ (གདམ་ཁ་ཅན།)",
"dialog.server.add.namePlaceholder": "Localhost།",
"dialog.server.add.username": "ལག་ལེན་པའི་མིང་ (གདམ་ཁ་ཅན་)།",
"dialog.server.add.usernamePlaceholder": "ལག་ལེན་པའི་མིང་།",
"dialog.server.add.password": "ཆོག་ཡིག་ (གདམ་ཁ་ཅན།)",
"dialog.server.add.passwordPlaceholder": "གསང༌ཨང",
"dialog.server.edit.title": "སར་བར་ཞུན་དག་འབད།",
@@ -508,6 +510,7 @@ export const dict: Record<string, string> = {
"wsl.onboarding.desktopVersion": "ཌེཀསི་ཊོཔ་ {{version}}",
"wsl.onboarding.versionMismatch": "གཞི་བཙུགས་འབད་ཡོད་པའི་ཐོན་རིམ་འདི་ ཌེཀསི་ཊོཔ་ཨེཔ་ཐོན་རིམ་དང་མཐུན་སྒྲིག་མི་འབད།",
"wsl.onboarding.adding": "ཁ་སྐོང་བརྐྱབ་དོ།",
"server.row.noUsername": "སྤྱོད་མིང་མེད།",
"dialog.project.edit.title": "ཞུན་དག་ལས་གཞི།",
"dialog.project.edit.name": "མིང",
"dialog.project.edit.icon": "ངོས་དཔར།",
+3
View File
@@ -420,6 +420,8 @@ export const dict = {
"dialog.server.add.button": "Προσθήκη διακομιστή",
"dialog.server.add.name": "Όνομα διακομιστή (προαιρετικό)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "Όνομα χρήστη (προαιρετικό)",
"dialog.server.add.usernamePlaceholder": "όνομα χρήστη",
"dialog.server.add.password": "Κωδικός πρόσβασης (προαιρετικό)",
"dialog.server.add.passwordPlaceholder": "κωδικός πρόσβασης",
"dialog.server.edit.title": "Επεξεργασία διακομιστή",
@@ -505,6 +507,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "επιτραπέζιος υπολογιστής {{version}}",
"wsl.onboarding.versionMismatch": "Η εγκατεστημένη έκδοση δεν ταιριάζει με την έκδοση της εφαρμογής για υπολογιστές.",
"wsl.onboarding.adding": "Προσθήκη...",
"server.row.noUsername": "χωρίς όνομα χρήστη",
"dialog.project.edit.title": "Επεξεργασία έργου",
"dialog.project.edit.name": "Όνομα",
"dialog.project.edit.icon": "Εικονίδιο",
+4 -7
View File
@@ -2,12 +2,6 @@ import { DESKTOP_NATIVE_ENGLISH } from "./desktop-native"
export const dict = {
...DESKTOP_NATIVE_ENGLISH,
"session.location.unavailable": "Session location unavailable",
"session.location.description": "Choose another directory to continue this session.",
"session.location.choose": "Choose directory",
"session.location.worktree": "Choose worktree",
"session.location.worktreesFailed": "Failed to load worktrees",
"session.location.moving": "Moving session…",
"command.category.suggested": "Suggested",
"command.category.view": "View",
"command.category.project": "Project",
@@ -363,7 +357,9 @@ export const dict = {
"dialog.server.add.button": "Add server",
"dialog.server.add.name": "Server name (optional)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.password": "Password",
"dialog.server.add.username": "Username (optional)",
"dialog.server.add.usernamePlaceholder": "username",
"dialog.server.add.password": "Password (optional)",
"dialog.server.add.passwordPlaceholder": "password",
"dialog.server.edit.title": "Edit server",
"dialog.server.default.title": "Default server",
@@ -451,6 +447,7 @@ export const dict = {
"wsl.onboarding.versionMismatch": "Installed version does not match the desktop app version.",
"wsl.onboarding.adding": "Adding...",
"server.row.noUsername": "no username",
"server.row.incompatible":
"This server is running OpenCode {{version}}, which isn't compatible with this app. Upgrade it to OpenCode V2 to continue.",
+3
View File
@@ -450,6 +450,7 @@ export const dict = {
"dialog.server.add.button": "Añadir servidor",
"dialog.server.add.name": "Nombre del servidor (opcional)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "Nombre de usuario (opcional)",
"dialog.server.add.password": "Contraseña (opcional)",
"dialog.server.edit.title": "Editar servidor",
"dialog.server.default.title": "Servidor predeterminado",
@@ -1117,7 +1118,9 @@ export const dict = {
"app.server.unreachable": "No se pudo conectar con {{server}}",
"app.server.retrying": "Reintentando automáticamente...",
"app.server.otherServers": "Otros servidores",
"dialog.server.add.usernamePlaceholder": "usuario",
"dialog.server.add.passwordPlaceholder": "contraseña",
"server.row.noUsername": "sin usuario",
"session.review.noVcs.createGit.title": "Crear repositorio Git",
"session.review.noVcs.createGit.description": "Rastrea, revisa y deshaz cambios en este proyecto",
"session.review.noVcs.createGit.actionLoading": "Creando repositorio Git...",
+3
View File
@@ -417,6 +417,8 @@ export const dict = {
"dialog.server.add.button": "Lisa server",
"dialog.server.add.name": "Serveri nimi (valikuline)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "Kasutajanimi (valikuline)",
"dialog.server.add.usernamePlaceholder": "kasutajanimi",
"dialog.server.add.password": "Parool (valikuline)",
"dialog.server.add.passwordPlaceholder": "parool",
"dialog.server.edit.title": "Muuda serverit",
@@ -501,6 +503,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "töölaud {{version}}",
"wsl.onboarding.versionMismatch": "Installitud versioon ei ühti töölauarakenduse versiooniga.",
"wsl.onboarding.adding": "Lisamine...",
"server.row.noUsername": "kasutajanime pole",
"dialog.project.edit.title": "Redigeeri projekti",
"dialog.project.edit.name": "Nimi",
"dialog.project.edit.icon": "Ikoon",
+3
View File
@@ -418,6 +418,8 @@ export const dict = {
"dialog.server.add.button": "سرور اضافه کنید",
"dialog.server.add.name": "نام سرور (اختیاری)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "نام کاربری (اختیاری)",
"dialog.server.add.usernamePlaceholder": "نام کاربری",
"dialog.server.add.password": "رمز عبور (اختیاری)",
"dialog.server.add.passwordPlaceholder": "رمز عبور",
"dialog.server.edit.title": "ویرایش سرور",
@@ -503,6 +505,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "دسکتاپ {{version}}",
"wsl.onboarding.versionMismatch": "نسخه نصب شده با نسخه برنامه دسکتاپ مطابقت ندارد.",
"wsl.onboarding.adding": "در حال افزودن...",
"server.row.noUsername": "بدون نام کاربری",
"dialog.project.edit.title": "ویرایش پروژه",
"dialog.project.edit.name": "نام",
"dialog.project.edit.icon": "نماد",
+3
View File
@@ -325,6 +325,8 @@ export const dict = {
"dialog.server.add.button": "Lisää palvelin",
"dialog.server.add.name": "Palvelimen nimi (valinnainen)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "Käyttäjätunnus (valinnainen)",
"dialog.server.add.usernamePlaceholder": "käyttäjätunnus",
"dialog.server.add.password": "Salasana (valinnainen)",
"dialog.server.add.passwordPlaceholder": "salasana",
"dialog.server.edit.title": "Muokkaa palvelinta",
@@ -410,6 +412,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "Desktop {{version}}",
"wsl.onboarding.versionMismatch": "Asennettu versio ei vastaa työpöytäsovelluksen versiota.",
"wsl.onboarding.adding": "Lisätään...",
"server.row.noUsername": "ei käyttäjätunnusta",
"dialog.project.edit.title": "Muokkaa projektia",
"dialog.project.edit.name": "Nimi",
"dialog.project.edit.icon": "Kuvake",
+3
View File
@@ -417,6 +417,8 @@ export const dict = {
"dialog.server.add.button": "Legg ambætara til",
"dialog.server.add.name": "Ambætaranavn (valfrítt)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "Brúkaranavn (valfrítt)",
"dialog.server.add.usernamePlaceholder": "brúkaranavn",
"dialog.server.add.password": "Loyniorð (valfrítt)",
"dialog.server.add.passwordPlaceholder": "loyniorð",
"dialog.server.edit.title": "Rætta ambætara",
@@ -502,6 +504,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "skriviborð {{version}}",
"wsl.onboarding.versionMismatch": "Innsetta útgávan passar ikki til skriviborðsappiútgávuna.",
"wsl.onboarding.adding": "Leggi til...",
"server.row.noUsername": "einki brúkaranavn",
"dialog.project.edit.title": "Rætta verkætlan",
"dialog.project.edit.name": "Navn",
"dialog.project.edit.icon": "Ikon",
+3
View File
@@ -426,6 +426,7 @@ export const dict = {
"dialog.server.add.button": "Ajouter un serveur",
"dialog.server.add.name": "Nom du serveur (optionnel)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "Nom d'utilisateur (optionnel)",
"dialog.server.add.password": "Mot de passe (optionnel)",
"dialog.server.edit.title": "Modifier le serveur",
"dialog.server.default.title": "Serveur par défaut",
@@ -1043,7 +1044,9 @@ export const dict = {
"app.server.unreachable": "Impossible de joindre {{server}}",
"app.server.retrying": "Nouvelle tentative automatique...",
"app.server.otherServers": "Autres serveurs",
"dialog.server.add.usernamePlaceholder": "nom d'utilisateur",
"dialog.server.add.passwordPlaceholder": "mot de passe",
"server.row.noUsername": "aucun nom d'utilisateur",
"session.review.noVcs.createGit.title": "Créer un dépôt Git",
"session.review.noVcs.createGit.description": "Suivre, examiner et annuler les modifications dans ce projet",
"session.review.noVcs.createGit.actionLoading": "Création du dépôt Git...",
+3
View File
@@ -416,6 +416,8 @@ export const dict = {
"dialog.server.add.button": "הוסף שרת",
"dialog.server.add.name": "שם שרת (אופציונלי)",
"dialog.server.add.namePlaceholder": "מארח מקומי",
"dialog.server.add.username": "שם משתמש (אופציונלי)",
"dialog.server.add.usernamePlaceholder": "שם משתמש",
"dialog.server.add.password": "סיסמה (אופציונלי)",
"dialog.server.add.passwordPlaceholder": "סיסמה",
"dialog.server.edit.title": "ערוך שרת",
@@ -499,6 +501,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "גרסת שולחן העבודה: {{version}}",
"wsl.onboarding.versionMismatch": "הגרסה המותקנת אינה תואמת את גרסת האפליקציה לשולחן העבודה.",
"wsl.onboarding.adding": "מוסיף...",
"server.row.noUsername": "ללא שם משתמש",
"dialog.project.edit.title": "ערוך פרויקט",
"dialog.project.edit.name": "שם",
"dialog.project.edit.icon": "סמל",
+3
View File
@@ -425,6 +425,8 @@ export const dict = {
"dialog.server.add.button": "सर्वर जोड़ें",
"dialog.server.add.name": "सर्वर नाम (वैकल्पिक)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "उपयोगकर्ता नाम (वैकल्पिक)",
"dialog.server.add.usernamePlaceholder": "उपयोक्तानाम",
"dialog.server.add.password": "पासवर्ड (वैकल्पिक)",
"dialog.server.add.passwordPlaceholder": "पासवर्ड",
"dialog.server.edit.title": "सर्वर संपादित करें",
@@ -510,6 +512,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "डेस्कटॉप {{version}}",
"wsl.onboarding.versionMismatch": "इंस्टॉल किया गया संस्करण डेस्कटॉप ऐप संस्करण से मेल नहीं खाता।",
"wsl.onboarding.adding": "जोड़ा जा रहा है...",
"server.row.noUsername": "कोई उपयोगकर्ता नाम नहीं",
"dialog.project.edit.title": "प्रोजेक्ट संपादित करें",
"dialog.project.edit.name": "नाम",
"dialog.project.edit.icon": "आइकन",
+3
View File
@@ -422,6 +422,8 @@ export const dict = {
"dialog.server.add.button": "Dodaj poslužitelj",
"dialog.server.add.name": "Naziv poslužitelja (neobavezno)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "korisničko ime (nije obavezno)",
"dialog.server.add.usernamePlaceholder": "korisničko ime",
"dialog.server.add.password": "Lozinka (nije obavezno)",
"dialog.server.add.passwordPlaceholder": "lozinka",
"dialog.server.edit.title": "Uredi poslužitelj",
@@ -507,6 +509,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "radna površina {{version}}",
"wsl.onboarding.versionMismatch": "Instalirana verzija ne odgovara verziji aplikacije za stolna računala.",
"wsl.onboarding.adding": "Dodavanje...",
"server.row.noUsername": "nema korisničkog imena",
"dialog.project.edit.title": "Uredi projekt",
"dialog.project.edit.name": "Ime",
"dialog.project.edit.icon": "Ikona",
+3
View File
@@ -422,6 +422,8 @@ export const dict = {
"dialog.server.add.button": "Szerver hozzáadása",
"dialog.server.add.name": "Szerver neve (nem kötelező)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "Felhasználónév (nem kötelező)",
"dialog.server.add.usernamePlaceholder": "felhasználónév",
"dialog.server.add.password": "Jelszó (nem kötelező)",
"dialog.server.add.passwordPlaceholder": "jelszó",
"dialog.server.edit.title": "Szerver szerkesztése",
@@ -507,6 +509,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "asztali {{version}}",
"wsl.onboarding.versionMismatch": "A telepített verzió nem egyezik az asztali alkalmazás verziójával.",
"wsl.onboarding.adding": "Hozzáadás...",
"server.row.noUsername": "nincs felhasználónév",
"dialog.project.edit.title": "Projekt szerkesztése",
"dialog.project.edit.name": "Név",
"dialog.project.edit.icon": "Ikon",
+3
View File
@@ -420,6 +420,8 @@ export const dict = {
"dialog.server.add.button": "Ավելացնել սերվեր",
"dialog.server.add.name": "Սերվերի անունը (ըստ ցանկության)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "Օգտագործողի անուն (ըստ ցանկության)",
"dialog.server.add.usernamePlaceholder": "օգտվողի անուն",
"dialog.server.add.password": "Գաղտնաբառ (ըստ ցանկության)",
"dialog.server.add.passwordPlaceholder": "գաղտնաբառ",
"dialog.server.edit.title": "Խմբագրել սերվերը",
@@ -505,6 +507,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "աշխատասեղանի {{version}}",
"wsl.onboarding.versionMismatch": "Տեղադրված տարբերակը չի համապատասխանում աշխատասեղանի հավելվածի տարբերակին։",
"wsl.onboarding.adding": "Ավելացվում է...",
"server.row.noUsername": "առանց օգտվողի անուն",
"dialog.project.edit.title": "Խմբագրել նախագիծը",
"dialog.project.edit.name": "Անուն",
"dialog.project.edit.icon": "Պատկերակ",
+3
View File
@@ -453,6 +453,8 @@ export const dict = {
"dialog.server.add.button": "Tambah server",
"dialog.server.add.name": "Nama server (opsional)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "Nama pengguna (opsional)",
"dialog.server.add.usernamePlaceholder": "nama pengguna",
"dialog.server.add.password": "Kata sandi (opsional)",
"dialog.server.add.passwordPlaceholder": "kata sandi",
"dialog.server.edit.title": "Sunting server",
@@ -540,6 +542,7 @@ export const dict = {
"wsl.onboarding.versionMismatch": "Versi terinstal tidak cocok dengan versi aplikasi desktop.",
"wsl.onboarding.adding": "Menambahkan...",
"server.row.noUsername": "tanpa nama pengguna",
"dialog.project.edit.title": "Sunting proyek",
"dialog.project.edit.name": "Nama",
+3
View File
@@ -422,6 +422,8 @@ export const dict = {
"dialog.server.add.button": "Bæta við netþjóni",
"dialog.server.add.name": "Nafn netþjóns (valfrjálst)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "Notandanafn (valfrjálst)",
"dialog.server.add.usernamePlaceholder": "notendanafn",
"dialog.server.add.password": "Lykilorð (valfrjálst)",
"dialog.server.add.passwordPlaceholder": "lykilorð",
"dialog.server.edit.title": "Breyta miðlara",
@@ -507,6 +509,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "skrifborð {{version}}",
"wsl.onboarding.versionMismatch": "Uppsett útgáfa passar ekki við útgáfu skrifborðsforritsins.",
"wsl.onboarding.adding": "Bætir við...",
"server.row.noUsername": "ekkert notendanafn",
"dialog.project.edit.title": "Breyta verkefni",
"dialog.project.edit.name": "Nafn",
"dialog.project.edit.icon": "Táknmynd",
+3
View File
@@ -327,6 +327,8 @@ export const dict = {
"dialog.server.add.button": "Aggiungi server",
"dialog.server.add.name": "Nome del server (facoltativo)",
"dialog.server.add.namePlaceholder": "Host locale",
"dialog.server.add.username": "Nome utente (facoltativo)",
"dialog.server.add.usernamePlaceholder": "nomeutente",
"dialog.server.add.password": "Password (facoltativa)",
"dialog.server.add.passwordPlaceholder": "password",
"dialog.server.edit.title": "Modifica server",
@@ -412,6 +414,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "desktop {{version}}",
"wsl.onboarding.versionMismatch": "La versione installata non corrisponde alla versione dell'app desktop.",
"wsl.onboarding.adding": "Aggiunta...",
"server.row.noUsername": "nessun nome utente",
"dialog.project.edit.title": "Modifica progetto",
"dialog.project.edit.name": "Nome",
"dialog.project.edit.icon": "Icona",
+3
View File
@@ -419,6 +419,7 @@ export const dict = {
"dialog.server.add.button": "サーバーを追加",
"dialog.server.add.name": "サーバー名 (オプション)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "ユーザー名 (オプション)",
"dialog.server.add.password": "パスワード (オプション)",
"dialog.server.edit.title": "サーバーを編集",
"dialog.server.default.title": "デフォルトサーバー",
@@ -1018,7 +1019,9 @@ export const dict = {
"app.server.unreachable": "{{server}} に到達できませんでした",
"app.server.retrying": "自動的に再試行中...",
"app.server.otherServers": "その他のサーバー",
"dialog.server.add.usernamePlaceholder": "ユーザー名",
"dialog.server.add.passwordPlaceholder": "パスワード",
"server.row.noUsername": "ユーザー名なし",
"session.review.noVcs.createGit.title": "Git リポジトリを作成",
"session.review.noVcs.createGit.description": "このプロジェクトの変更を追跡、レビュー、元に戻す",
"session.review.noVcs.createGit.actionLoading": "Git リポジトリを作成中...",
+3
View File
@@ -418,6 +418,8 @@ export const dict = {
"dialog.server.add.button": "სერვერის დამატება",
"dialog.server.add.name": "სერვერის სახელი (არასავალდებულო)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "მომხმარებლის სახელი (არასავალდებულო)",
"dialog.server.add.usernamePlaceholder": "მომხმარებლის სახელი",
"dialog.server.add.password": "პაროლი (არასავალდებულო)",
"dialog.server.add.passwordPlaceholder": "პაროლი",
"dialog.server.edit.title": "სერვერის რედაქტირება",
@@ -503,6 +505,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "დესკტოპის {{version}}",
"wsl.onboarding.versionMismatch": "დაინსტალირებული ვერსია არ ემთხვევა დესკტოპის აპის ვერსიას.",
"wsl.onboarding.adding": "დამატება...",
"server.row.noUsername": "მომხმარებლის სახელის გარეშე",
"dialog.project.edit.title": "პროექტის რედაქტირება",
"dialog.project.edit.name": "სახელი",
"dialog.project.edit.icon": "ხატულა",
+3
View File
@@ -417,6 +417,8 @@ export const dict = {
"dialog.server.add.button": "បន្ថែមម៉ាស៊ីនមេ",
"dialog.server.add.name": "ឈ្មោះម៉ាស៊ីនមេ (ជាជម្រើស)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "ឈ្មោះអ្នកប្រើប្រាស់ (ជាជម្រើស)",
"dialog.server.add.usernamePlaceholder": "ឈ្មោះអ្នកប្រើ",
"dialog.server.add.password": "ពាក្យសម្ងាត់ (ជាជម្រើស)\nពាក្យសម្ងាត់",
"dialog.server.add.passwordPlaceholder": "ពាក្យសម្ងាត់",
"dialog.server.edit.title": "កែសម្រួលម៉ាស៊ីនមេ",
@@ -502,6 +504,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "ផ្ទៃតុ {{version}}",
"wsl.onboarding.versionMismatch": "កំណែដែលបានដំឡើងមិនត្រូវគ្នានឹងកំណែកម្មវិធីកុំព្យូទ័រលើតុទេ។",
"wsl.onboarding.adding": "កំពុងបន្ថែម...",
"server.row.noUsername": "គ្មានឈ្មោះអ្នកប្រើប្រាស់",
"dialog.project.edit.title": "កែសម្រួលគម្រោង",
"dialog.project.edit.name": "ឈ្មោះ",
"dialog.project.edit.icon": "រូបតំណាង",
+3
View File
@@ -308,6 +308,7 @@ export const dict = {
"dialog.server.add.button": "서버 추가",
"dialog.server.add.name": "서버 이름 (선택 사항)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "사용자 이름 (선택 사항)",
"dialog.server.add.password": "비밀번호 (선택 사항)",
"dialog.server.edit.title": "서버 편집",
"dialog.server.default.title": "기본 서버",
@@ -767,7 +768,9 @@ export const dict = {
"app.server.unreachable": "{{server}}에 연결할 수 없습니다",
"app.server.retrying": "자동으로 재시도 중...",
"app.server.otherServers": "다른 서버",
"dialog.server.add.usernamePlaceholder": "사용자 이름",
"dialog.server.add.passwordPlaceholder": "비밀번호",
"server.row.noUsername": "사용자 이름 없음",
"session.review.noVcs.createGit.title": "Git 저장소 생성",
"session.review.noVcs.createGit.description": "이 프로젝트의 변경 사항을 추적, 검토 및 실행 취소",
"session.review.noVcs.createGit.actionLoading": "Git 저장소 생성 중...",
+3
View File
@@ -417,6 +417,8 @@ export const dict = {
"dialog.server.add.button": "ເພີ່ມເຊີບເວີ",
"dialog.server.add.name": "ຊື່ເຊີບເວີ (ທາງເລືອກ)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "ຊື່ຜູ້ໃຊ້ (ທາງເລືອກ)",
"dialog.server.add.usernamePlaceholder": "ຊື່ຜູ້ໃຊ້",
"dialog.server.add.password": "ລະຫັດຜ່ານ (ທາງເລືອກ)",
"dialog.server.add.passwordPlaceholder": "ລະຫັດຜ່ານ",
"dialog.server.edit.title": "ແກ້ໄຂເຊີບເວີ",
@@ -501,6 +503,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "ເດັສທັອບ {{version}}",
"wsl.onboarding.versionMismatch": "ເວີຊັນທີ່ຕິດຕັ້ງບໍ່ກົງກັບເວີຊັນຂອງແອັບ desktop.",
"wsl.onboarding.adding": "ກຳລັງເພີ່ມ...",
"server.row.noUsername": "ບໍ່ມີຊື່ຜູ້ໃຊ້",
"dialog.project.edit.title": "ແກ້ໄຂໂຄງການ",
"dialog.project.edit.name": "ຊື່",
"dialog.project.edit.icon": "ໄອຄອນ",
+3
View File
@@ -423,6 +423,8 @@ export const dict = {
"dialog.server.add.button": "Pridėti serverį",
"dialog.server.add.name": "Serverio pavadinimas (neprivaloma)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "Vartotojo vardas (neprivaloma)",
"dialog.server.add.usernamePlaceholder": "vartotojo vardas",
"dialog.server.add.password": "Slaptažodis (neprivaloma)",
"dialog.server.add.passwordPlaceholder": "slaptažodis",
"dialog.server.edit.title": "Redaguoti serverį",
@@ -508,6 +510,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "darbalaukis {{version}}",
"wsl.onboarding.versionMismatch": "Įdiegta versija neatitinka darbalaukio programos versijos.",
"wsl.onboarding.adding": "Pridedama...",
"server.row.noUsername": "jokio vartotojo vardo",
"dialog.project.edit.title": "Redaguoti projektą",
"dialog.project.edit.name": "Vardas",
"dialog.project.edit.icon": "Piktograma",
+3
View File
@@ -418,6 +418,8 @@ export const dict = {
"dialog.server.add.button": "Pievienot serveri",
"dialog.server.add.name": "Servera nosaukums (nav obligāti)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "Lietotājvārds (nav obligāti)",
"dialog.server.add.usernamePlaceholder": "lietotājvārds",
"dialog.server.add.password": "Parole (nav obligāti)",
"dialog.server.add.passwordPlaceholder": "parole",
"dialog.server.edit.title": "Rediģēt serveri",
@@ -503,6 +505,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "darbvirsmas {{version}}",
"wsl.onboarding.versionMismatch": "Instalētā versija neatbilst darbvirsmas lietotnes versijai.",
"wsl.onboarding.adding": "Pievieno...",
"server.row.noUsername": "nav lietotājvārda",
"dialog.project.edit.title": "Rediģēt projektu",
"dialog.project.edit.name": "Nosaukums",
"dialog.project.edit.icon": "Ikona",
+3
View File
@@ -419,6 +419,8 @@ export const dict = {
"dialog.server.add.button": "Додај сервер",
"dialog.server.add.name": "Име на сервер (изборно)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "Корисничко име (изборно)",
"dialog.server.add.usernamePlaceholder": "корисничко име",
"dialog.server.add.password": "Лозинка (изборно)",
"dialog.server.add.passwordPlaceholder": "лозинка",
"dialog.server.edit.title": "Уреди сервер",
@@ -504,6 +506,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "работна површина {{version}}",
"wsl.onboarding.versionMismatch": "Инсталираната верзија не се совпаѓа со верзијата на апликацијата за десктоп.",
"wsl.onboarding.adding": "Се додава...",
"server.row.noUsername": "нема корисничко име",
"dialog.project.edit.title": "Уреди проект",
"dialog.project.edit.name": "Име",
"dialog.project.edit.icon": "Икона",
+3
View File
@@ -421,6 +421,8 @@ export const dict = {
"dialog.server.add.button": "Сервер нэмэх",
"dialog.server.add.name": "Серверийн нэр (заавал биш)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "Хэрэглэгчийн нэр (заавал биш)",
"dialog.server.add.usernamePlaceholder": "хэрэглэгчийн нэр",
"dialog.server.add.password": "Нууц үг (заавал биш)",
"dialog.server.add.passwordPlaceholder": "нууц үг",
"dialog.server.edit.title": "Сервер засах",
@@ -506,6 +508,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "ширээний компьютер {{version}}",
"wsl.onboarding.versionMismatch": "Суулгасан хувилбар нь ширээний програмын хувилбартай таарахгүй байна.",
"wsl.onboarding.adding": "Нэмэж байна...",
"server.row.noUsername": "хэрэглэгчийн нэр байхгүй",
"dialog.project.edit.title": "Төслийг засварлах",
"dialog.project.edit.name": "Нэр",
"dialog.project.edit.icon": "Дүрс",
+3
View File
@@ -418,6 +418,8 @@ export const dict = {
"dialog.server.add.button": "Tambah pelayan",
"dialog.server.add.name": "Nama pelayan (pilihan)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "Nama pengguna (pilihan)",
"dialog.server.add.usernamePlaceholder": "nama pengguna",
"dialog.server.add.password": "Kata laluan (pilihan)",
"dialog.server.add.passwordPlaceholder": "kata laluan",
"dialog.server.edit.title": "Edit pelayan",
@@ -503,6 +505,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "desktop {{version}}",
"wsl.onboarding.versionMismatch": "Versi yang dipasang tidak sepadan dengan versi aplikasi desktop.",
"wsl.onboarding.adding": "Sedang menambah...",
"server.row.noUsername": "tiada nama pengguna",
"dialog.project.edit.title": "Edit projek",
"dialog.project.edit.name": "Nama",
"dialog.project.edit.icon": "Ikon",
+3
View File
@@ -422,6 +422,8 @@ export const dict = {
"dialog.server.add.button": "ဆာဗာထည့်ပါ။",
"dialog.server.add.name": "ဆာဗာအမည် (ချန်လှပ်ထားနိုင်သည်)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "အသုံးပြုသူအမည် (ချန်လှပ်ထားနိုင်သည်)",
"dialog.server.add.usernamePlaceholder": "အသုံးပြုသူအမည်",
"dialog.server.add.password": "စကားဝှက် (ချန်လှပ်ထားနိုင်သည်)",
"dialog.server.add.passwordPlaceholder": "စကားဝှက်",
"dialog.server.edit.title": "ဆာဗာကို တည်းဖြတ်ပါ။",
@@ -507,6 +509,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "ဒက်စ်တော့ {{version}}",
"wsl.onboarding.versionMismatch": "ထည့်သွင်းထားသောဗားရှင်းသည် ဒက်စ်တော့အက်ပ်ဗားရှင်းနှင့် မကိုက်ညီပါ။",
"wsl.onboarding.adding": "ထည့်နေသည်...",
"server.row.noUsername": "အသုံးပြုသူအမည်မရှိပါ။",
"dialog.project.edit.title": "ပရောဂျက်ကို တည်းဖြတ်ပါ။",
"dialog.project.edit.name": "အမည်",
"dialog.project.edit.icon": "သင်္ကေတ",
+3
View File
@@ -419,6 +419,8 @@ export const dict: Record<string, string> = {
"dialog.server.add.button": "सर्भर थप्नुहोस्",
"dialog.server.add.name": "सर्भर नाम (वैकल्पिक)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "प्रयोगकर्ता नाम (वैकल्पिक)",
"dialog.server.add.usernamePlaceholder": "प्रयोगकर्ता नाम",
"dialog.server.add.password": "पासवर्ड (वैकल्पिक)",
"dialog.server.add.passwordPlaceholder": "पासवर्ड",
"dialog.server.edit.title": "सर्भर सम्पादन गर्नुहोस्",
@@ -504,6 +506,7 @@ export const dict: Record<string, string> = {
"wsl.onboarding.desktopVersion": "डेस्कटप {{version}}",
"wsl.onboarding.versionMismatch": "स्थापना गरिएको संस्करण डेस्कटप एप संस्करणसँग मेल खाँदैन।",
"wsl.onboarding.adding": "थप्दै...",
"server.row.noUsername": "प्रयोगकर्ता नाम छैन",
"dialog.project.edit.title": "परियोजना सम्पादन गर्नुहोस्",
"dialog.project.edit.name": "नाम",
"dialog.project.edit.icon": "आइकन",
+3
View File
@@ -419,6 +419,8 @@ export const dict = {
"dialog.server.add.button": "Server toevoegen",
"dialog.server.add.name": "Servernaam (optioneel)",
"dialog.server.add.namePlaceholder": "Lokale host",
"dialog.server.add.username": "Gebruikersnaam (optioneel)",
"dialog.server.add.usernamePlaceholder": "gebruikersnaam",
"dialog.server.add.password": "Wachtwoord (optioneel)",
"dialog.server.add.passwordPlaceholder": "wachtwoord",
"dialog.server.edit.title": "Server bewerken",
@@ -504,6 +506,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "desktop {{version}}",
"wsl.onboarding.versionMismatch": "De geïnstalleerde versie komt niet overeen met de versie van de desktop-app.",
"wsl.onboarding.adding": "Toevoegen...",
"server.row.noUsername": "geen gebruikersnaam",
"dialog.project.edit.title": "Project bewerken",
"dialog.project.edit.name": "Naam",
"dialog.project.edit.icon": "Pictogram",
+3
View File
@@ -439,6 +439,7 @@ export const dict = {
"dialog.server.add.button": "Legg til server",
"dialog.server.add.name": "Servernavn (valgfritt)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "Brukernavn (valgfritt)",
"dialog.server.add.password": "Passord (valgfritt)",
"dialog.server.edit.title": "Rediger server",
"dialog.server.default.title": "Standardserver",
@@ -962,7 +963,9 @@ export const dict = {
"app.server.unreachable": "Kunne ikke nå {{server}}",
"app.server.retrying": "Prøver på nytt automatisk...",
"app.server.otherServers": "Andre servere",
"dialog.server.add.usernamePlaceholder": "brukernavn",
"dialog.server.add.passwordPlaceholder": "passord",
"server.row.noUsername": "ikke noe brukernavn",
"session.review.noVcs.createGit.title": "Opprett et Git-depot",
"session.review.noVcs.createGit.description": "Spor, gjennomgå og angre endringer i dette prosjektet",
"session.review.noVcs.createGit.actionLoading": "Oppretter Git-depot...",
+3
View File
@@ -425,6 +425,8 @@ export const dict = {
"dialog.server.add.button": "سرور شامل کرو",
"dialog.server.add.name": "سرور دا ناں (اختیاری)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "ورتن آلا ناں (اختیاری)",
"dialog.server.add.usernamePlaceholder": "ورتن آلا ناں",
"dialog.server.add.password": "پاس ورڈ (اختیاری)",
"dialog.server.add.passwordPlaceholder": "پاس ورڈ",
"dialog.server.edit.title": "سرور وچ ترمیم کرو",
@@ -510,6 +512,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "ڈیسک ٹاپ {{version}}",
"wsl.onboarding.versionMismatch": "انسٹال کیتا گیا ورژن ڈیسک ٹاپ ایپ ورژن نال میل نئیں کھاندا۔",
"wsl.onboarding.adding": "شامل کر رہیا واں...",
"server.row.noUsername": "کوئی ورتن آلا ناں نئیں",
"dialog.project.edit.title": "پروجیکٹ وچ ترمیم کرو",
"dialog.project.edit.name": "ناں",
"dialog.project.edit.icon": "آئکن",
+3
View File
@@ -422,6 +422,7 @@ export const dict = {
"dialog.server.add.button": "Dodaj serwer",
"dialog.server.add.name": "Nazwa serwera (opcjonalnie)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "Nazwa użytkownika (opcjonalnie)",
"dialog.server.add.password": "Hasło (opcjonalnie)",
"dialog.server.edit.title": "Edytuj serwer",
"dialog.server.default.title": "Domyślny serwer",
@@ -1035,7 +1036,9 @@ export const dict = {
"app.server.unreachable": "Nie można połączyć z {{server}}",
"app.server.retrying": "Ponawianie automatycznie...",
"app.server.otherServers": "Inne serwery",
"dialog.server.add.usernamePlaceholder": "nazwa użytkownika",
"dialog.server.add.passwordPlaceholder": "hasło",
"server.row.noUsername": "brak nazwy użytkownika",
"session.review.noVcs.createGit.title": "Utwórz repozytorium Git",
"session.review.noVcs.createGit.description": "Śledź, przeglądaj i cofaj zmiany w tym projekcie",
"session.review.noVcs.createGit.actionLoading": "Tworzenie repozytorium Git...",
+3
View File
@@ -417,6 +417,8 @@ export const dict = {
"dialog.server.add.button": "Adaugă server",
"dialog.server.add.name": "Nume server (opțional)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "Utilizator (opțional)",
"dialog.server.add.usernamePlaceholder": "utilizator",
"dialog.server.add.password": "Parolă (opțional)",
"dialog.server.add.passwordPlaceholder": "parolă",
"dialog.server.edit.title": "Editează server",
@@ -502,6 +504,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "desktop {{version}}",
"wsl.onboarding.versionMismatch": "Versiunea instalată nu corespunde cu versiunea aplicației desktop.",
"wsl.onboarding.adding": "Se adaugă...",
"server.row.noUsername": "fără nume de utilizator",
"dialog.project.edit.title": "Editează proiectul",
"dialog.project.edit.name": "Nume",
"dialog.project.edit.icon": "Pictogramă",
+3
View File
@@ -447,6 +447,7 @@ export const dict = {
"dialog.server.add.button": "Добавить сервер",
"dialog.server.add.name": "Имя сервера (необязательно)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "Имя пользователя (необязательно)",
"dialog.server.add.password": "Пароль (необязательно)",
"dialog.server.edit.title": "Редактировать сервер",
"dialog.server.default.title": "Сервер по умолчанию",
@@ -1112,7 +1113,9 @@ export const dict = {
"app.server.unreachable": "Не удалось связаться с {{server}}",
"app.server.retrying": "Автоматическая повторная попытка...",
"app.server.otherServers": "Другие серверы",
"dialog.server.add.usernamePlaceholder": "имя пользователя",
"dialog.server.add.passwordPlaceholder": "пароль",
"server.row.noUsername": "нет имени пользователя",
"session.review.noVcs.createGit.title": "Создать репозиторий Git",
"session.review.noVcs.createGit.description": "Отслеживайте, просматривайте и отменяйте изменения в этом проекте",
"session.review.noVcs.createGit.actionLoading": "Создание репозитория Git...",
+3
View File
@@ -417,6 +417,8 @@ export const dict: Record<string, string> = {
"dialog.server.add.button": "සේවාදායකය එක් කරන්න",
"dialog.server.add.name": "සේවාදායකයේ නම (විකල්ප)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "පරිශීලක නාමය (විකල්ප)",
"dialog.server.add.usernamePlaceholder": "පරිශීලක නාමය",
"dialog.server.add.password": "මුරපදය (විකල්ප)",
"dialog.server.add.passwordPlaceholder": "මුරපදය",
"dialog.server.edit.title": "සේවාදායකය සංස්කරණය කරන්න",
@@ -502,6 +504,7 @@ export const dict: Record<string, string> = {
"wsl.onboarding.desktopVersion": "ඩෙස්ක්ටොප් {{version}}",
"wsl.onboarding.versionMismatch": "ස්ථාපිත අනුවාදය ඩෙස්ක්ටොප් යෙදුම් අනුවාදයට නොගැලපේ.",
"wsl.onboarding.adding": "එකතු කරමින්...",
"server.row.noUsername": "පරිශීලක නාමයක් නැත",
"dialog.project.edit.title": "ව්‍යාපෘතිය සංස්කරණය කරන්න",
"dialog.project.edit.name": "නම",
"dialog.project.edit.icon": "නිරූපකය",
+3
View File
@@ -417,6 +417,8 @@ export const dict = {
"dialog.server.add.button": "Pridať server",
"dialog.server.add.name": "Názov servera (voliteľné)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "Používateľské meno (voliteľné)",
"dialog.server.add.usernamePlaceholder": "používateľ",
"dialog.server.add.password": "Heslo (voliteľné)",
"dialog.server.add.passwordPlaceholder": "heslo",
"dialog.server.edit.title": "Upraviť server",
@@ -502,6 +504,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "desktop {{version}}",
"wsl.onboarding.versionMismatch": "Nainštalovaná verzia sa nezhoduje s verziou desktopovej aplikácie.",
"wsl.onboarding.adding": "Pridáva sa...",
"server.row.noUsername": "bez používateľského mena",
"dialog.project.edit.title": "Upraviť projekt",
"dialog.project.edit.name": "Názov",
"dialog.project.edit.icon": "Ikona",
+3
View File
@@ -417,6 +417,8 @@ export const dict = {
"dialog.server.add.button": "Dodaj strežnik",
"dialog.server.add.name": "Ime strežnika (neobvezno)",
"dialog.server.add.namePlaceholder": "Lokalni gostitelj",
"dialog.server.add.username": "uporabniško ime (neobvezno)",
"dialog.server.add.usernamePlaceholder": "uporabniško ime",
"dialog.server.add.password": "Geslo (neobvezno)",
"dialog.server.add.passwordPlaceholder": "geslo",
"dialog.server.edit.title": "Uredi strežnik",
@@ -502,6 +504,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "namizje {{version}}",
"wsl.onboarding.versionMismatch": "Nameščena različica se ne ujema z različico namizne aplikacije.",
"wsl.onboarding.adding": "Dodajanje ...",
"server.row.noUsername": "brez uporabniškega imena",
"dialog.project.edit.title": "Uredi projekt",
"dialog.project.edit.name": "Ime",
"dialog.project.edit.icon": "Ikona",
+3
View File
@@ -419,6 +419,8 @@ export const dict = {
"dialog.server.add.button": "Shto server",
"dialog.server.add.name": "Emri i serverit (opsionale)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "Emri i përdoruesit (opsionale)",
"dialog.server.add.usernamePlaceholder": "emri i përdoruesit",
"dialog.server.add.password": "Fjalëkalimi (opsionale)",
"dialog.server.add.passwordPlaceholder": "fjalëkalimin",
"dialog.server.edit.title": "Redakto serverin",
@@ -503,6 +505,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "desktop {{version}}",
"wsl.onboarding.versionMismatch": "Versioni i instaluar nuk përputhet me versionin e aplikacionit desktop.",
"wsl.onboarding.adding": "Po shtohet...",
"server.row.noUsername": "asnjë emër përdoruesi",
"dialog.project.edit.title": "Redakto projektin",
"dialog.project.edit.name": "Emri",
"dialog.project.edit.icon": "Ikona",
+3
View File
@@ -418,6 +418,8 @@ export const dict = {
"dialog.server.add.button": "Додај сервер",
"dialog.server.add.name": "Име сервера (опционо)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "Корисничко име (опционално)",
"dialog.server.add.usernamePlaceholder": "корисничко име",
"dialog.server.add.password": "Лозинка (опционо)",
"dialog.server.add.passwordPlaceholder": "лозинка",
"dialog.server.edit.title": "Уреди сервер",
@@ -503,6 +505,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "десктоп {{version}}",
"wsl.onboarding.versionMismatch": "Инсталирана верзија не одговара верзији апликације за рачунар.",
"wsl.onboarding.adding": "Додавање...",
"server.row.noUsername": "нема корисничког имена",
"dialog.project.edit.title": "Уреди пројекат",
"dialog.project.edit.name": "Назив",
"dialog.project.edit.icon": "Икона",
+3
View File
@@ -419,6 +419,8 @@ export const dict = {
"dialog.server.add.button": "Lägg till server",
"dialog.server.add.name": "Servernamn (valfritt)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "Användarnamn (valfritt)",
"dialog.server.add.usernamePlaceholder": "användarnamn",
"dialog.server.add.password": "Lösenord (valfritt)",
"dialog.server.add.passwordPlaceholder": "lösenord",
"dialog.server.edit.title": "Redigera server",
@@ -504,6 +506,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "desktop {{version}}",
"wsl.onboarding.versionMismatch": "Den installerade versionen matchar inte versionen av skrivbordsappen.",
"wsl.onboarding.adding": "Lägger till...",
"server.row.noUsername": "inget användarnamn",
"dialog.project.edit.title": "Redigera projekt",
"dialog.project.edit.name": "Namn",
"dialog.project.edit.icon": "Ikon",
+3
View File
@@ -420,6 +420,8 @@ export const dict = {
"dialog.server.add.button": "Илова кардани сервер",
"dialog.server.add.name": "Номи сервер (ихтиёрӣ)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "Номи корбар (ихтиёрӣ)",
"dialog.server.add.usernamePlaceholder": "номи корбар",
"dialog.server.add.password": "Рамз (ихтиёрӣ)",
"dialog.server.add.passwordPlaceholder": "парол",
"dialog.server.edit.title": "Серверро таҳрир кунед",
@@ -505,6 +507,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "мизи корӣ {{version}}",
"wsl.onboarding.versionMismatch": "Версияи насбшуда ба версияи барномаи мизи корӣ мувофиқат намекунад.",
"wsl.onboarding.adding": "Илова кардан...",
"server.row.noUsername": "номи корбар нест",
"dialog.project.edit.title": "Таҳрири лоиҳа",
"dialog.project.edit.name": "Ном",
"dialog.project.edit.icon": "Нишона",
+3
View File
@@ -446,6 +446,7 @@ export const dict = {
"dialog.server.add.button": "เพิ่มเซิร์ฟเวอร์",
"dialog.server.add.name": "ชื่อเซิร์ฟเวอร์ (ไม่บังคับ)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "ชื่อผู้ใช้ (ไม่บังคับ)",
"dialog.server.add.password": "รหัสผ่าน (ไม่บังคับ)",
"dialog.server.edit.title": "แก้ไขเซิร์ฟเวอร์",
"dialog.server.default.title": "เซิร์ฟเวอร์เริ่มต้น",
@@ -1096,7 +1097,9 @@ export const dict = {
"app.server.unreachable": "ไม่สามารถติดต่อ {{server}}",
"app.server.retrying": "กำลังลองใหม่โดยอัตโนมัติ...",
"app.server.otherServers": "เซิร์ฟเวอร์อื่น ๆ",
"dialog.server.add.usernamePlaceholder": "ชื่อผู้ใช้",
"dialog.server.add.passwordPlaceholder": "รหัสผ่าน",
"server.row.noUsername": "ไม่มีชื่อผู้ใช้",
"session.review.noVcs.createGit.title": "สร้าง Git รีโพซิทอรี",
"session.review.noVcs.createGit.description": "ติดตาม ตรวจสอบ และเลิกทำการเปลี่ยนแปลงในโปรเจกต์นี้",
"session.review.noVcs.createGit.actionLoading": "กำลังสร้าง Git รีโพซิทอรี...",
+3
View File
@@ -418,6 +418,8 @@ export const dict = {
"dialog.server.add.button": "Serwer goşuň",
"dialog.server.add.name": "Serweriň ady (islege görä)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "Ulanyjy ady (islege görä)",
"dialog.server.add.usernamePlaceholder": "ulanyjy ady",
"dialog.server.add.password": "Parol (islege görä)",
"dialog.server.add.passwordPlaceholder": "parol",
"dialog.server.edit.title": "Serweri redaktirläň",
@@ -502,6 +504,7 @@ export const dict = {
"wsl.onboarding.desktopVersion": "iş stoly {{version}}",
"wsl.onboarding.versionMismatch": "Gurlan wersiýa iş stoly programma wersiýasyna gabat gelenok.",
"wsl.onboarding.adding": "Goşmak ...",
"server.row.noUsername": "ulanyjy ady ýok",
"dialog.project.edit.title": "Taslamany redaktirläň",
"dialog.project.edit.name": "Ady",
"dialog.project.edit.icon": "Nyşan",
+3
View File
@@ -453,6 +453,7 @@ export const dict = {
"dialog.server.add.button": "Sunucu ekle",
"dialog.server.add.name": "Sunucu adı (isteğe bağlı)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "Kullanıcı adı (isteğe bağlı)",
"dialog.server.add.password": "Parola (isteğe bağlı)",
"dialog.server.edit.title": "Sunucuyu düzenle",
"dialog.server.default.title": "Varsayılan sunucu",
@@ -1119,7 +1120,9 @@ export const dict = {
"app.server.unreachable": "{{server}} sunucusuna ulaşılamadı",
"app.server.retrying": "Otomatik olarak tekrar deneniyor...",
"app.server.otherServers": "Diğer sunucular",
"dialog.server.add.usernamePlaceholder": "kullanıcı adı",
"dialog.server.add.passwordPlaceholder": "parola",
"server.row.noUsername": "kullanıcı adı yok",
"session.review.noVcs.createGit.title": "Git deposu oluştur",
"session.review.noVcs.createGit.description": "Bu projedeki değişiklikleri takip et, incele ve geri al",
"session.review.noVcs.createGit.actionLoading": "Git deposu oluşturuluyor...",
+3
View File
@@ -454,6 +454,8 @@ export const dict = {
"dialog.server.add.button": "Додати сервер",
"dialog.server.add.name": "Назва сервера (необов'язково)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "Ім'я користувача (необов'язково)",
"dialog.server.add.usernamePlaceholder": "ім'я користувача",
"dialog.server.add.password": "Пароль (необов'язково)",
"dialog.server.add.passwordPlaceholder": "пароль",
"dialog.server.edit.title": "Редагувати сервер",
@@ -541,6 +543,7 @@ export const dict = {
"wsl.onboarding.versionMismatch": "Встановлена версія не відповідає версії десктопного застосунку.",
"wsl.onboarding.adding": "Додавання...",
"server.row.noUsername": "без імені користувача",
"dialog.project.edit.title": "Редагувати проєкт",
"dialog.project.edit.name": "Назва",

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