mirror of
https://gitee.com/zhangsken/opencode.git
synced 2026-10-25 10:00:48 +08:00
Compare commits
454 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cc49556e61 | |||
| e059ac5918 | |||
| 70a24697ea | |||
| cf494c2029 | |||
| c10134729d | |||
| 0e3dfd1769 | |||
| 8bf288ecb1 | |||
| 17b7678875 | |||
| d870e22c70 | |||
| 45ad8dc38a | |||
| 8b2de7317b | |||
| ac88182b3e | |||
| ebb7b76eca | |||
| 11be5bc29c | |||
| 83abc64a5c | |||
| fee476bb90 | |||
| ae93d4afb3 | |||
| 9a53565790 | |||
| 61eae8811a | |||
| 34b4c3cd9f | |||
| 3c7f191972 | |||
| 7c22dbbaf9 | |||
| 6da6d47aab | |||
| f1aacaba22 | |||
| 4e1c49630a | |||
| a6cad7743f | |||
| ae2da69004 | |||
| 285cff53da | |||
| 5f9d9187c0 | |||
| 1573a7b608 | |||
| 3dd1b30539 | |||
| e80db11160 | |||
| a7f2373a05 | |||
| b02acc1e30 | |||
| 5a8335857b | |||
| 88c6c7abc7 | |||
| a6183af6f3 | |||
| 2e018f70f2 | |||
| 1364769e51 | |||
| 350c726aa8 | |||
| ee91f190c5 | |||
| 501ff62cbf | |||
| e03db9bc69 | |||
| 403627b031 | |||
| 5cf4e13588 | |||
| 4ae176520d | |||
| a74c472ffb | |||
| a97622c801 | |||
| 7f209437b5 | |||
| 228e9095ba | |||
| df23b7f948 | |||
| 631f67a9f3 | |||
| 199a4cdbea | |||
| a453386e9d | |||
| ca34cecc03 | |||
| 95daf90670 | |||
| 0b934e9516 | |||
| 5de2f241c6 | |||
| 193de13a88 | |||
| 43e89ea167 | |||
| b7ca4f91d9 | |||
| b3f1a96c6d | |||
| 859106eb17 | |||
| 486e8460b1 | |||
| 28a62b7148 | |||
| a9a6fad0fa | |||
| b6914b39db | |||
| 72635a3720 | |||
| 9f8db119fc | |||
| f69beceaff | |||
| 830d5eb535 | |||
| 5cd8e68fdd | |||
| dff8fbc149 | |||
| ac1758c0e6 | |||
| d6855b6b47 | |||
| eebd85f5d1 | |||
| ecbc6ccac8 | |||
| 0b082b065d | |||
| 57ef382843 | |||
| f914cac3d4 | |||
| 53fec37d8d | |||
| a03bba5f2c | |||
| 56d4fb24a1 | |||
| 13e3744f76 | |||
| e207624c48 | |||
| ea2d59d7ca | |||
| bec9ee41af | |||
| c470c79513 | |||
| 23ec4f55c8 | |||
| 337fd144d2 | |||
| 1ddaeec480 | |||
| 7c2199d84a | |||
| bbd72fb8b0 | |||
| e2894562f8 | |||
| 70b4ca8c18 | |||
| 4db1eec321 | |||
| 79903a4cf7 | |||
| e8548ae8b2 | |||
| 7774580e61 | |||
| 5b1e31988e | |||
| 02a167e048 | |||
| 500c46ec79 | |||
| 5cf9f517cf | |||
| 5d5c35ee71 | |||
| 51f86c8537 | |||
| 20a7743876 | |||
| 31afdd5938 | |||
| 4178fd74f1 | |||
| 3f31139064 | |||
| 475b408119 | |||
| 70f74112e3 | |||
| 03cb632435 | |||
| dd417f1a6b | |||
| 8b9f89e7e8 | |||
| c0f09afef5 | |||
| 8a6cf2c9aa | |||
| a935432b5c | |||
| 7561b4a050 | |||
| d8eb3b80fb | |||
| 08c483dc36 | |||
| d2efd81fb3 | |||
| 79d503150c | |||
| f12e14cf16 | |||
| b578b7261f | |||
| bbe4c952d7 | |||
| 3a9d4e78b6 | |||
| 8d1f8916d3 | |||
| 05028334b2 | |||
| b04697366f | |||
| 4eb29a64f0 | |||
| 68abdce1a0 | |||
| ef2792511d | |||
| 9a71624d2d | |||
| ffbdee7b17 | |||
| 77aec36da1 | |||
| 50efc055de | |||
| 69c172e8a7 | |||
| 82b665075b | |||
| 8e0f1c253b | |||
| 8ff796f133 | |||
| 8100c68b50 | |||
| 86387e90b2 | |||
| c10d6767c9 | |||
| 765ae641d7 | |||
| c066339d80 | |||
| 1542195217 | |||
| af1f9e6269 | |||
| 2961956c74 | |||
| 3f39a329c3 | |||
| dc8753f82a | |||
| 4502ee568e | |||
| 55c54d14b8 | |||
| 2da5a4b034 | |||
| 216ba8f05f | |||
| df6aecdbc5 | |||
| 1ce281b7ab | |||
| 5341a5e442 | |||
| ebece6efd7 | |||
| 1ead9e3d7f | |||
| be3b703a7b | |||
| f7da00f35e | |||
| 5c5c709fee | |||
| 2386fcec75 | |||
| ba790579ea | |||
| 04284921ac | |||
| b639de07ac | |||
| 26ff3ed3d3 | |||
| 9f69463f1d | |||
| 10765ff2a9 | |||
| be53e17e19 | |||
| dc4449df0d | |||
| 62a2f0e617 | |||
| df35e842f5 | |||
| 1be9fd55a9 | |||
| 755ebdb94e | |||
| c2e39bb556 | |||
| 733562e92a | |||
| 8a7cc0c0ff | |||
| 19db518e0a | |||
| 15537a41d2 | |||
| 790fb5b86f | |||
| 517ee736b3 | |||
| c77100a40c | |||
| 03afae5b95 | |||
| 05ea5073be | |||
| 5f5ea53afb | |||
| 1120d0704e | |||
| 6568a82455 | |||
| c2eacd72af | |||
| c5ef753d28 | |||
| 530535c6ea | |||
| 023620b57e | |||
| 902e67eba9 | |||
| 830aaf2059 | |||
| a0f36c9df7 | |||
| 1216c55094 | |||
| ec25388937 | |||
| c7134cbb01 | |||
| ba4d0ea8bf | |||
| 1cc53890dc | |||
| ae2ea3c723 | |||
| 3f31551fad | |||
| 13c27598d3 | |||
| 2564a4f172 | |||
| fd9bd448a2 | |||
| b72b50006b | |||
| ac1c048e64 | |||
| 8615731d46 | |||
| a7444bf944 | |||
| 69aaa22793 | |||
| 322e2b9dd5 | |||
| afd6f3b064 | |||
| 1e86be2bc5 | |||
| a57230b80b | |||
| 6bb1a76e58 | |||
| bdcb6be649 | |||
| 2f36ffe35d | |||
| 31c409a865 | |||
| d0ceaef6aa | |||
| 3ef72fe8f6 | |||
| 51070b6f59 | |||
| 18b4cb6819 | |||
| 0561bac189 | |||
| f4019cab3e | |||
| f8b4dd70ac | |||
| 611cc73d84 | |||
| 7cde8329bc | |||
| be15db5861 | |||
| 55f984126c | |||
| 2a6be0a03b | |||
| 2a36236132 | |||
| 105b398c2a | |||
| 9fa27bd41c | |||
| 6bb772215b | |||
| 03521003fa | |||
| 754bb7e390 | |||
| 4371298fff | |||
| 3f2e0e89ec | |||
| 4dbeeddc77 | |||
| 4161695885 | |||
| 7bbfe425f6 | |||
| f2a1d547f1 | |||
| b1ce938ebe | |||
| 03bba464d4 | |||
| 63a883a4f7 | |||
| b3bad6b581 | |||
| ca10088bdf | |||
| dd3f915956 | |||
| c11c41bd86 | |||
| fa117558ee | |||
| 17b47301ba | |||
| 3d31e4bcec | |||
| bb72277407 | |||
| 32c3637da0 | |||
| 9d466cd849 | |||
| e3bd6e0947 | |||
| dc13c6bb3d | |||
| 3a31c4ea80 | |||
| e00890c672 | |||
| 3a4c253969 | |||
| 34a83b27c3 | |||
| ff3ef6e3e6 | |||
| e2ec62d073 | |||
| bcf1103a8c | |||
| 9cb6fb6529 | |||
| 1b937c860b | |||
| ad0bb6d9a3 | |||
| 487d584402 | |||
| 57fa34f235 | |||
| 0af9dd61d2 | |||
| 8ecd4c21bf | |||
| 361a71ffad | |||
| fa8f17014c | |||
| 813e6f3cec | |||
| ba72a6ff2b | |||
| b6a1f95858 | |||
| f357e70779 | |||
| a9cac91d60 | |||
| 35fe5b7212 | |||
| 62cb3f77bd | |||
| e0b9e68a68 | |||
| 1e4c153aed | |||
| c9dc1a1b3d | |||
| 40282c1d4d | |||
| e11dbd0206 | |||
| 7b11ef5a5e | |||
| 2859603cbb | |||
| c313504c82 | |||
| e49772a8b4 | |||
| 08faeb3893 | |||
| 5e75e5e990 | |||
| 5cc5b622f0 | |||
| 11e8110f9e | |||
| 82dee24302 | |||
| ad192a59b5 | |||
| 71d08e94d5 | |||
| b155b15694 | |||
| d06bf487a5 | |||
| ccccd75328 | |||
| 03d65f36d3 | |||
| f4a89683da | |||
| f715478a13 | |||
| e0e9bd7d5f | |||
| 5ccb1e88e1 | |||
| be469f328c | |||
| fc80874f45 | |||
| 3d26b12251 | |||
| 1b77242d40 | |||
| 4c5960dc4b | |||
| e2505d434a | |||
| a466a357b6 | |||
| 6386e67949 | |||
| 16af134053 | |||
| d545d8fba5 | |||
| da4730e4a4 | |||
| 18fc3eea7c | |||
| 101ff6d1a2 | |||
| 20ad497bc8 | |||
| 1b18a50418 | |||
| 0a6397272c | |||
| 3477d28a87 | |||
| fb772e8a02 | |||
| 4e29e04f24 | |||
| fbcd4faa61 | |||
| da3ba63872 | |||
| e2a2bc301d | |||
| 72824fe65d | |||
| 64b4f7df4c | |||
| 7774461bbf | |||
| 8b65fa2ef6 | |||
| 0033bb3559 | |||
| ad905f8e6c | |||
| 9b0dd36cda | |||
| 4e81a0b73f | |||
| 32320409b1 | |||
| 040b856140 | |||
| 65c35977bd | |||
| 57075d8cdc | |||
| e14acea581 | |||
| 7af274a921 | |||
| a97fec8af5 | |||
| 2cba7e227d | |||
| 4d68d30b48 | |||
| 5a0e07efcf | |||
| cba6b5f2f7 | |||
| 1c965451b5 | |||
| a0f8dccbfe | |||
| fb8344f3c2 | |||
| 3fd77ae980 | |||
| 976c185172 | |||
| 4643e65ad6 | |||
| e23586af26 | |||
| 92d29ba4a3 | |||
| 886fd98f52 | |||
| 722e717e99 | |||
| 6d635007ab | |||
| 0e3474509a | |||
| d8bf79225f | |||
| 8a55ba75b5 | |||
| 2449581543 | |||
| 3e25e80f7a | |||
| f06e9491e1 | |||
| d0c2b41adf | |||
| c7af47f9ed | |||
| 6c035e1fd7 | |||
| ab7cbc808f | |||
| 62387f39d4 | |||
| 864889ab9f | |||
| cc4b456129 | |||
| 14b37df391 | |||
| 91df883231 | |||
| 6fea419feb | |||
| beeabe2e4b | |||
| 502310f4df | |||
| 37fe5c83dc | |||
| dab2637217 | |||
| 39fb919a05 | |||
| 999be62662 | |||
| 521906f5fa | |||
| 069165eb5b | |||
| df09c3ec61 | |||
| 6d3ae4d63d | |||
| 284187ac55 | |||
| 7e0353cca9 | |||
| 959c8bd498 | |||
| ca3df21b7f | |||
| 8571a922db | |||
| d92d1e654b | |||
| 46a14e685a | |||
| 1f94d8a3c8 | |||
| c78986831c | |||
| d470434746 | |||
| 561afb401a | |||
| 2b8a5969e9 | |||
| 36b205370d | |||
| 6afef2f881 | |||
| 0d927ba03f | |||
| 9fdd4824d3 | |||
| d041eee55c | |||
| d62e50075a | |||
| 5d953482ab | |||
| 3a90639cb5 | |||
| b9f3b382fc | |||
| 550d1ffd24 | |||
| 941e71dbbb | |||
| d90532a595 | |||
| 0bff28de09 | |||
| 38e10eb140 | |||
| fe82a1b6ca | |||
| 2ea728f731 | |||
| 284214c78d | |||
| b05a0d1fbe | |||
| be25c905fa | |||
| 5347b5e009 | |||
| b467518dae | |||
| 1ec6bdc8c6 | |||
| 265472ef72 | |||
| 325529761b | |||
| 741244b69d | |||
| 4abddbbfdd | |||
| 5aa5cb3523 | |||
| 9113255114 | |||
| 23cc677108 | |||
| 28bcc0e4f4 | |||
| 8bf5062b89 | |||
| 20750c332e | |||
| db581e47a3 | |||
| a54a693af2 | |||
| d468201952 | |||
| 31c5c4e924 | |||
| 8d65dbdd04 | |||
| b7f9363393 | |||
| 69f2cbaa3a | |||
| dc898ca2c3 | |||
| 03bff6500a | |||
| b379d71d19 | |||
| def7220bfc | |||
| f6c5afe5a9 | |||
| 847771fe06 | |||
| b8bd88901a | |||
| f1adabcddc | |||
| 24470e52a5 | |||
| 23bbc5cd14 | |||
| ebf6fc07a1 | |||
| 04513d9692 | |||
| 146720e197 | |||
| b1f8cc04af | |||
| 082fe93e16 | |||
| 709c195905 | |||
| 3355b78d91 | |||
| b84c63d034 | |||
| 057b5a9dee | |||
| 61aefc0759 | |||
| f929f8f100 |
@@ -10,6 +10,7 @@ kitlangton
|
||||
kommander
|
||||
ludvigrask
|
||||
MrMushrooooom
|
||||
neriousy
|
||||
nexxeln
|
||||
R44VC0RP
|
||||
rekram1-node
|
||||
@@ -19,3 +20,4 @@ Slickstef11
|
||||
usrnk1
|
||||
vimtor
|
||||
StarpTech
|
||||
vaprdev
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
name: beta
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "0 * * * *"
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Bun
|
||||
uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: Setup Git Committer
|
||||
id: setup-git-committer
|
||||
uses: ./.github/actions/setup-git-committer
|
||||
with:
|
||||
opencode-app-id: ${{ vars.OPENCODE_APP_ID }}
|
||||
opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }}
|
||||
|
||||
- name: Install OpenCode
|
||||
run: bun i -g opencode-ai
|
||||
|
||||
- name: Sync beta branch
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.setup-git-committer.outputs.token }}
|
||||
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||
run: bun script/beta.ts
|
||||
@@ -35,6 +35,7 @@ jobs:
|
||||
|
||||
- run: bun sst deploy --stage=${{ github.ref_name }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
PLANETSCALE_SERVICE_TOKEN_NAME: ${{ secrets.PLANETSCALE_SERVICE_TOKEN_NAME }}
|
||||
PLANETSCALE_SERVICE_TOKEN: ${{ secrets.PLANETSCALE_SERVICE_TOKEN }}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
name: models-snapshot
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "0 12 * * *"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
refresh:
|
||||
if: github.repository == 'anomalyco/opencode'
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: v2
|
||||
|
||||
# The refresh script only needs `fetch`; skip the monorepo install.
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
|
||||
with:
|
||||
bun-version-file: package.json
|
||||
|
||||
- name: Setup git committer
|
||||
uses: ./.github/actions/setup-git-committer
|
||||
with:
|
||||
opencode-app-id: ${{ vars.OPENCODE_APP_ID }}
|
||||
opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }}
|
||||
|
||||
- name: Refresh snapshot
|
||||
run: bun packages/core/script/update-models-snapshot.ts
|
||||
|
||||
- name: Commit changes
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
FILE="packages/core/src/models-dev/snapshot.txt"
|
||||
|
||||
if [ -z "$(git status --short -- "$FILE")" ]; then
|
||||
echo "No changes to commit"
|
||||
echo "### models.dev snapshot" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "Status: no changes" >> "$GITHUB_STEP_SUMMARY"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
git add "$FILE"
|
||||
git commit -m "chore(core): refresh bundled models.dev snapshot"
|
||||
|
||||
git pull --rebase --autostash origin v2
|
||||
git push origin HEAD:v2 --no-verify
|
||||
|
||||
echo "### models.dev snapshot" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "Status: committed $(git rev-parse --short HEAD)" >> "$GITHUB_STEP_SUMMARY"
|
||||
@@ -322,6 +322,7 @@ jobs:
|
||||
working-directory: packages/desktop
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
|
||||
OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_ORG: ${{ vars.SENTRY_ORG }}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
name: unlock
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
stage:
|
||||
description: SST stage to unlock
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- dev
|
||||
- production
|
||||
|
||||
concurrency: deploy-${{ inputs.stage }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
unlock:
|
||||
runs-on: ubuntu-latest
|
||||
environment: ${{ inputs.stage }}
|
||||
steps:
|
||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
||||
|
||||
- uses: ./.github/actions/setup-bun
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version: "24"
|
||||
|
||||
- uses: aws-actions/configure-aws-credentials@7474bc4690e29a8392af63c5b98e7449536d5c3a # v4.3.1
|
||||
with:
|
||||
role-to-assume: ${{ vars.AWS_DEPLOY_ROLE_ARN }}
|
||||
role-session-name: opencode-${{ github.run_id }}
|
||||
aws-region: us-east-1
|
||||
|
||||
- run: bun sst unlock --stage=${{ inputs.stage }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
PLANETSCALE_SERVICE_TOKEN_NAME: ${{ secrets.PLANETSCALE_SERVICE_TOKEN_NAME }}
|
||||
PLANETSCALE_SERVICE_TOKEN: ${{ secrets.PLANETSCALE_SERVICE_TOKEN }}
|
||||
STRIPE_SECRET_KEY: ${{ inputs.stage == 'production' && secrets.STRIPE_SECRET_KEY_PROD || secrets.STRIPE_SECRET_KEY_DEV }}
|
||||
HONEYCOMB_API_KEY: ${{ secrets.HONEYCOMB_API_KEY }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_ORG: ${{ vars.SENTRY_ORG }}
|
||||
SENTRY_PROJECT: ${{ vars.WEB_SENTRY_PROJECT }}
|
||||
SENTRY_RELEASE: unlock@${{ github.sha }}
|
||||
VITE_SENTRY_DSN: ${{ vars.WEB_SENTRY_DSN }}
|
||||
VITE_SENTRY_RELEASE: unlock@${{ github.sha }}
|
||||
@@ -4,7 +4,7 @@ import { tool } from "@opencode-ai/plugin"
|
||||
const TEAM = {
|
||||
tui: ["kommander", "simonklee"],
|
||||
desktop_web: ["Hona", "Brendonovich"],
|
||||
core: ["jlongster", "rekram1-node", "nexxeln", "kitlangton"],
|
||||
core: ["jlongster", "rekram1-node", "neriousy", "nexxeln", "kitlangton"],
|
||||
inference: ["fwang", "MrMushrooooom", "starptech"],
|
||||
windows: ["Hona"],
|
||||
} as const
|
||||
|
||||
+1
-1
@@ -157,7 +157,7 @@ Caveats:
|
||||
- If `spawn` does not work for you, you can debug the server separately:
|
||||
- Debug server: `bun run --inspect=ws://localhost:6499/ --cwd packages/opencode ./src/index.ts serve --port 4096`,
|
||||
then attach TUI with `opencode attach http://localhost:4096`
|
||||
- Debug TUI: `bun run --inspect=ws://localhost:6499/ --cwd packages/opencode --conditions=browser ./src/index.ts`
|
||||
- Debug TUI: `bun run --inspect=ws://localhost:6499/ --cwd packages/opencode ./src/index.ts`
|
||||
|
||||
Other tips and tricks:
|
||||
|
||||
|
||||
+16
-2
@@ -150,8 +150,9 @@ const zenLitePrice = new stripe.Price("ZenLitePrice", {
|
||||
})
|
||||
const ZEN_LITE_PRICE = new sst.Linkable("ZEN_LITE_PRICE", {
|
||||
properties: {
|
||||
product: zenLiteProduct.id,
|
||||
price: zenLitePrice.id,
|
||||
// Use existing Go resources in dev's checkout Stripe account.
|
||||
product: $app.stage === "dev" ? "prod_U1tUscpmwtV2bG" : zenLiteProduct.id,
|
||||
price: $app.stage === "dev" ? "price_1T3phhE7fOCwHSD4zS6w2NPy" : zenLitePrice.id,
|
||||
priceInr: 92900,
|
||||
firstMonth50Coupon: zenLiteCouponFirstMonth50.id,
|
||||
firstMonth100Coupon: zenLiteCouponFirstMonth100.id,
|
||||
@@ -221,6 +222,15 @@ const STRIPE_PUBLISHABLE_KEY = new sst.Secret("STRIPE_PUBLISHABLE_KEY")
|
||||
const AUTH_API_URL = new sst.Linkable("AUTH_API_URL", {
|
||||
properties: { value: auth.url.apply((url) => url!) },
|
||||
})
|
||||
// Preview branches have independent databases; do not send their workspaces to shared dev.
|
||||
const migrationDomain =
|
||||
$app.stage === "production" ? "opencode.ai" : $app.stage === "dev" ? "dev.opencode.ai" : undefined
|
||||
const consoleMigration = new sst.Linkable("ConsoleMigration", {
|
||||
properties: {
|
||||
consoleUrl: migrationDomain ? `https://${migrationDomain}/console` : "",
|
||||
inferenceUrl: migrationDomain ? `https://${migrationDomain}/inference` : "",
|
||||
},
|
||||
})
|
||||
const STRIPE_WEBHOOK_SECRET = new sst.Linkable("STRIPE_WEBHOOK_SECRET", {
|
||||
properties: { value: stripeWebhook.secret },
|
||||
})
|
||||
@@ -235,6 +245,7 @@ const bucketNew = new sst.cloudflare.Bucket("ZenDataNew")
|
||||
const DISCORD_INCIDENT_WEBHOOK_URL = new sst.Secret("DISCORD_INCIDENT_WEBHOOK_URL")
|
||||
const AWS_SES_ACCESS_KEY_ID = new sst.Secret("AWS_SES_ACCESS_KEY_ID")
|
||||
const AWS_SES_SECRET_ACCESS_KEY = new sst.Secret("AWS_SES_SECRET_ACCESS_KEY")
|
||||
const ENTERPRISE_SALES_INBOX_EMAIL = new sst.Secret("ENTERPRISE_SALES_INBOX_EMAIL")
|
||||
|
||||
const SALESFORCE_CLIENT_ID = new sst.Secret("SALESFORCE_CLIENT_ID")
|
||||
const SALESFORCE_CLIENT_SECRET = new sst.Secret("SALESFORCE_CLIENT_SECRET")
|
||||
@@ -255,6 +266,7 @@ new sst.cloudflare.x.SolidStart("Console", {
|
||||
SECRET.UpstashRedisRestUrl,
|
||||
SECRET.UpstashRedisRestToken,
|
||||
AUTH_API_URL,
|
||||
consoleMigration,
|
||||
STRIPE_WEBHOOK_SECRET,
|
||||
SECRET.SupportApiKey,
|
||||
DISCORD_INCIDENT_WEBHOOK_URL,
|
||||
@@ -263,6 +275,7 @@ new sst.cloudflare.x.SolidStart("Console", {
|
||||
EMAILOCTOPUS_API_KEY,
|
||||
AWS_SES_ACCESS_KEY_ID,
|
||||
AWS_SES_SECRET_ACCESS_KEY,
|
||||
ENTERPRISE_SALES_INBOX_EMAIL,
|
||||
SALESFORCE_CLIENT_ID,
|
||||
SALESFORCE_CLIENT_SECRET,
|
||||
SALESFORCE_INSTANCE_URL,
|
||||
@@ -270,6 +283,7 @@ new sst.cloudflare.x.SolidStart("Console", {
|
||||
ZEN_LITE_PRICE,
|
||||
new sst.Secret("ZEN_LIMITS"),
|
||||
new sst.Secret("ZEN_SESSION_SECRET"),
|
||||
new sst.Secret("CLOUDFLARE_ACCESS_CLIENT_ID"),
|
||||
...ZEN_MODELS,
|
||||
...($dev
|
||||
? [
|
||||
|
||||
+13
-1
@@ -181,6 +181,16 @@ const statsSyncConfig = new sst.Linkable("StatsSyncConfig", {
|
||||
},
|
||||
})
|
||||
|
||||
const r2SqlAuthToken = new sst.Secret("R2SqlAuthToken")
|
||||
const r2Sql = new sst.Linkable("R2Sql", {
|
||||
properties: {
|
||||
accountId: "15d29c8639fd3733b1b5486a2acfd968",
|
||||
bucket: `platform-${$app.stage}-lake`,
|
||||
namespace: "inference",
|
||||
table: "generation",
|
||||
},
|
||||
})
|
||||
|
||||
export const statSync = new sst.aws.Service("StatsSyncService", {
|
||||
cluster: lakeCluster,
|
||||
architecture: "arm64",
|
||||
@@ -193,7 +203,9 @@ export const statSync = new sst.aws.Service("StatsSyncService", {
|
||||
dockerfile: "packages/stats/server/Dockerfile",
|
||||
},
|
||||
command: ["bun", "src/stat-sync.ts"],
|
||||
link: [database, inferenceEvent, statsSyncConfig],
|
||||
// Keep the legacy Athena link and IAM permissions during the first R2-backed
|
||||
// release so reverting the application code remains a one-deploy rollback.
|
||||
link: [database, inferenceEvent, r2Sql, r2SqlAuthToken, statsSyncConfig],
|
||||
permissions: lakeQueryPermissions,
|
||||
scaling: {
|
||||
min: 1,
|
||||
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-GRjnvvyj37H36RqiCB7dz5ALAEwvw16izwuk1wsHEpU=",
|
||||
"aarch64-linux": "sha256-0OIn1o6dpqIQ5XgIMzpenMCMqsYzraaYVJk+te5eINU=",
|
||||
"aarch64-darwin": "sha256-sQSQcuox78d8wT1lsKYHqVBl11NusiszQ+gu2XYXZi8=",
|
||||
"x86_64-darwin": "sha256-SINkhMRd4oM1zABSs1Uf3OAzxJ1lgcycl1U4fmi+baY="
|
||||
"x86_64-linux": "sha256-5u7uXnkg2bDPgkYubLCE4tNMwO9jGjmA2Gh1Qtaq/Us=",
|
||||
"aarch64-linux": "sha256-XNjoxsIwxR1Kh9s3sJYgvSgiz4zJHmgKj2OLUf4LBKU=",
|
||||
"aarch64-darwin": "sha256-6BVFUUe4CUGa9baXHIaHodWmxFAUdiBPeXDAk20fDt8=",
|
||||
"x86_64-darwin": "sha256-QdbSHVtAHZPcSLDiwCRh6VfnxoD4d/wIh9coVBM9HOY="
|
||||
}
|
||||
}
|
||||
|
||||
+8
-3
@@ -6,7 +6,7 @@
|
||||
"type": "module",
|
||||
"packageManager": "bun@1.3.14",
|
||||
"scripts": {
|
||||
"dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",
|
||||
"dev": "bun run --cwd packages/opencode src/index.ts",
|
||||
"dev:desktop": "bun --cwd packages/desktop dev",
|
||||
"dev:web": "bun --cwd packages/app dev",
|
||||
"dev:console": "ulimit -n 10240 2>/dev/null; bun run --cwd packages/console/app dev",
|
||||
@@ -137,6 +137,7 @@
|
||||
"electron"
|
||||
],
|
||||
"overrides": {
|
||||
"@ai-sdk/anthropic": "3.0.111",
|
||||
"@opentui/core": "catalog:",
|
||||
"@opentui/keymap": "catalog:",
|
||||
"@opentui/solid": "catalog:",
|
||||
@@ -145,7 +146,6 @@
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"@dnd-kit/dom@0.5.0": "patches/@dnd-kit%2Fdom@0.5.0.patch",
|
||||
"@ff-labs/fff-bun@0.9.3": "patches/@ff-labs%2Ffff-bun@0.9.3.patch",
|
||||
"@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch",
|
||||
"@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch",
|
||||
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
|
||||
@@ -158,6 +158,11 @@
|
||||
"@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch",
|
||||
"@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch",
|
||||
"effect@4.0.0-beta.83": "patches/effect@4.0.0-beta.83.patch",
|
||||
"@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch"
|
||||
"@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch",
|
||||
"@ai-sdk/openai-compatible@2.0.41": "patches/@ai-sdk%2Fopenai-compatible@2.0.41.patch",
|
||||
"@ai-sdk/groq@3.0.31": "patches/@ai-sdk%2Fgroq@3.0.31.patch",
|
||||
"@ai-sdk/anthropic@3.0.111": "patches/@ai-sdk%2Fanthropic@3.0.111.patch",
|
||||
"@ai-sdk/amazon-bedrock@4.0.166": "patches/@ai-sdk%2Famazon-bedrock@4.0.166.patch",
|
||||
"@ai-sdk/openai@3.0.88": "patches/@ai-sdk%2Fopenai@3.0.88.patch"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
- `opencode dev web` proxies `https://app.opencode.ai`, so local UI/CSS changes will not show there.
|
||||
- For local UI changes, run the backend and app dev servers separately.
|
||||
- Backend (from `packages/opencode`): `bun run --conditions=browser ./src/index.ts serve --port 4096`
|
||||
- Backend (from `packages/opencode`): `bun run ./src/index.ts serve --port 4096`
|
||||
- App (from `packages/app`): `bun dev -- --port 4444`
|
||||
- Open `http://localhost:4444` to verify UI changes (it targets the backend at `http://localhost:4096`).
|
||||
|
||||
@@ -24,7 +24,14 @@
|
||||
- NEVER hardcode user-visible English strings in production code. ALWAYS use an i18n key for visible copy, placeholders, accessible labels, tooltips, menus, dialogs, toasts, empty states, and displayed errors.
|
||||
- When migrating existing copy to i18n, preserve the English text byte-for-byte unless the task explicitly requests a copy change.
|
||||
- NEVER change existing English text or English keys to facilitate translation. English is intentional, designer-written source copy; adapt locale-specific translations and i18n mechanics around it.
|
||||
- Keep locale complexity behind the shared typed i18n APIs. Feature and component code should use `language.t(...)` for ordinary copy and `language.plural(baseKey, count, params)` for count-sensitive copy. It must not inspect the locale, call `Intl.PluralRules`, construct or select plural-category keys such as `.one` or `.other`, or branch on locale-specific grammar.
|
||||
- Prefer complete translated phrases. Do not concatenate grammatical fragments or make call sites assemble sentences. Keep placeholders to irreducible dynamic values such as names, paths, and counts.
|
||||
- If a translation cannot be expressed by the current API, deepen the shared language/UI i18n module so one typed call owns locale selection, plural resolution, fallback, and interpolation. Do not leak that machinery into product code.
|
||||
- Do not translate from model knowledge alone. Verify terminology and grammar with Unicode CLDR locale/plural data, Microsoft Localization Style Guides and terminology, Apple localization/style guidance and localized platform UI, Mozilla localization style guides, Mozilla Pontoon, and the Firefox localization corpus at `github.com/mozilla-l10n/firefox-l10n`.
|
||||
- For developer-facing terminology, prefer the words already used by the target language's developer community over literal dictionary translations. Cross-check maintained localized developer products such as Firefox, KDE, and VS Code; use at least two independent corpora when they are available. If established practice keeps an English loanword or acronym, keep it rather than inventing a translation.
|
||||
- Translate complete UI phrases in context. A glossary hit is evidence, not permission to translate word-by-word. Check terse labels such as session, prompt, agent, model, fork, shell, terminal, workspace, and worktree in the same grammatical role before choosing a term.
|
||||
- Before a locale is ready, audit recurring concepts for one consistent translation and review every value that still equals English. Classify retained English as a product name, provider/tool name, URL, code token, keyboard legend, acronym, asset name, or established borrowing; translate unexplained leftovers.
|
||||
- In translation review notes, name the corpora used and call out uncertain or region-specific terminology so native speakers can focus review where it matters.
|
||||
- Also use the relevant language authority or official dictionary for the locale (for example RAE/Fundéu, FranceTerme, Duden, TDK, Kotus/Kielitoimiston sanakirja, Språkrådet/Bokmålsordboka, Rada Języka Polskiego/PWN, the Russian and Arabic language academies, the Ukrainian Orthography, Taiwan MOE dictionaries, or the Royal Society of Thailand). Treat the English dictionary as the semantic source of truth and preserve placeholders, code identifiers, product names, and keyboard labels.
|
||||
|
||||
## Tool Calling
|
||||
|
||||
@@ -60,6 +60,39 @@ test("keeps the file-browser sidebar mounted when switching file tabs", async ({
|
||||
await expect.poll(() => viewport.evaluate((element) => element.scrollTop)).toBe(scrolled)
|
||||
})
|
||||
|
||||
test("keeps previous file search results visible while the next search loads", async ({ page }) => {
|
||||
const searchPending = Promise.withResolvers<void>()
|
||||
await setup(page, async ({ query }) => {
|
||||
if (query === "file-0") return ["file-00.ts"]
|
||||
if (query === "file-7") {
|
||||
await searchPending.promise
|
||||
return ["file-79.ts"]
|
||||
}
|
||||
return []
|
||||
})
|
||||
|
||||
await page.goto(`/server/${base64Encode(server)}/session/${sessionID}`)
|
||||
await expectSessionTitle(page, title)
|
||||
|
||||
const panel = page.locator("#review-panel")
|
||||
await panel.getByRole("button", { name: "Open file" }).click()
|
||||
const filter = panel.getByRole("combobox", { name: "Filter files" })
|
||||
await filter.fill("file-0")
|
||||
await expect(panel.getByRole("option", { name: "file-00.ts" })).toBeVisible()
|
||||
|
||||
const nextSearch = page.waitForRequest((request) => {
|
||||
const url = new URL(request.url())
|
||||
return url.pathname === "/find/file" && url.searchParams.get("query") === "file-7"
|
||||
})
|
||||
await filter.fill("file-7")
|
||||
await nextSearch
|
||||
await expect(panel.getByRole("option", { name: "file-00.ts" })).toBeVisible()
|
||||
|
||||
searchPending.resolve()
|
||||
await expect(panel.getByRole("option", { name: "file-79.ts" })).toBeVisible()
|
||||
await expect(panel.getByRole("option", { name: "file-00.ts" })).toBeHidden()
|
||||
})
|
||||
|
||||
type Probed = HTMLElement & { __e2eProbe?: string }
|
||||
|
||||
async function writeProbe(page: Page) {
|
||||
@@ -74,7 +107,10 @@ async function readProbe(page: Page) {
|
||||
.evaluate((el) => (el as Probed).__e2eProbe)
|
||||
}
|
||||
|
||||
async function setup(page: Page) {
|
||||
async function setup(
|
||||
page: Page,
|
||||
findFiles?: (input: { query: string; dirs?: string; limit?: number }) => unknown | Promise<unknown>,
|
||||
) {
|
||||
await mockOpenCodeServer(page, {
|
||||
directory,
|
||||
project: {
|
||||
@@ -119,6 +155,7 @@ async function setup(page: Page) {
|
||||
}))
|
||||
},
|
||||
fileContent: (path) => ({ type: "text", content: `contents:${path}` }),
|
||||
findFiles,
|
||||
pageMessages: () => ({ items: [] }),
|
||||
})
|
||||
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
import { expect, test } from "@playwright/test"
|
||||
import { base64Encode } from "@opencode-ai/core/util/encode"
|
||||
import { mockOpenCodeServer } from "../utils/mock-server"
|
||||
|
||||
const draftID = "draft_legacy_new_session"
|
||||
const directory = "C:/OpenCode/LegacyNewSession"
|
||||
const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`
|
||||
|
||||
test("redirects a draft to the legacy new-session route", async ({ page }) => {
|
||||
await mockOpenCodeServer(page, {
|
||||
directory,
|
||||
project: {
|
||||
id: "proj_legacy_new_session",
|
||||
worktree: directory,
|
||||
vcs: "git",
|
||||
name: "legacy-new-session",
|
||||
time: { created: 1700000000000, updated: 1700000000000 },
|
||||
sandboxes: [],
|
||||
},
|
||||
provider: { all: [], connected: [], default: {} },
|
||||
sessions: [],
|
||||
pageMessages: () => ({ items: [] }),
|
||||
})
|
||||
await page.addInitScript(
|
||||
({ directory, draftID, server }) => {
|
||||
localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: false } }))
|
||||
localStorage.setItem("app-version.v1", JSON.stringify({ version: "1.17.20" }))
|
||||
localStorage.setItem(
|
||||
"opencode.window.browser.dat:tabs",
|
||||
JSON.stringify([{ type: "draft", draftID, server, directory }]),
|
||||
)
|
||||
},
|
||||
{ directory, draftID, server },
|
||||
)
|
||||
|
||||
await page.goto(`/new-session?draftId=${draftID}`)
|
||||
|
||||
await expect(page).toHaveURL(`/${base64Encode(directory)}/session`)
|
||||
await expect(page.locator("header[data-tauri-drag-region]")).toBeVisible()
|
||||
await expect(page.locator('[data-component="prompt-input"]')).toBeVisible()
|
||||
})
|
||||
@@ -0,0 +1,141 @@
|
||||
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, {
|
||||
protocol: "v1",
|
||||
sessions,
|
||||
provider: fixture.provider,
|
||||
directory: fixture.directory,
|
||||
project: fixture.project,
|
||||
pageMessages,
|
||||
})
|
||||
await page.route(/\/session\/[^/]+(?:\?.*)?$/, async (route) => {
|
||||
if (route.request().method() !== "PATCH") return route.fallback()
|
||||
const id = new URL(route.request().url()).pathname.split("/").at(-1)
|
||||
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({ json: session, headers: { "access-control-allow-origin": "*" } })
|
||||
})
|
||||
await page.addInitScript((directory) => {
|
||||
localStorage.setItem(
|
||||
"opencode.global.dat:server",
|
||||
JSON.stringify({
|
||||
projects: { local: [{ worktree: directory, expanded: true }] },
|
||||
lastProject: { local: directory },
|
||||
}),
|
||||
)
|
||||
}, fixture.directory)
|
||||
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.getByRole("textbox", { name: "Prompt", exact: true }).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(/\/session\/[^/]+(?:\?.*)?$/, (route) => {
|
||||
if (route.request().method() !== "PATCH") return route.fallback()
|
||||
return 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()
|
||||
})
|
||||
@@ -128,7 +128,7 @@ test.describe("session timeline projection", () => {
|
||||
await expect(page.locator('[data-timeline-row="TurnGap"]')).toBeVisible()
|
||||
})
|
||||
|
||||
test("renders comment strips and historical diff summary overflow", async ({ page }) => {
|
||||
test("renders inline comments and historical diff summary overflow", async ({ page }) => {
|
||||
const user = userMessage(
|
||||
[
|
||||
userText("The user made the following comment regarding lines 4 through 8 of src/a.ts: Keep this stable", {
|
||||
@@ -152,14 +152,10 @@ test.describe("session timeline projection", () => {
|
||||
parentID: "msg_2000_diff_next_user",
|
||||
created: 1700000011000,
|
||||
})
|
||||
await setupTimeline(page, {
|
||||
messages: [user, assistantMessage(), nextUser, nextAssistant],
|
||||
settings: { newLayoutDesigns: false },
|
||||
})
|
||||
await setupTimeline(page, { messages: [user, assistantMessage(), nextUser, nextAssistant] })
|
||||
const scroller = page.locator(".scroll-view__viewport", { has: page.locator("[data-timeline-row]") })
|
||||
await scroller.evaluate((element) => (element.scrollTop = 0))
|
||||
|
||||
await expect(page.locator('[data-timeline-row="CommentStrip"]')).toBeVisible()
|
||||
await expect(page.getByText("Keep this stable", { exact: true })).toBeVisible()
|
||||
await expect(page.locator('[data-timeline-row="DiffSummary"]')).toBeVisible()
|
||||
await expect(page.getByText(/show all/i)).toBeVisible()
|
||||
|
||||
@@ -39,7 +39,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" })).toBeVisible()
|
||||
await expect(page.getByRole("button", { name: "Close Tab", exact: true })).toBeVisible()
|
||||
await expect(page.getByRole("heading", { name: taskDescription })).toHaveCount(0)
|
||||
})
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ export interface MockServerConfig {
|
||||
questions?: unknown[] | (() => unknown[])
|
||||
fileList?: (path: string) => unknown | Promise<unknown>
|
||||
fileContent?: (path: string) => unknown | Promise<unknown>
|
||||
findFiles?: (input: { query: string; dirs?: string; limit?: number }) => unknown
|
||||
findFiles?: (input: { query: string; dirs?: string; limit?: number }) => unknown | Promise<unknown>
|
||||
sessionStatus?: Record<string, unknown> | (() => Record<string, unknown>)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.18.13",
|
||||
"version": "1.18.31",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
@@ -32,7 +32,7 @@
|
||||
},
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@happy-dom/global-registrator": "20.0.11",
|
||||
"@happy-dom/global-registrator": "20.12.0",
|
||||
"@playwright/test": "catalog:",
|
||||
"@sentry/vite-plugin": "catalog:",
|
||||
"@tailwindcss/vite": "catalog:",
|
||||
|
||||
@@ -33,6 +33,7 @@ import { ExternalLink } from "@/components/external-link"
|
||||
import { useServerSDK } from "@/context/server-sdk"
|
||||
import { useServerSync } from "@/context/server-sync"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
import { useSettings } from "@/context/settings"
|
||||
import { popularProviders, useProviders } from "@/hooks/use-providers"
|
||||
import { CustomProviderForm } from "./dialog-custom-provider"
|
||||
@@ -386,6 +387,7 @@ function ProviderConnection(props: {
|
||||
const serverSDK = useServerSDK()
|
||||
const params = useParams()
|
||||
const language = useLanguage()
|
||||
const platform = usePlatform()
|
||||
const settings = useSettings()
|
||||
const newLayout = settings.general.newLayoutDesigns
|
||||
const providers = useProviders(() => props.directory?.())
|
||||
@@ -560,6 +562,11 @@ function ProviderConnection(props: {
|
||||
})
|
||||
.then((x) => {
|
||||
if (!alive.value) return
|
||||
if (props.provider === "opencode" && platform.platform === "desktop") {
|
||||
const url = new URL(x.data.url)
|
||||
url.searchParams.set("client_id", "opencode-desktop")
|
||||
x.data.url = url.href
|
||||
}
|
||||
dispatch({ type: "auth.complete", authorization: x.data })
|
||||
})
|
||||
.catch((e) => {
|
||||
|
||||
@@ -450,7 +450,7 @@ function ModelSelectorPopoverV2View(props: {
|
||||
<For each={groups()}>
|
||||
{(group) => (
|
||||
<MenuV2.Group>
|
||||
<MenuV2.GroupLabel class="gap-2 px-3">
|
||||
<MenuV2.GroupLabel class="sticky top-0 z-10 gap-2 bg-v2-background-bg-layer-01 px-3">
|
||||
<span class="min-w-0 truncate">{group.items[0].provider.name}</span>
|
||||
</MenuV2.GroupLabel>
|
||||
<MenuV2.RadioGroup value={props.current()}>
|
||||
|
||||
@@ -145,7 +145,7 @@ function ServerForm(props: ServerFormProps) {
|
||||
type="text"
|
||||
label={language.t("dialog.server.add.name")}
|
||||
placeholder={language.t("dialog.server.add.namePlaceholder")}
|
||||
value={props.name}
|
||||
defaultValue={props.name}
|
||||
disabled={props.busy}
|
||||
onChange={props.onNameChange}
|
||||
onKeyDown={keyDown}
|
||||
@@ -155,7 +155,7 @@ function ServerForm(props: ServerFormProps) {
|
||||
type="text"
|
||||
label={language.t("dialog.server.add.username")}
|
||||
placeholder={language.t("dialog.server.add.usernamePlaceholder")}
|
||||
value={props.username}
|
||||
defaultValue={props.username}
|
||||
disabled={props.busy}
|
||||
onChange={props.onUsernameChange}
|
||||
onKeyDown={keyDown}
|
||||
@@ -164,7 +164,7 @@ function ServerForm(props: ServerFormProps) {
|
||||
type="password"
|
||||
label={language.t("dialog.server.add.password")}
|
||||
placeholder={language.t("dialog.server.add.passwordPlaceholder")}
|
||||
value={props.password}
|
||||
defaultValue={props.password}
|
||||
disabled={props.busy}
|
||||
onChange={props.onPasswordChange}
|
||||
onKeyDown={keyDown}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { usePlatform } from "@/context/platform"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
@@ -14,6 +15,7 @@ export type DialogGoUpsellProps = {
|
||||
|
||||
export function DialogUsageExceeded(props: DialogGoUpsellProps) {
|
||||
const dialog = useDialog()
|
||||
const language = useLanguage()
|
||||
const platform = usePlatform()
|
||||
|
||||
const runAction = () => {
|
||||
@@ -32,7 +34,7 @@ export function DialogUsageExceeded(props: DialogGoUpsellProps) {
|
||||
<div class="flex flex-col gap-4 pl-6 pr-2.5 pb-3">
|
||||
<div class="flex justify-end gap-2">
|
||||
<Button variant="ghost" size="large" onClick={dismiss}>
|
||||
Don't show again
|
||||
{language.t("dialog.usageExceeded.dontShowAgain")}
|
||||
</Button>
|
||||
<Button variant="primary" size="large" onClick={runAction}>
|
||||
{props.actionLabel}
|
||||
|
||||
@@ -139,6 +139,7 @@ test("resolves directory autocomplete from the current browser root", async () =
|
||||
directories.push(input.location?.directory ?? "")
|
||||
return Promise.resolve({ data: [] })
|
||||
},
|
||||
list: () => Promise.resolve({ data: [] }),
|
||||
},
|
||||
},
|
||||
} as unknown as Parameters<typeof createDirectorySearch>[0]["sdk"]
|
||||
@@ -152,6 +153,67 @@ test("resolves directory autocomplete from the current browser root", async () =
|
||||
expect(directories).toEqual(["/repo", "/repo/src"])
|
||||
})
|
||||
|
||||
test("keeps indexed directory results for servers that support empty search", async () => {
|
||||
const sdk = {
|
||||
api: {
|
||||
file: {
|
||||
find: () => Promise.resolve({ data: [{ path: "projects/", type: "directory" }] }),
|
||||
list: () => Promise.reject(new Error("listing should not run when search returns results")),
|
||||
},
|
||||
},
|
||||
} as unknown as Parameters<typeof createDirectorySearch>[0]["sdk"]
|
||||
const search = createDirectorySearch({ sdk, home: () => "/home/luke", base: () => "/home/luke" })
|
||||
|
||||
expect(await search("")).toEqual(["/home/luke/projects"])
|
||||
})
|
||||
|
||||
test("lists the default directory when empty search is unsupported", async () => {
|
||||
const calls: string[] = []
|
||||
const directories = Array.from({ length: 60 }, (_, index) => ({
|
||||
path: `project-${index}/`,
|
||||
type: "directory" as const,
|
||||
}))
|
||||
const sdk = {
|
||||
api: {
|
||||
file: {
|
||||
find: () => Promise.resolve({ data: [] }),
|
||||
list: (input: { location?: { directory?: string } }) => {
|
||||
calls.push(input.location?.directory ?? "")
|
||||
return Promise.resolve({
|
||||
data: [...directories, { path: "README.md", type: "file" }],
|
||||
})
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as Parameters<typeof createDirectorySearch>[0]["sdk"]
|
||||
const search = createDirectorySearch({ sdk, home: () => "/home/luke", base: () => "/home/luke" })
|
||||
|
||||
const results = await search("")
|
||||
expect(results).toHaveLength(60)
|
||||
expect(results.at(-1)).toBe("/home/luke/project-59")
|
||||
expect(calls).toEqual(["/home/luke"])
|
||||
})
|
||||
|
||||
test("matches the default directory listing when typed search is unsupported", async () => {
|
||||
const sdk = {
|
||||
api: {
|
||||
file: {
|
||||
find: () => Promise.resolve({ data: [] }),
|
||||
list: () =>
|
||||
Promise.resolve({
|
||||
data: [
|
||||
{ path: "Documents/", type: "directory" },
|
||||
{ path: "Downloads/", type: "directory" },
|
||||
],
|
||||
}),
|
||||
},
|
||||
},
|
||||
} as unknown as Parameters<typeof createDirectorySearch>[0]["sdk"]
|
||||
const search = createDirectorySearch({ sdk, home: () => "/home/luke", base: () => "/home/luke" })
|
||||
|
||||
expect(await search("documents")).toEqual(["/home/luke/Documents"])
|
||||
})
|
||||
|
||||
test("searches from an absolute root without a default base", async () => {
|
||||
const directories: string[] = []
|
||||
const sdk = {
|
||||
|
||||
@@ -379,7 +379,14 @@ export function createDirectorySearch(args: { sdk: ServerSDK; base: () => string
|
||||
.then((result) => result.data.map((entry) => entry.path))
|
||||
.catch(() => [])
|
||||
if (!active()) return []
|
||||
return results.map((path) => joinPickerPath(input.directory, path)).slice(0, 50)
|
||||
if (results.length) {
|
||||
return results.map((path) => joinPickerPath(input.directory, path)).slice(0, 50)
|
||||
}
|
||||
const fallback = query
|
||||
? await match(input.directory, query, 50)
|
||||
: (await directories(input.directory)).map((item) => item.absolute)
|
||||
if (!active()) return []
|
||||
return fallback
|
||||
}
|
||||
const segments = query.replace(/^\/+/, "").split("/")
|
||||
const head = segments.slice(0, -1).filter((part) => part && part !== ".")
|
||||
|
||||
@@ -31,7 +31,10 @@ export function OpenInAppV2(props: { directory: () => string }) {
|
||||
disabled={state.opening()}
|
||||
aria-label={language.t("session.header.open.ariaLabel", { app: state.current().label })}
|
||||
>
|
||||
<Show when={state.opening()} fallback={<AppIcon id={state.current().icon} class="size-[18px]" />}>
|
||||
<Show
|
||||
when={state.opening()}
|
||||
fallback={<AppIcon id={state.current().icon} style={{ width: "20px", height: "20px" }} />}
|
||||
>
|
||||
<Spinner class="size-3.5" />
|
||||
</Show>
|
||||
</SplitButtonV2Action>
|
||||
|
||||
@@ -5,12 +5,15 @@ import { checksum } from "@opencode-ai/core/util/encode"
|
||||
import { findLast } from "@opencode-ai/core/util/array"
|
||||
import { same } from "@/utils/same"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { Accordion } from "@opencode-ai/ui/accordion"
|
||||
import { StickyAccordionHeader } from "@opencode-ai/ui/sticky-accordion-header"
|
||||
import { File } from "@opencode-ai/session-ui/file"
|
||||
import { Markdown } from "@opencode-ai/session-ui/markdown"
|
||||
import { ScrollView } from "@opencode-ai/ui/scroll-view"
|
||||
import type { Message, Part, UserMessage } from "@opencode-ai/sdk/v2/client"
|
||||
import { showToast } from "@/utils/toast"
|
||||
import { downloadSessionExport, fetchSessionExport, sessionExportFilename } from "@/utils/session-export"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { useProviders } from "@/hooks/use-providers"
|
||||
import { useSDK } from "@/context/sdk"
|
||||
@@ -120,7 +123,8 @@ export function SessionContextTab() {
|
||||
() => {
|
||||
const revert = info()?.revert?.messageID
|
||||
if (!revert) return userMessages()
|
||||
return userMessages().filter((m) => m.id < revert)
|
||||
const boundary = userMessages().findIndex((message) => message.id === revert)
|
||||
return boundary < 0 ? userMessages() : userMessages().slice(0, boundary)
|
||||
},
|
||||
emptyUserMessages,
|
||||
{ equals: same },
|
||||
@@ -220,6 +224,31 @@ export function SessionContextTab() {
|
||||
{ label: "context.stats.lastActivity", value: () => formatter().time(ctx()?.message.time.created) },
|
||||
] satisfies { label: string; value: () => JSX.Element }[]
|
||||
|
||||
const exportSession = async () => {
|
||||
const sessionID = params.id
|
||||
if (!sessionID) return
|
||||
try {
|
||||
const data = await fetchSessionExport({
|
||||
sessionID,
|
||||
client: sdk().client,
|
||||
})
|
||||
const filename = sessionExportFilename(data.info)
|
||||
downloadSessionExport(filename, data)
|
||||
showToast({
|
||||
variant: "success",
|
||||
icon: "circle-check",
|
||||
title: language.t("toast.session.export.success.title"),
|
||||
description: language.t("toast.session.export.success.description", { filename }),
|
||||
})
|
||||
} catch (err) {
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: language.t("toast.session.export.failed.title"),
|
||||
description: err instanceof Error ? err.message : language.t("toast.session.export.failed.description"),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
let scroll: HTMLDivElement | undefined
|
||||
let frame: number | undefined
|
||||
let pending: { x: number; y: number } | undefined
|
||||
@@ -328,7 +357,18 @@ export function SessionContextTab() {
|
||||
</Show>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="text-12-regular text-text-weak">{language.t("context.rawMessages.title")}</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="text-12-regular text-text-weak">{language.t("context.rawMessages.title")}</div>
|
||||
<Button
|
||||
size="small"
|
||||
variant="ghost"
|
||||
class="gap-1.5 px-2 text-text-weak hover:text-text-base"
|
||||
onClick={exportSession}
|
||||
>
|
||||
<Icon name="download" size="small" />
|
||||
<span>{language.t("context.export.session")}</span>
|
||||
</Button>
|
||||
</div>
|
||||
<Accordion multiple>
|
||||
<For each={messages()}>
|
||||
{(message) => (
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { createEffect, createMemo, createSignal, onCleanup, Show, type Ref } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { makeEventListener } from "@solid-primitives/event-listener"
|
||||
import { createResizeObserver } from "@solid-primitives/resize-observer"
|
||||
import { createMutation } from "@tanstack/solid-query"
|
||||
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
|
||||
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
|
||||
import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2"
|
||||
import { useGlobal } from "@/context/global"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { ServerConnection, serverName } from "@/context/server"
|
||||
@@ -33,6 +35,8 @@ export function TabNavItem(props: {
|
||||
pressed?: boolean
|
||||
hidden?: boolean
|
||||
}) {
|
||||
const language = useLanguage()
|
||||
const [menu, setMenu] = createStore({ open: false, rename: false })
|
||||
const [editing, setEditing] = createSignal(false)
|
||||
const [titleOverflowing, setTitleOverflowing] = createSignal(false)
|
||||
let tabRoot!: HTMLDivElement
|
||||
@@ -76,7 +80,7 @@ export function TabNavItem(props: {
|
||||
})
|
||||
|
||||
const [popoverOpen, setPopoverOpen] = createSignal(false)
|
||||
const previewBlocked = () => !!props.dragging || editing() || !!props.pressed || !props.session()
|
||||
const previewBlocked = () => !!props.dragging || editing() || menu.open || !!props.pressed || !props.session()
|
||||
|
||||
const measureTitleOverflow = () => {
|
||||
if (!titleEl || editing()) {
|
||||
@@ -138,9 +142,9 @@ export function TabNavItem(props: {
|
||||
titleEl.textContent = value
|
||||
})
|
||||
|
||||
const openRename = (event: MouseEvent) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
const openRename = (event?: MouseEvent) => {
|
||||
event?.preventDefault()
|
||||
event?.stopPropagation()
|
||||
if (!canOpenTabRename(props.dragging, editing(), rename.isPending)) return
|
||||
const session = props.session()
|
||||
if (!session) return
|
||||
@@ -171,7 +175,7 @@ export function TabNavItem(props: {
|
||||
onCleanup(cleanup)
|
||||
})
|
||||
|
||||
const tab = (
|
||||
const tab = () => (
|
||||
<div
|
||||
ref={(el) => {
|
||||
tabRoot = el
|
||||
@@ -196,7 +200,11 @@ export function TabNavItem(props: {
|
||||
closeTab(event)
|
||||
}}
|
||||
>
|
||||
<a
|
||||
<MenuV2.Context.Trigger
|
||||
as="a"
|
||||
disabled={editing() || props.dragging}
|
||||
aria-haspopup="menu"
|
||||
aria-expanded={menu.open}
|
||||
data-slot="tab-link"
|
||||
data-titlebar-tab-link
|
||||
href={props.href}
|
||||
@@ -276,7 +284,7 @@ export function TabNavItem(props: {
|
||||
event.preventDefault()
|
||||
}}
|
||||
/>
|
||||
</a>
|
||||
</MenuV2.Context.Trigger>
|
||||
|
||||
<div data-slot="tab-close">
|
||||
<IconButtonV2
|
||||
@@ -289,26 +297,49 @@ export function TabNavItem(props: {
|
||||
}}
|
||||
onClick={closeTab}
|
||||
icon={<IconV2 name="xmark-small" />}
|
||||
aria-label={language.t("common.closeTab")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<TabPreviewPopover
|
||||
trigger={tab}
|
||||
open={popoverOpen() && !previewBlocked()}
|
||||
onOpenChange={(value) => {
|
||||
if (value && previewBlocked()) return
|
||||
setPopoverOpen(value)
|
||||
<MenuV2.Context
|
||||
onOpenChange={(open) => {
|
||||
setMenu("open", open)
|
||||
if (open) setPopoverOpen(false)
|
||||
}}
|
||||
data={{
|
||||
projectName: projectName(),
|
||||
title: props.session()?.title,
|
||||
path: previewPath(),
|
||||
serverName: serverLabel(),
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<TabPreviewPopover
|
||||
trigger={tab()}
|
||||
open={popoverOpen() && !previewBlocked()}
|
||||
onOpenChange={(value) => {
|
||||
if (value && previewBlocked()) return
|
||||
setPopoverOpen(value)
|
||||
}}
|
||||
data={{
|
||||
projectName: projectName(),
|
||||
title: props.session()?.title,
|
||||
path: previewPath(),
|
||||
serverName: serverLabel(),
|
||||
}}
|
||||
/>
|
||||
<MenuV2.Context.Portal>
|
||||
<MenuV2.Context.Content
|
||||
onCloseAutoFocus={(event) => {
|
||||
if (!menu.rename) return
|
||||
event.preventDefault()
|
||||
setMenu("rename", false)
|
||||
openRename()
|
||||
}}
|
||||
>
|
||||
<MenuV2.Item disabled={!props.session() || rename.isPending} onSelect={() => setMenu("rename", true)}>
|
||||
{language.t("common.rename")}
|
||||
</MenuV2.Item>
|
||||
<MenuV2.Item onSelect={props.onClose}>{language.t("common.closeTab")}</MenuV2.Item>
|
||||
</MenuV2.Context.Content>
|
||||
</MenuV2.Context.Portal>
|
||||
</MenuV2.Context>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -628,7 +628,7 @@ function TitlebarUpdateIconButton(props: { state: TitlebarUpdatePillState }) {
|
||||
aria-label={props.state.ariaLabel}
|
||||
>
|
||||
<span class="shrink-0 ml-[8px] mr-px text-[11px] text-v2-text-text-accent [font-weight:530] opacity-0 translate-x-2 motion-safe:transition-all duration-150 ease-out group-hover:opacity-100 group-hover:translate-x-0 group-focus-within:opacity-100 group-focus-within:translate-x-0 motion-reduce:translate-x-0">
|
||||
Update
|
||||
{props.state.label}
|
||||
</span>
|
||||
<span class="flex size-5 shrink-0 items-center justify-center">
|
||||
<Show
|
||||
|
||||
@@ -15,12 +15,12 @@ const rootSession = (input: { id: string; parentID?: string; archived?: number }
|
||||
},
|
||||
}) as Session
|
||||
|
||||
const userMessage = (id: string, sessionID: string) =>
|
||||
const userMessage = (id: string, sessionID: string, created = 1) =>
|
||||
({
|
||||
id,
|
||||
sessionID,
|
||||
role: "user",
|
||||
time: { created: 1 },
|
||||
time: { created },
|
||||
agent: "assistant",
|
||||
model: { providerID: "openai", modelID: "gpt" },
|
||||
}) as Message
|
||||
@@ -370,13 +370,13 @@ describe("applyDirectoryEvent", () => {
|
||||
const sessionID = "ses_1"
|
||||
const [store, setStore] = createStore(
|
||||
baseState({
|
||||
message: { [sessionID]: [userMessage("msg_1", sessionID), userMessage("msg_3", sessionID)] },
|
||||
part: { msg_2: [textPart("prt_1", sessionID, "msg_2")] },
|
||||
message: { [sessionID]: [userMessage("msg_z", sessionID, 1), userMessage("msg_b", sessionID, 3)] },
|
||||
part: { msg_a: [textPart("prt_1", sessionID, "msg_a")] },
|
||||
}),
|
||||
)
|
||||
|
||||
applyDirectoryEvent({
|
||||
event: { type: "message.updated", properties: { info: userMessage("msg_2", sessionID) } },
|
||||
event: { type: "message.updated", properties: { info: userMessage("msg_a", sessionID, 2) } },
|
||||
store,
|
||||
setStore,
|
||||
push() {},
|
||||
@@ -384,14 +384,14 @@ describe("applyDirectoryEvent", () => {
|
||||
loadLsp() {},
|
||||
})
|
||||
|
||||
expect(store.message[sessionID]?.map((x) => x.id)).toEqual(["msg_1", "msg_2", "msg_3"])
|
||||
expect(store.message[sessionID]?.map((x) => x.id)).toEqual(["msg_z", "msg_a", "msg_b"])
|
||||
|
||||
applyDirectoryEvent({
|
||||
event: {
|
||||
type: "message.updated",
|
||||
properties: {
|
||||
info: {
|
||||
...userMessage("msg_2", sessionID),
|
||||
...userMessage("msg_a", sessionID, 2),
|
||||
role: "assistant",
|
||||
} as Message,
|
||||
},
|
||||
@@ -403,10 +403,10 @@ describe("applyDirectoryEvent", () => {
|
||||
loadLsp() {},
|
||||
})
|
||||
|
||||
expect(store.message[sessionID]?.find((x) => x.id === "msg_2")?.role).toBe("assistant")
|
||||
expect(store.message[sessionID]?.find((x) => x.id === "msg_a")?.role).toBe("assistant")
|
||||
|
||||
applyDirectoryEvent({
|
||||
event: { type: "message.removed", properties: { sessionID, messageID: "msg_2" } },
|
||||
event: { type: "message.removed", properties: { sessionID, messageID: "msg_a" } },
|
||||
store,
|
||||
setStore,
|
||||
push() {},
|
||||
@@ -414,8 +414,8 @@ describe("applyDirectoryEvent", () => {
|
||||
loadLsp() {},
|
||||
})
|
||||
|
||||
expect(store.message[sessionID]?.map((x) => x.id)).toEqual(["msg_1", "msg_3"])
|
||||
expect(store.part.msg_2).toBeUndefined()
|
||||
expect(store.message[sessionID]?.map((x) => x.id)).toEqual(["msg_z", "msg_b"])
|
||||
expect(store.part.msg_a).toBeUndefined()
|
||||
})
|
||||
|
||||
test("upserts and prunes message parts", () => {
|
||||
|
||||
@@ -15,6 +15,7 @@ import type { State, VcsCache } from "./types"
|
||||
import { trimSessions } from "./session-trim"
|
||||
import { dropSessionCaches } from "./session-cache"
|
||||
import { diffs as list, message as clean } from "@/utils/diffs"
|
||||
import { messageKey } from "@/utils/session-message"
|
||||
|
||||
const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"])
|
||||
const SESSION_CONTENT_EVENTS = new Set([
|
||||
@@ -275,7 +276,7 @@ export function applyDirectoryEvent(input: {
|
||||
input.setStore("message", info.sessionID, [info])
|
||||
break
|
||||
}
|
||||
const result = Binary.search(messages, info.id, (m) => m.id)
|
||||
const result = Binary.search(messages, messageKey(info), messageKey)
|
||||
if (result.found) {
|
||||
input.setStore("message", info.sessionID, result.index, reconcile(info))
|
||||
break
|
||||
@@ -295,8 +296,8 @@ export function applyDirectoryEvent(input: {
|
||||
produce((draft) => {
|
||||
const messages = draft.message[props.sessionID]
|
||||
if (messages) {
|
||||
const result = Binary.search(messages, props.messageID, (m) => m.id)
|
||||
if (result.found) messages.splice(result.index, 1)
|
||||
const index = messages.findIndex((message) => message.id === props.messageID)
|
||||
if (index >= 0) messages.splice(index, 1)
|
||||
}
|
||||
const parts = draft.part[props.messageID]
|
||||
if (parts) {
|
||||
@@ -322,7 +323,7 @@ export function applyDirectoryEvent(input: {
|
||||
input.setStore("part", part.messageID, [part])
|
||||
break
|
||||
}
|
||||
const result = Binary.search(parts, part.id, (p) => p.id)
|
||||
const result = Binary.search(parts, part.id, (item) => item.id)
|
||||
if (result.found) {
|
||||
input.setStore("part", part.messageID, result.index, reconcile(part))
|
||||
break
|
||||
@@ -345,13 +346,13 @@ export function applyDirectoryEvent(input: {
|
||||
)
|
||||
const parts = input.store.part[props.messageID]
|
||||
if (!parts) break
|
||||
const result = Binary.search(parts, props.partID, (p) => p.id)
|
||||
const result = Binary.search(parts, props.partID, (part) => part.id)
|
||||
if (result.found) {
|
||||
input.setStore(
|
||||
produce((draft) => {
|
||||
const list = draft.part[props.messageID]
|
||||
if (!list) return
|
||||
const next = Binary.search(list, props.partID, (p) => p.id)
|
||||
const next = Binary.search(list, props.partID, (part) => part.id)
|
||||
if (!next.found) return
|
||||
list.splice(next.index, 1)
|
||||
if (list.length === 0) delete draft.part[props.messageID]
|
||||
@@ -364,7 +365,7 @@ export function applyDirectoryEvent(input: {
|
||||
const props = event.properties as { messageID: string; partID: string; field: string; delta: string }
|
||||
const parts = input.store.part[props.messageID]
|
||||
if (!parts) break
|
||||
const result = Binary.search(parts, props.partID, (p) => p.id)
|
||||
const result = Binary.search(parts, props.partID, (part) => part.id)
|
||||
if (!result.found) break
|
||||
const field = props.field as keyof (typeof parts)[number]
|
||||
const current = parts[result.index]?.[field]
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import type { SessionV2Info } from "@opencode-ai/sdk/v2/client"
|
||||
import { QueryClient } from "@tanstack/solid-query"
|
||||
import type { Session, SessionV2Info } from "@opencode-ai/sdk/v2/client"
|
||||
import {
|
||||
applyHomeSessionEvent,
|
||||
appendHomeSessionEvent,
|
||||
createHomeSessionIndexCache,
|
||||
HOME_V2_SESSION_PAGE_LIMIT,
|
||||
loadHomeSessionIndex,
|
||||
homeSessionIndexSessions,
|
||||
@@ -151,4 +153,33 @@ describe("Home V2 session index", () => {
|
||||
expect(homeSessionIndexRefresh("global.disposed", true).refetch).toBe(true)
|
||||
expect(homeSessionIndexRefresh("session.next.moved", true).refetch).toBe(true)
|
||||
})
|
||||
|
||||
test("removes a session from the loaded Home index", () => {
|
||||
const queryClient = new QueryClient()
|
||||
const cache = createHomeSessionIndexCache(queryClient, "server")
|
||||
const sessions = [
|
||||
{ id: "a", time: { created: 1, updated: 1 } },
|
||||
{ id: "b", time: { created: 1, updated: 1 } },
|
||||
] as Session[]
|
||||
queryClient.setQueryData(cache.indexKey, { sessions, eventSequence: 0 })
|
||||
|
||||
cache.remove("a")
|
||||
|
||||
const index = queryClient.getQueryData<{ sessions: Session[] }>(cache.indexKey)
|
||||
expect(index?.sessions.map((item) => item.id)).toEqual(["b"])
|
||||
})
|
||||
|
||||
test("keeps the session out of the Home list when the index is not mounted", () => {
|
||||
const queryClient = new QueryClient()
|
||||
const cache = createHomeSessionIndexCache(queryClient, "server")
|
||||
const sessions = [
|
||||
{ id: "a", time: { created: 1, updated: 1 } },
|
||||
{ id: "b", time: { created: 1, updated: 1 } },
|
||||
] as Session[]
|
||||
|
||||
cache.remove("a")
|
||||
|
||||
expect(queryClient.getQueryData(cache.indexKey)).toBeUndefined()
|
||||
expect(cache.sessions({ sessions, eventSequence: 0 }, undefined).map((item) => item.id)).toEqual(["b"])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -85,6 +85,7 @@ export function createHomeSessionIndexCache(queryClient: QueryClient, server: st
|
||||
const indexKey = homeSessionIndexKey(server)
|
||||
const eventsKey = homeSessionEventsKey(server)
|
||||
let connected = false
|
||||
const removed = new Set<string>()
|
||||
|
||||
return {
|
||||
indexKey,
|
||||
@@ -97,7 +98,8 @@ export function createHomeSessionIndexCache(queryClient: QueryClient, server: st
|
||||
queryClient.setQueryData<HomeSessionEvents>(eventsKey, (current) => trimHomeSessionEvents(current, sequence))
|
||||
},
|
||||
sessions(index: HomeSessionIndex | undefined, events: HomeSessionEvents | undefined) {
|
||||
return homeSessionIndexSessions(index, events)
|
||||
const sessions = homeSessionIndexSessions(index, events)
|
||||
return removed.size === 0 ? sessions : sessions.filter((session) => !removed.has(session.id))
|
||||
},
|
||||
apply(event: HomeSessionEvent) {
|
||||
if (!queryClient.getQueryState(indexKey)) return
|
||||
@@ -116,6 +118,16 @@ export function createHomeSessionIndexCache(queryClient: QueryClient, server: st
|
||||
}
|
||||
queryClient.setQueryData<HomeSessionEvents>(eventsKey, { sequence: next.sequence, entries: [] })
|
||||
},
|
||||
remove(sessionID: string) {
|
||||
removed.add(sessionID)
|
||||
if (!queryClient.getQueryState(indexKey)) return
|
||||
queryClient.setQueryData<HomeSessionIndex>(indexKey, (index) => {
|
||||
if (!index) return index
|
||||
const at = index.sessions.findIndex((session) => session.id === sessionID)
|
||||
if (at === -1) return index
|
||||
return { ...index, sessions: index.sessions.toSpliced(at, 1) }
|
||||
})
|
||||
},
|
||||
refresh(event: Event["type"]) {
|
||||
const result = homeSessionIndexRefresh(event, connected)
|
||||
connected = result.connected
|
||||
|
||||
@@ -103,6 +103,7 @@ describe("normalizeProviderList", () => {
|
||||
)
|
||||
|
||||
expect(result.connected).toEqual(["openai"])
|
||||
expect(result.defaultModel).toEqual({ providerID: "openai", modelID: "gpt-5" })
|
||||
expect(result.default).toEqual({ openai: "gpt-5" })
|
||||
expect(result.all.get("openai")?.models["gpt-old"]).toBeUndefined()
|
||||
expect(result.all.get("openai")?.models["gpt-5"]).toMatchObject({
|
||||
@@ -113,6 +114,10 @@ describe("normalizeProviderList", () => {
|
||||
variants: { high: {} },
|
||||
})
|
||||
})
|
||||
|
||||
test("preserves an empty current default", () => {
|
||||
expect(normalizeProviderList([] as ProviderListOutput["data"], [], null).defaultModel).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe("directoryKey", () => {
|
||||
|
||||
@@ -139,6 +139,7 @@ export function normalizeProviderList(
|
||||
return {
|
||||
all,
|
||||
connected: providers.map((provider) => provider.id),
|
||||
defaultModel: defaultModel ? { providerID: defaultModel.providerID, modelID: defaultModel.id } : null,
|
||||
default: Object.fromEntries(
|
||||
providers.flatMap((provider) => {
|
||||
const model =
|
||||
|
||||
@@ -8,8 +8,11 @@ import { dict as en } from "@/i18n/en"
|
||||
import { dict as uiEn } from "@opencode-ai/ui/i18n/en"
|
||||
import {
|
||||
createDesktopNativeBundle,
|
||||
detectDesktopNativeLocale,
|
||||
DESKTOP_NATIVE_ENGLISH,
|
||||
DESKTOP_NATIVE_LABELS,
|
||||
DESKTOP_NATIVE_LOCALES,
|
||||
DESKTOP_NATIVE_LOCALE_TAGS,
|
||||
type DesktopNativeBundle,
|
||||
type DesktopNativeLocale,
|
||||
} from "@/i18n/desktop-native"
|
||||
@@ -17,7 +20,7 @@ import {
|
||||
export type Locale = DesktopNativeLocale
|
||||
export type Direction = "ltr" | "rtl"
|
||||
|
||||
const RTL_LOCALES: ReadonlySet<Locale> = new Set(["ar", "ur", "pa"])
|
||||
const RTL_LOCALES: ReadonlySet<Locale> = new Set(["ar", "ur", "pa", "fa", "dv"])
|
||||
|
||||
function localeDirection(locale: Locale): Direction {
|
||||
return RTL_LOCALES.has(locale) ? "rtl" : "ltr"
|
||||
@@ -38,70 +41,7 @@ function cookie(locale: Locale) {
|
||||
|
||||
const LOCALES: readonly Locale[] = DESKTOP_NATIVE_LOCALES
|
||||
|
||||
const INTL: Record<Locale, string> = {
|
||||
en: "en",
|
||||
zh: "zh-Hans",
|
||||
zht: "zh-Hant",
|
||||
ko: "ko",
|
||||
de: "de",
|
||||
es: "es",
|
||||
fr: "fr",
|
||||
da: "da",
|
||||
ja: "ja",
|
||||
pl: "pl",
|
||||
ru: "ru",
|
||||
uk: "uk",
|
||||
ar: "ar",
|
||||
no: "nb-NO",
|
||||
br: "pt-BR",
|
||||
th: "th",
|
||||
bs: "bs",
|
||||
tr: "tr",
|
||||
hi: "hi-IN",
|
||||
nl: "nl-NL",
|
||||
id: "id-ID",
|
||||
vi: "vi-VN",
|
||||
it: "it-IT",
|
||||
ur: "ur-PK",
|
||||
pa: "pa-Arab-PK",
|
||||
az: "az-Latn-AZ",
|
||||
fi: "fi-FI",
|
||||
sv: "sv-SE",
|
||||
}
|
||||
|
||||
const LABEL_KEY: Partial<Record<Locale, keyof Dictionary>> = {
|
||||
en: "language.en",
|
||||
zh: "language.zh",
|
||||
zht: "language.zht",
|
||||
ko: "language.ko",
|
||||
de: "language.de",
|
||||
es: "language.es",
|
||||
fr: "language.fr",
|
||||
da: "language.da",
|
||||
ja: "language.ja",
|
||||
pl: "language.pl",
|
||||
ru: "language.ru",
|
||||
uk: "language.uk",
|
||||
ar: "language.ar",
|
||||
no: "language.no",
|
||||
br: "language.br",
|
||||
th: "language.th",
|
||||
bs: "language.bs",
|
||||
tr: "language.tr",
|
||||
}
|
||||
|
||||
const LABEL: Partial<Record<Locale, string>> = {
|
||||
hi: "हिन्दी",
|
||||
nl: "Nederlands",
|
||||
id: "Bahasa Indonesia",
|
||||
vi: "Tiếng Việt",
|
||||
it: "Italiano",
|
||||
ur: "اردو",
|
||||
pa: "پنجابی",
|
||||
az: "Azərbaycanca",
|
||||
fi: "Suomi",
|
||||
sv: "Svenska",
|
||||
}
|
||||
const INTL = DESKTOP_NATIVE_LOCALE_TAGS
|
||||
|
||||
const base = i18n.flatten({ ...en, ...uiEn })
|
||||
const dicts = new Map<Locale, Dictionary>([["en", base]])
|
||||
@@ -137,6 +77,40 @@ const loaders: Record<Exclude<Locale, "en">, () => Promise<Dictionary>> = {
|
||||
az: () => merge(import("@/i18n/az"), import("@opencode-ai/ui/i18n/az")),
|
||||
fi: () => merge(import("@/i18n/fi"), import("@opencode-ai/ui/i18n/fi")),
|
||||
sv: () => merge(import("@/i18n/sv"), import("@opencode-ai/ui/i18n/sv")),
|
||||
am: () => merge(import("@/i18n/am"), import("@opencode-ai/ui/i18n/am")),
|
||||
bg: () => merge(import("@/i18n/bg"), import("@opencode-ai/ui/i18n/bg")),
|
||||
bn: () => merge(import("@/i18n/bn"), import("@opencode-ai/ui/i18n/bn")),
|
||||
ca: () => merge(import("@/i18n/ca"), import("@opencode-ai/ui/i18n/ca")),
|
||||
cs: () => merge(import("@/i18n/cs"), import("@opencode-ai/ui/i18n/cs")),
|
||||
dv: () => merge(import("@/i18n/dv"), import("@opencode-ai/ui/i18n/dv")),
|
||||
dz: () => merge(import("@/i18n/dz"), import("@opencode-ai/ui/i18n/dz")),
|
||||
el: () => merge(import("@/i18n/el"), import("@opencode-ai/ui/i18n/el")),
|
||||
et: () => merge(import("@/i18n/et"), import("@opencode-ai/ui/i18n/et")),
|
||||
fa: () => merge(import("@/i18n/fa"), import("@opencode-ai/ui/i18n/fa")),
|
||||
fo: () => merge(import("@/i18n/fo"), import("@opencode-ai/ui/i18n/fo")),
|
||||
hr: () => merge(import("@/i18n/hr"), import("@opencode-ai/ui/i18n/hr")),
|
||||
hu: () => merge(import("@/i18n/hu"), import("@opencode-ai/ui/i18n/hu")),
|
||||
hy: () => merge(import("@/i18n/hy"), import("@opencode-ai/ui/i18n/hy")),
|
||||
is: () => merge(import("@/i18n/is"), import("@opencode-ai/ui/i18n/is")),
|
||||
ka: () => merge(import("@/i18n/ka"), import("@opencode-ai/ui/i18n/ka")),
|
||||
km: () => merge(import("@/i18n/km"), import("@opencode-ai/ui/i18n/km")),
|
||||
lo: () => merge(import("@/i18n/lo"), import("@opencode-ai/ui/i18n/lo")),
|
||||
lt: () => merge(import("@/i18n/lt"), import("@opencode-ai/ui/i18n/lt")),
|
||||
lv: () => merge(import("@/i18n/lv"), import("@opencode-ai/ui/i18n/lv")),
|
||||
mk: () => merge(import("@/i18n/mk"), import("@opencode-ai/ui/i18n/mk")),
|
||||
mn: () => merge(import("@/i18n/mn"), import("@opencode-ai/ui/i18n/mn")),
|
||||
ms: () => merge(import("@/i18n/ms"), import("@opencode-ai/ui/i18n/ms")),
|
||||
my: () => merge(import("@/i18n/my"), import("@opencode-ai/ui/i18n/my")),
|
||||
ne: () => merge(import("@/i18n/ne"), import("@opencode-ai/ui/i18n/ne")),
|
||||
ro: () => merge(import("@/i18n/ro"), import("@opencode-ai/ui/i18n/ro")),
|
||||
si: () => merge(import("@/i18n/si"), import("@opencode-ai/ui/i18n/si")),
|
||||
sk: () => merge(import("@/i18n/sk"), import("@opencode-ai/ui/i18n/sk")),
|
||||
sl: () => merge(import("@/i18n/sl"), import("@opencode-ai/ui/i18n/sl")),
|
||||
sq: () => merge(import("@/i18n/sq"), import("@opencode-ai/ui/i18n/sq")),
|
||||
sr: () => merge(import("@/i18n/sr"), import("@opencode-ai/ui/i18n/sr")),
|
||||
tg: () => merge(import("@/i18n/tg"), import("@opencode-ai/ui/i18n/tg")),
|
||||
tk: () => merge(import("@/i18n/tk"), import("@opencode-ai/ui/i18n/tk")),
|
||||
uz: () => merge(import("@/i18n/uz"), import("@opencode-ai/ui/i18n/uz")),
|
||||
}
|
||||
|
||||
function loadDict(locale: Locale) {
|
||||
@@ -154,63 +128,9 @@ export function loadLocaleDict(locale: Locale) {
|
||||
return loadDict(locale).then(() => undefined)
|
||||
}
|
||||
|
||||
const localeMatchers: Array<{ locale: Locale; match: (language: string) => boolean }> = [
|
||||
{ locale: "en", match: (language) => language.startsWith("en") },
|
||||
{
|
||||
locale: "zht",
|
||||
match: (language) =>
|
||||
language.startsWith("zh") &&
|
||||
(language.includes("hant") || language.includes("-tw") || language.includes("-hk") || language.includes("-mo")),
|
||||
},
|
||||
{ locale: "zh", match: (language) => language.startsWith("zh") },
|
||||
{ locale: "ko", match: (language) => language.startsWith("ko") },
|
||||
{ locale: "de", match: (language) => language.startsWith("de") },
|
||||
{ locale: "es", match: (language) => language.startsWith("es") },
|
||||
{ locale: "fr", match: (language) => language.startsWith("fr") },
|
||||
{ locale: "da", match: (language) => language.startsWith("da") },
|
||||
{ locale: "ja", match: (language) => language.startsWith("ja") },
|
||||
{ locale: "pl", match: (language) => language.startsWith("pl") },
|
||||
{ locale: "ru", match: (language) => language.startsWith("ru") },
|
||||
{ locale: "uk", match: (language) => language.startsWith("uk") },
|
||||
{ locale: "ar", match: (language) => language.startsWith("ar") },
|
||||
{
|
||||
locale: "no",
|
||||
match: (language) => language.startsWith("no") || language.startsWith("nb") || language.startsWith("nn"),
|
||||
},
|
||||
{ locale: "br", match: (language) => language.startsWith("pt") },
|
||||
{ locale: "th", match: (language) => language.startsWith("th") },
|
||||
{ locale: "bs", match: (language) => language.startsWith("bs") },
|
||||
{ locale: "tr", match: (language) => language.startsWith("tr") },
|
||||
{ locale: "hi", match: (language) => language.startsWith("hi") },
|
||||
{ locale: "nl", match: (language) => language.startsWith("nl") },
|
||||
{ locale: "id", match: (language) => language.startsWith("id") },
|
||||
{ locale: "vi", match: (language) => language.startsWith("vi") },
|
||||
{ locale: "it", match: (language) => language.startsWith("it") },
|
||||
{ locale: "ur", match: (language) => language.startsWith("ur") },
|
||||
{
|
||||
locale: "pa",
|
||||
match: (language) => language.startsWith("pa") && (language.includes("arab") || language.includes("-pk")),
|
||||
},
|
||||
{
|
||||
locale: "az",
|
||||
match: (language) => language.startsWith("az") && !language.includes("arab") && !language.includes("cyrl"),
|
||||
},
|
||||
{ locale: "fi", match: (language) => language.startsWith("fi") },
|
||||
{ locale: "sv", match: (language) => language.startsWith("sv") },
|
||||
]
|
||||
|
||||
function detectLocale(): Locale {
|
||||
if (typeof navigator !== "object") return "en"
|
||||
|
||||
const languages = navigator.languages?.length ? navigator.languages : [navigator.language]
|
||||
for (const language of languages) {
|
||||
if (!language) continue
|
||||
const normalized = language.toLowerCase()
|
||||
const match = localeMatchers.find((entry) => entry.match(normalized))
|
||||
if (match) return match.locale
|
||||
}
|
||||
|
||||
return "en"
|
||||
return detectDesktopNativeLocale(navigator.languages?.length ? navigator.languages : [navigator.language])
|
||||
}
|
||||
|
||||
export function normalizeLocale(value: string): Locale {
|
||||
@@ -231,7 +151,17 @@ function readStoredLocale() {
|
||||
}
|
||||
|
||||
const warm = readStoredLocale() ?? detectLocale()
|
||||
if (warm !== "en") void loadDict(warm)
|
||||
const initialLocale =
|
||||
warm === "en"
|
||||
? Promise.resolve(warm)
|
||||
: loadDict(warm).then(
|
||||
() => warm,
|
||||
() => "en" as const,
|
||||
)
|
||||
|
||||
export function loadInitialLocale() {
|
||||
return initialLocale
|
||||
}
|
||||
|
||||
export const { use: useLanguage, provider: LanguageProvider } = createSimpleContext({
|
||||
name: "Language",
|
||||
@@ -272,16 +202,12 @@ export const { use: useLanguage, provider: LanguageProvider } = createSimpleCont
|
||||
return i18n.resolveTemplate(current[candidate] ?? current[fallback] ?? fallback, { ...params, count })
|
||||
}
|
||||
|
||||
const label = (value: Locale) => {
|
||||
const key = LABEL_KEY[value]
|
||||
if (key) return t(key)
|
||||
return LABEL[value] ?? value
|
||||
}
|
||||
const label = (value: Locale) => DESKTOP_NATIVE_LABELS[value]
|
||||
|
||||
createEffect(() => {
|
||||
if (typeof document !== "object") return
|
||||
const value = locale()
|
||||
document.documentElement.lang = value
|
||||
document.documentElement.lang = intl()
|
||||
document.documentElement.dir = direction()
|
||||
document.cookie = cookie(value)
|
||||
})
|
||||
|
||||
@@ -6,6 +6,7 @@ import { createStore } from "solid-js/store"
|
||||
import { useModels } from "@/context/models"
|
||||
import { useSettings } from "@/context/settings"
|
||||
import { useProviders } from "@/hooks/use-providers"
|
||||
import { resolveDefaultModel } from "@/hooks/provider-catalog"
|
||||
import { Persist, persisted } from "@/utils/persist"
|
||||
import { hasCustomAgent, resolveAgent } from "./local-agent"
|
||||
import { cycleModelVariant, getConfiguredAgentVariant, resolveModelVariant } from "./model-variant"
|
||||
@@ -149,10 +150,8 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
})
|
||||
|
||||
const configuredModel = () => {
|
||||
const configured = sync().data.config.model
|
||||
if (!configured) return
|
||||
const [providerID, modelID] = configured.split("/")
|
||||
const model = { providerID, modelID }
|
||||
const model = resolveDefaultModel(providers.defaultModel(), sync().data.config.model)
|
||||
if (!model) return
|
||||
if (validModel(model)) return model
|
||||
}
|
||||
|
||||
|
||||
@@ -264,6 +264,7 @@ describe("server session", () => {
|
||||
|
||||
expect(requests).toEqual([{ sessionID: "root", limit: 20, order: "desc" }])
|
||||
expect(store.data.session_message.root.map((message) => message.id)).toEqual([user.id, assistant.id])
|
||||
expect(store.data.message.root.map((message) => message.id)).toEqual([user.id, assistant.id])
|
||||
})
|
||||
|
||||
test("extends a current page to include the user for split assistant turns", async () => {
|
||||
@@ -1497,7 +1498,7 @@ describe("server session", () => {
|
||||
|
||||
await store.sync("child", { force: true })
|
||||
|
||||
expect(store.data.message.child).toEqual([boundary, older])
|
||||
expect(store.data.message.child).toEqual([older, boundary])
|
||||
})
|
||||
|
||||
test("preserves a part update for a message being loaded from history", async () => {
|
||||
|
||||
@@ -18,7 +18,7 @@ import { message as cleanMessage } from "@/utils/diffs"
|
||||
import { sessionNotFoundError } from "@/utils/server-errors"
|
||||
import { rootSession } from "@/utils/session-route"
|
||||
import { normalizeSessionInfo } from "@/utils/session"
|
||||
import { normalizeSessionMessages } from "@/utils/session-message"
|
||||
import { compareMessages, messageKey, normalizeSessionMessages } from "@/utils/session-message"
|
||||
import { dropSessionCaches, pickSessionCacheEvictions, SESSION_CACHE_LIMIT } from "./global-sync/session-cache"
|
||||
import { createV2SessionReducer, type V2SessionReduction } from "./server-session-v2-reducer"
|
||||
import type { ServerApi } from "@/utils/server"
|
||||
@@ -26,7 +26,6 @@ import type { ServerApi } from "@/utils/server"
|
||||
type MessageApi = ServerApi["message"]
|
||||
|
||||
const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0)
|
||||
const cmpMessage = (a: Message, b: Message) => a.time.created - b.time.created || cmp(a.id, b.id)
|
||||
const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"])
|
||||
const initialMessagePageSize = 20
|
||||
const historyMessagePageSize = 200
|
||||
@@ -64,7 +63,7 @@ type MessagePage = {
|
||||
function legacyMessageSource(items: { info: Message; parts: Part[] }[]): SessionMessageInfo[] {
|
||||
return items
|
||||
.slice()
|
||||
.sort((a, b) => cmp(a.info.id, b.info.id))
|
||||
.sort((a, b) => compareMessages(a.info, b.info))
|
||||
.map((item) => {
|
||||
if (item.info.role === "user") {
|
||||
return {
|
||||
@@ -111,17 +110,16 @@ function mergeOptimisticPage(page: MessagePage, items: OptimisticItem[]) {
|
||||
const part = new Map(page.part.map((item) => [item.id, item.part]))
|
||||
const observed: { messageID: string; parts: Part[] }[] = []
|
||||
for (const item of items) {
|
||||
const result = Binary.search(session, item.message.id, (message) => message.id)
|
||||
if (!result.found) session.splice(result.index, 0, item.message)
|
||||
const result = Binary.search(session, messageKey(item.message), messageKey)
|
||||
const found = result.found
|
||||
if (!found) session.splice(result.index, 0, item.message)
|
||||
const current = part.get(item.message.id)
|
||||
const confirmed = result.found
|
||||
? item.parts.filter((part) => Binary.search(current ?? [], part.id, (value) => value.id).found)
|
||||
: []
|
||||
if (result.found) observed.push({ messageID: item.message.id, parts: confirmed })
|
||||
const confirmed = found ? item.parts.filter((part) => current?.some((value) => value.id === part.id)) : []
|
||||
if (found) observed.push({ messageID: item.message.id, parts: confirmed })
|
||||
part.set(
|
||||
item.message.id,
|
||||
merge(
|
||||
result.found ? (current ?? []) : merge(item.confirmedParts ?? [], current ?? []),
|
||||
found ? (current ?? []) : merge(item.confirmedParts ?? [], current ?? []),
|
||||
item.parts.filter((part) => !confirmed.includes(part)),
|
||||
),
|
||||
)
|
||||
@@ -158,6 +156,7 @@ function reconcileFetched<T extends { id: string }>(
|
||||
retained?: ReadonlySet<string>
|
||||
removed?: ReadonlySet<string>
|
||||
preserveUnfetched?: boolean | ((item: T) => boolean)
|
||||
compare?: (a: T, b: T) => number
|
||||
} = {},
|
||||
) {
|
||||
const result = new Map(fetched.map((item) => [item.id, item]))
|
||||
@@ -180,7 +179,8 @@ function reconcileFetched<T extends { id: string }>(
|
||||
if (!item) result.delete(id)
|
||||
}
|
||||
for (const id of options.removed ?? emptyIDs) result.delete(id)
|
||||
return [...result.values()].sort((a, b) => cmp(a.id, b.id))
|
||||
const items = [...result.values()]
|
||||
return options.compare ? items.sort(options.compare) : items
|
||||
}
|
||||
|
||||
type ServerSessionOptions = { retry?: typeof retry; protocol?: Promise<"v1" | "v2"> }
|
||||
@@ -413,8 +413,7 @@ export function createServerSession(
|
||||
if (!load) return
|
||||
// A part event keeps an existing parent when the fetched page omits it without overriding fetched metadata.
|
||||
const messages = data.message[sessionID]
|
||||
if (messages && Binary.search(messages, messageID, (message) => message.id).found)
|
||||
load.retainedMessages.add(messageID)
|
||||
if (messages?.some((message) => message.id === messageID)) load.retainedMessages.add(messageID)
|
||||
const parts = load.touchedParts.get(messageID)
|
||||
if (parts) {
|
||||
parts.add(partID)
|
||||
@@ -437,16 +436,14 @@ export function createServerSession(
|
||||
load.touchedParts.set(messageID, new Set(parts))
|
||||
load.carriedDeltaParts.set(messageID, new Set(parts))
|
||||
const messages = data.message[sessionID]
|
||||
if (messages && Binary.search(messages, messageID, (message) => message.id).found)
|
||||
load.retainedMessages.add(messageID)
|
||||
if (messages?.some((message) => message.id === messageID)) load.retainedMessages.add(messageID)
|
||||
}
|
||||
for (const [messageID, parts] of load.removedParts) {
|
||||
const touched = load.touchedParts.get(messageID) ?? new Set<string>()
|
||||
parts.forEach((partID) => touched.add(partID))
|
||||
load.touchedParts.set(messageID, touched)
|
||||
const messages = data.message[sessionID]
|
||||
if (messages && Binary.search(messages, messageID, (message) => message.id).found)
|
||||
load.retainedMessages.add(messageID)
|
||||
if (messages?.some((message) => message.id === messageID)) load.retainedMessages.add(messageID)
|
||||
}
|
||||
for (const [messageID, parts] of load.optimisticParts) {
|
||||
load.removedMessages.delete(messageID)
|
||||
@@ -555,7 +552,7 @@ export function createServerSession(
|
||||
const source = pages.flatMap((page) => page.data).toReversed()
|
||||
const normalized = normalizeSessionMessages(sessionID, source)
|
||||
return {
|
||||
session: normalized.messages.sort((a, b) => cmp(a.id, b.id)),
|
||||
session: normalized.messages.sort(compareMessages),
|
||||
part: [...normalized.parts.entries()]
|
||||
.map(([id, part]) => ({ id, part: part.sort((a, b) => cmp(a.id, b.id)) }))
|
||||
.sort((a, b) => cmp(a.id, b.id)),
|
||||
@@ -572,7 +569,7 @@ export function createServerSession(
|
||||
})
|
||||
const items = (response.data ?? []).filter((item) => !!item?.info?.id)
|
||||
return {
|
||||
session: items.map((item) => cleanMessage(item.info)).sort((a, b) => cmp(a.id, b.id)),
|
||||
session: items.map((item) => cleanMessage(item.info)).sort(compareMessages),
|
||||
part: items.map((item) => ({
|
||||
id: item.info.id,
|
||||
part: item.parts.filter((part) => !!part?.id).sort((a, b) => cmp(a.id, b.id)),
|
||||
@@ -696,7 +693,7 @@ export function createServerSession(
|
||||
const normalized = normalizeSessionMessages(sessionID, source)
|
||||
return {
|
||||
...page,
|
||||
session: normalized.messages.sort((a, b) => cmp(a.id, b.id)),
|
||||
session: normalized.messages.sort(compareMessages),
|
||||
part: [...normalized.parts.entries()]
|
||||
.map(([id, part]) => ({ id, part: part.sort((a, b) => cmp(a.id, b.id)) }))
|
||||
.sort((a, b) => cmp(a.id, b.id)),
|
||||
@@ -713,6 +710,7 @@ export function createServerSession(
|
||||
retained: load?.retainedMessages,
|
||||
removed: load?.removedMessages,
|
||||
preserveUnfetched,
|
||||
compare: compareMessages,
|
||||
})
|
||||
batch(() => {
|
||||
if (source) setData("session_message", sessionID, reconcile(source))
|
||||
@@ -754,7 +752,7 @@ export function createServerSession(
|
||||
try {
|
||||
const page = await fetchMessages(sessionID, limit, before, () => resetMessageLoad(sessionID, load))
|
||||
const first = page.session.reduce<Message | undefined>(
|
||||
(oldest, message) => (!oldest || cmpMessage(message, oldest) < 0 ? message : oldest),
|
||||
(oldest, message) => (!oldest || compareMessages(message, oldest) < 0 ? message : oldest),
|
||||
undefined,
|
||||
)
|
||||
if (generations.get(sessionID) !== active) return
|
||||
@@ -804,14 +802,15 @@ export function createServerSession(
|
||||
session: merge(
|
||||
page.session,
|
||||
parents.map((parent) => parent.message),
|
||||
),
|
||||
).sort(compareMessages),
|
||||
part: merge(
|
||||
page.part,
|
||||
parents.map((parent) => ({ id: parent.message.id, part: parent.parts })),
|
||||
),
|
||||
}
|
||||
const preserveUnfetched =
|
||||
mode === "prepend" || (!result.complete && (!first || ((message: Message) => cmpMessage(message, first) < 0)))
|
||||
mode === "prepend" ||
|
||||
(!result.complete && (!first || ((message: Message) => compareMessages(message, first) < 0)))
|
||||
applyMessagePage(
|
||||
sessionID,
|
||||
result,
|
||||
@@ -928,7 +927,7 @@ export function createServerSession(
|
||||
.message({ sessionID, messageID })
|
||||
.then((message) => {
|
||||
const current = data.session_message[sessionID] ?? []
|
||||
const messages = [...current.filter((item) => item.id !== message.id), message].sort((a, b) => cmp(a.id, b.id))
|
||||
const messages = [...current.filter((item) => item.id !== message.id), message].sort(compareMessages)
|
||||
projectV2({ sessionID, messages, touched: [message.id] })
|
||||
})
|
||||
.catch(() => {})
|
||||
@@ -1051,7 +1050,7 @@ export function createServerSession(
|
||||
setData("message", info.sessionID, [info])
|
||||
return
|
||||
}
|
||||
const result = Binary.search(messages, info.id, (message) => message.id)
|
||||
const result = Binary.search(messages, messageKey(info), messageKey)
|
||||
if (result.found) setData("message", info.sessionID, result.index, reconcile(info))
|
||||
if (!result.found)
|
||||
setData("message", info.sessionID, (value = []) => {
|
||||
@@ -1084,8 +1083,8 @@ export function createServerSession(
|
||||
produce((draft) => {
|
||||
const messages = draft.message[props.sessionID]
|
||||
if (messages) {
|
||||
const result = Binary.search(messages, props.messageID, (message) => message.id)
|
||||
if (result.found) messages.splice(result.index, 1)
|
||||
const index = messages.findIndex((message) => message.id === props.messageID)
|
||||
if (index >= 0) messages.splice(index, 1)
|
||||
}
|
||||
deleteMessageParts(draft, props.messageID)
|
||||
}),
|
||||
@@ -1097,7 +1096,7 @@ export function createServerSession(
|
||||
if (SKIP_PARTS.has(part.type)) return
|
||||
const messages = data.message[part.sessionID]
|
||||
const load = messageLoads.get(part.sessionID)
|
||||
const missing = !messages || !Binary.search(messages, part.messageID, (message) => message.id).found
|
||||
const missing = !messages?.some((message) => message.id === part.messageID)
|
||||
// Outside a page load, accepting a part without its ordered parent event would create an unbounded orphan.
|
||||
if (
|
||||
missing &&
|
||||
@@ -1341,7 +1340,7 @@ export function createServerSession(
|
||||
if (items) items.set(input.message.id, { ...input, parts, confirmedParts: [] })
|
||||
if (!items)
|
||||
optimistic.set(input.sessionID, new Map([[input.message.id, { ...input, parts, confirmedParts: [] }]]))
|
||||
setData("message", input.sessionID, (messages = []) => merge(messages, [input.message]))
|
||||
setData("message", input.sessionID, (messages = []) => merge(messages, [input.message]).sort(compareMessages))
|
||||
setData(
|
||||
"part_text_accum_delta",
|
||||
produce((draft) => {
|
||||
|
||||
@@ -4,11 +4,11 @@ import { applyOptimisticAdd, applyOptimisticRemove, mergeOptimisticPage } from "
|
||||
|
||||
type Text = Extract<Part, { type: "text" }>
|
||||
|
||||
const userMessage = (id: string, sessionID: string): Message => ({
|
||||
const userMessage = (id: string, sessionID: string, created = 1): Message => ({
|
||||
id,
|
||||
sessionID,
|
||||
role: "user",
|
||||
time: { created: 1 },
|
||||
time: { created },
|
||||
agent: "assistant",
|
||||
model: { providerID: "openai", modelID: "gpt" },
|
||||
})
|
||||
@@ -22,21 +22,21 @@ const textPart = (id: string, sessionID: string, messageID: string): Text => ({
|
||||
})
|
||||
|
||||
describe("sync optimistic reducers", () => {
|
||||
test("applyOptimisticAdd inserts message in sorted order and stores parts", () => {
|
||||
test("applyOptimisticAdd inserts by creation time", () => {
|
||||
const sessionID = "ses_1"
|
||||
const draft = {
|
||||
message: { [sessionID]: [userMessage("msg_2", sessionID)] },
|
||||
message: { [sessionID]: [userMessage("msg_z", sessionID, 1)] },
|
||||
part: {} as Record<string, Part[] | undefined>,
|
||||
}
|
||||
|
||||
applyOptimisticAdd(draft, {
|
||||
sessionID,
|
||||
message: userMessage("msg_1", sessionID),
|
||||
parts: [textPart("prt_2", sessionID, "msg_1"), textPart("prt_1", sessionID, "msg_1")],
|
||||
message: userMessage("msg_a", sessionID, 2),
|
||||
parts: [textPart("prt_2", sessionID, "msg_a"), textPart("prt_1", sessionID, "msg_a")],
|
||||
})
|
||||
|
||||
expect(draft.message[sessionID]?.map((x) => x.id)).toEqual(["msg_1", "msg_2"])
|
||||
expect(draft.part.msg_1?.map((x) => x.id)).toEqual(["prt_1", "prt_2"])
|
||||
expect(draft.message[sessionID]?.map((x) => x.id)).toEqual(["msg_z", "msg_a"])
|
||||
expect(draft.part.msg_a?.map((x) => x.id)).toEqual(["prt_1", "prt_2"])
|
||||
})
|
||||
|
||||
test("applyOptimisticRemove removes message and part entries", () => {
|
||||
@@ -60,19 +60,33 @@ describe("sync optimistic reducers", () => {
|
||||
const sessionID = "ses_1"
|
||||
const page = mergeOptimisticPage(
|
||||
{
|
||||
session: [userMessage("msg_1", sessionID)],
|
||||
part: [{ id: "msg_1", part: [textPart("prt_1", sessionID, "msg_1")] }],
|
||||
session: [userMessage("msg_z", sessionID, 1)],
|
||||
part: [{ id: "msg_z", part: [textPart("prt_1", sessionID, "msg_z")] }],
|
||||
complete: true,
|
||||
},
|
||||
[{ message: userMessage("msg_2", sessionID), parts: [textPart("prt_2", sessionID, "msg_2")] }],
|
||||
[{ message: userMessage("msg_a", sessionID, 2), parts: [textPart("prt_2", sessionID, "msg_a")] }],
|
||||
)
|
||||
|
||||
expect(page.session.map((x) => x.id)).toEqual(["msg_1", "msg_2"])
|
||||
expect(page.part.find((x) => x.id === "msg_2")?.part.map((x) => x.id)).toEqual(["prt_2"])
|
||||
expect(page.session.map((x) => x.id)).toEqual(["msg_z", "msg_a"])
|
||||
expect(page.part.find((x) => x.id === "msg_a")?.part.map((x) => x.id)).toEqual(["prt_2"])
|
||||
expect(page.confirmed).toEqual([])
|
||||
expect(page.complete).toBe(true)
|
||||
})
|
||||
|
||||
test("mergeOptimisticPage uses IDs only to break equal-time ties", () => {
|
||||
const sessionID = "ses_1"
|
||||
const page = mergeOptimisticPage(
|
||||
{
|
||||
session: [userMessage("msg_z", sessionID, 1)],
|
||||
part: [],
|
||||
complete: true,
|
||||
},
|
||||
[{ message: userMessage("msg_a", sessionID, 1), parts: [] }],
|
||||
)
|
||||
|
||||
expect(page.session.map((message) => message.id)).toEqual(["msg_a", "msg_z"])
|
||||
})
|
||||
|
||||
test("mergeOptimisticPage keeps missing optimistic parts until the server has them", () => {
|
||||
const sessionID = "ses_1"
|
||||
const page = mergeOptimisticPage(
|
||||
|
||||
@@ -3,6 +3,7 @@ import { createMemo } from "solid-js"
|
||||
import { useServerSync } from "./server-sync"
|
||||
import { useSDK } from "./sdk"
|
||||
import type { Message, Part } from "@opencode-ai/sdk/v2/client"
|
||||
import { messageKey } from "@/utils/session-message"
|
||||
|
||||
const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"])
|
||||
|
||||
@@ -67,7 +68,7 @@ export function mergeOptimisticPage(page: MessagePage, items: OptimisticItem[])
|
||||
const confirmed: string[] = []
|
||||
|
||||
for (const item of items) {
|
||||
const result = Binary.search(session, item.message.id, (message) => message.id)
|
||||
const result = Binary.search(session, messageKey(item.message), messageKey)
|
||||
const found = result.found
|
||||
if (!found) session.splice(result.index, 0, item.message)
|
||||
|
||||
@@ -92,7 +93,7 @@ export function mergeOptimisticPage(page: MessagePage, items: OptimisticItem[])
|
||||
export function applyOptimisticAdd(draft: OptimisticStore, input: OptimisticAddInput) {
|
||||
const messages = draft.message[input.sessionID]
|
||||
if (messages) {
|
||||
const result = Binary.search(messages, input.message.id, (m) => m.id)
|
||||
const result = Binary.search(messages, messageKey(input.message), messageKey)
|
||||
messages.splice(result.index, 0, input.message)
|
||||
} else {
|
||||
draft.message[input.sessionID] = [input.message]
|
||||
@@ -103,8 +104,8 @@ export function applyOptimisticAdd(draft: OptimisticStore, input: OptimisticAddI
|
||||
export function applyOptimisticRemove(draft: OptimisticStore, input: OptimisticRemoveInput) {
|
||||
const messages = draft.message[input.sessionID]
|
||||
if (messages) {
|
||||
const result = Binary.search(messages, input.messageID, (m) => m.id)
|
||||
if (result.found) messages.splice(result.index, 1)
|
||||
const index = messages.findIndex((message) => message.id === input.messageID)
|
||||
if (index >= 0) messages.splice(index, 1)
|
||||
}
|
||||
delete draft.part[input.messageID]
|
||||
}
|
||||
|
||||
+28
-25
@@ -3,6 +3,7 @@
|
||||
import * as Sentry from "@sentry/solid"
|
||||
import { render } from "solid-js/web"
|
||||
import { AppBaseProviders, AppInterface } from "@/app"
|
||||
import { loadInitialLocale } from "@/context/language"
|
||||
import { type Platform, PlatformProvider } from "@/context/platform"
|
||||
import { createBrowserDraftStore } from "@/utils/draft-store"
|
||||
import { dict as en } from "@/i18n/en"
|
||||
@@ -149,29 +150,31 @@ if (import.meta.env.VITE_SENTRY_DSN) {
|
||||
}
|
||||
|
||||
if (root instanceof HTMLElement) {
|
||||
const auth = authFromToken(new URLSearchParams(location.search).get("auth_token"))
|
||||
clearAuthToken()
|
||||
const server: ServerConnection.Http = {
|
||||
type: "http",
|
||||
authToken: !!auth,
|
||||
http: {
|
||||
url: getCurrentUrl(),
|
||||
...auth,
|
||||
},
|
||||
}
|
||||
render(
|
||||
() => (
|
||||
<PlatformProvider value={platform}>
|
||||
<AppBaseProviders>
|
||||
<AppInterface
|
||||
defaultServer={ServerConnection.Key.make(getDefaultUrl())}
|
||||
canonicalLocalServer={ServerConnection.key(server)}
|
||||
servers={[server]}
|
||||
disableHealthCheck
|
||||
/>
|
||||
</AppBaseProviders>
|
||||
</PlatformProvider>
|
||||
),
|
||||
root,
|
||||
)
|
||||
void loadInitialLocale().then((locale) => {
|
||||
const auth = authFromToken(new URLSearchParams(location.search).get("auth_token"))
|
||||
clearAuthToken()
|
||||
const server: ServerConnection.Http = {
|
||||
type: "http",
|
||||
authToken: !!auth,
|
||||
http: {
|
||||
url: getCurrentUrl(),
|
||||
...auth,
|
||||
},
|
||||
}
|
||||
render(
|
||||
() => (
|
||||
<PlatformProvider value={platform}>
|
||||
<AppBaseProviders locale={locale}>
|
||||
<AppInterface
|
||||
defaultServer={ServerConnection.Key.make(getDefaultUrl())}
|
||||
canonicalLocalServer={ServerConnection.key(server)}
|
||||
servers={[server]}
|
||||
disableHealthCheck
|
||||
/>
|
||||
</AppBaseProviders>
|
||||
</PlatformProvider>
|
||||
),
|
||||
root,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect, test } from "bun:test"
|
||||
import type { NormalizedProviderListResponse } from "@opencode-ai/session-ui/context"
|
||||
import { selectProviderCatalog } from "./provider-catalog"
|
||||
import { resolveDefaultModel, selectProviderCatalog } from "./provider-catalog"
|
||||
|
||||
const catalog = (id: string): NormalizedProviderListResponse => ({
|
||||
all: new Map([[id, { id, name: id, source: "api", env: [], options: {}, models: {} }]]),
|
||||
@@ -57,3 +57,21 @@ test("falls back to the global catalog for route consumers", () => {
|
||||
}),
|
||||
).toBe(global)
|
||||
})
|
||||
|
||||
test("uses the current server default model", () => {
|
||||
expect(resolveDefaultModel({ providerID: "openai", modelID: "gpt-5" }, "anthropic/claude")).toEqual({
|
||||
providerID: "openai",
|
||||
modelID: "gpt-5",
|
||||
})
|
||||
})
|
||||
|
||||
test("does not use legacy config when the current server has no default", () => {
|
||||
expect(resolveDefaultModel(null, "anthropic/claude")).toBeUndefined()
|
||||
})
|
||||
|
||||
test("uses config for legacy servers", () => {
|
||||
expect(resolveDefaultModel(undefined, "anthropic/claude")).toEqual({
|
||||
providerID: "anthropic",
|
||||
modelID: "claude",
|
||||
})
|
||||
})
|
||||
|
||||
@@ -25,3 +25,13 @@ export function selectProviderCatalog(input: ProviderCatalogInput) {
|
||||
if (input.explicit) return emptyProviderCatalog
|
||||
return input.global
|
||||
}
|
||||
|
||||
export function resolveDefaultModel(
|
||||
current: NormalizedProviderListResponse["defaultModel"],
|
||||
legacy: string | undefined,
|
||||
) {
|
||||
if (current !== undefined) return current ?? undefined
|
||||
if (!legacy) return undefined
|
||||
const [providerID, modelID] = legacy.split("/")
|
||||
return { providerID, modelID }
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ export function useProviders(directory: Accessor<string | undefined>) {
|
||||
return {
|
||||
all: () => providers().all,
|
||||
default: () => providers().default,
|
||||
defaultModel: () => providers().defaultModel,
|
||||
popular: () =>
|
||||
pipe(
|
||||
providers().all,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -184,6 +184,9 @@ export const dict = {
|
||||
"command.session.share.description": "مشاركة هذه الجلسة ونسخ الرابط إلى الحافظة",
|
||||
"command.session.unshare": "إلغاء مشاركة الجلسة",
|
||||
"command.session.unshare.description": "إيقاف مشاركة هذه الجلسة",
|
||||
"command.session.export": "تصدير الجلسة",
|
||||
"command.session.export.description": "تصدير النص الكامل للجلسة بصيغة JSON",
|
||||
|
||||
"palette.search.placeholder": "البحث في الملفات والأوامر والجلسات",
|
||||
"palette.search.placeholder.home": "البحث في الأوامر والجلسات",
|
||||
"palette.empty": "لا توجد نتائج",
|
||||
@@ -531,6 +534,8 @@ export const dict = {
|
||||
"dialog.project.edit.worktree.startup": "برنامج نصي لبدء تشغيل مساحة العمل",
|
||||
"dialog.project.edit.worktree.startup.description": "يتم تشغيله بعد إنشاء مساحة عمل جديدة (شجرة عمل).",
|
||||
"dialog.project.edit.worktree.startup.placeholder": "مثال: bun install",
|
||||
"dialog.usageExceeded.dontShowAgain": "عدم الإظهار مرة أخرى",
|
||||
|
||||
"context.breakdown.title": "تفصيل السياق",
|
||||
"context.breakdown.note": 'تفصيل تقريبي لرموز الإدخال المميزة. يشمل "أخرى" تعريفات الأدوات والأعباء الإضافية.',
|
||||
"context.breakdown.system": "النظام",
|
||||
@@ -540,6 +545,8 @@ export const dict = {
|
||||
"context.breakdown.other": "أخرى",
|
||||
"context.systemPrompt.title": "موجّه النظام",
|
||||
"context.rawMessages.title": "الرسائل الخام",
|
||||
"context.export.session": "تصدير الجلسة",
|
||||
|
||||
"context.stats.session": "جلسة",
|
||||
"context.stats.messages": "رسائل",
|
||||
"context.stats.provider": "موفر",
|
||||
@@ -606,6 +613,11 @@ export const dict = {
|
||||
"toast.session.unshare.success.description": "تم إلغاء مشاركة الجلسة بنجاح!",
|
||||
"toast.session.unshare.failed.title": "فشل إلغاء مشاركة الجلسة",
|
||||
"toast.session.unshare.failed.description": "حدث خطأ أثناء إلغاء مشاركة الجلسة",
|
||||
"toast.session.export.success.title": "تم تصدير الجلسة",
|
||||
"toast.session.export.success.description": "تم حفظ الجلسة في \u2068{{filename}}\u2069",
|
||||
"toast.session.export.failed.title": "فشل تصدير الجلسة",
|
||||
"toast.session.export.failed.description": "حدث خطأ أثناء تصدير الجلسة",
|
||||
|
||||
"toast.session.listFailed.title": "فشل تحميل الجلسات لـ {{project}}",
|
||||
"toast.update.title": "تحديث متاح",
|
||||
"toast.update.description": "يتوفر الآن إصدار جديد من OpenCode ({{version}}) للتثبيت.",
|
||||
@@ -793,6 +805,7 @@ export const dict = {
|
||||
"common.moreOptions": "مزيد من الخيارات",
|
||||
"common.learnMore": "اطّلع على المزيد",
|
||||
"common.rename": "إعادة تسمية",
|
||||
"common.export": "تصدير",
|
||||
"common.reset": "إعادة تعيين",
|
||||
"common.archive": "أرشفة",
|
||||
"common.delete": "حذف",
|
||||
|
||||
@@ -180,6 +180,9 @@ export const dict = {
|
||||
"command.session.share.description": "Bu sessiyanı paylaş və URL-ni buferə kopyala",
|
||||
"command.session.unshare": "Sessiyanın paylaşımını dayandır",
|
||||
"command.session.unshare.description": "Bu sessiyanın paylaşımını dayandır",
|
||||
"command.session.export": "Sessiyanı ixrac et",
|
||||
"command.session.export.description": "Sessiyanın tam transkriptini JSON formatında ixrac et",
|
||||
|
||||
"palette.search.placeholder": "Fayl, əmr və sessiya axtar",
|
||||
"palette.search.placeholder.home": "Əmr və sessiyaları axtar",
|
||||
"palette.empty": "Nəticə tapılmadı",
|
||||
@@ -541,6 +544,8 @@ export const dict = {
|
||||
"dialog.releaseNotes.action.next": "Növbəti",
|
||||
"dialog.releaseNotes.action.hideFuture": "Gələcəkdə göstərmə",
|
||||
"dialog.releaseNotes.media.alt": "Buraxılış önbaxışı",
|
||||
"dialog.usageExceeded.dontShowAgain": "Bir daha göstərmə",
|
||||
|
||||
"context.breakdown.title": "Kontekst bölgüsü",
|
||||
"context.breakdown.note": 'Giriş tokenlərinin təxmini bölgüsü. "Digər" alət təriflərini və əlavə yükü əhatə edir.',
|
||||
"context.breakdown.system": "Sistem",
|
||||
@@ -550,6 +555,8 @@ export const dict = {
|
||||
"context.breakdown.other": "Digər",
|
||||
"context.systemPrompt.title": "Sistem promptu",
|
||||
"context.rawMessages.title": "Xam mesajlar",
|
||||
"context.export.session": "Sessiyanı ixrac et",
|
||||
|
||||
"context.stats.session": "Sessiya",
|
||||
"context.stats.messages": "Mesajlar",
|
||||
"context.stats.provider": "Provayder",
|
||||
@@ -616,6 +623,11 @@ export const dict = {
|
||||
"toast.session.unshare.success.description": "Sessiyanın paylaşımı uğurla dayandırıldı!",
|
||||
"toast.session.unshare.failed.title": "Sessiyanın paylaşımı dayandırıla bilmədi",
|
||||
"toast.session.unshare.failed.description": "Sessiyanın paylaşımını dayandırarkən xəta baş verdi",
|
||||
"toast.session.export.success.title": "Sessiya ixrac edildi",
|
||||
"toast.session.export.success.description": "Sessiya {{filename}} faylına saxlanıldı",
|
||||
"toast.session.export.failed.title": "Sessiya ixrac edilə bilmədi",
|
||||
"toast.session.export.failed.description": "Sessiyanı ixrac edərkən xəta baş verdi",
|
||||
|
||||
"toast.session.listFailed.title": "{{project}} üçün sessiyalar yüklənə bilmədi",
|
||||
"toast.project.reloadFailed.title": "{{project}} yenidən yüklənə bilmədi",
|
||||
"toast.update.title": "Yeniləmə mövcuddur",
|
||||
@@ -832,6 +844,7 @@ export const dict = {
|
||||
"common.moreOptions": "Daha çox seçim",
|
||||
"common.learnMore": "Ətraflı öyrən",
|
||||
"common.rename": "Adını dəyiş",
|
||||
"common.export": "İxrac et",
|
||||
"common.reset": "Sıfırla",
|
||||
"common.archive": "Arxivlə",
|
||||
"common.delete": "Sil",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -186,6 +186,9 @@ export const dict = {
|
||||
"command.session.share.description": "Compartilhar esta sessão e copiar a URL para a área de transferência",
|
||||
"command.session.unshare": "Parar de compartilhar sessão",
|
||||
"command.session.unshare.description": "Parar de compartilhar esta sessão",
|
||||
"command.session.export": "Exportar sessão",
|
||||
"command.session.export.description": "Exportar a transcrição completa da sessão como JSON",
|
||||
|
||||
"palette.search.placeholder": "Buscar arquivos, comandos e sessões",
|
||||
"palette.search.placeholder.home": "Buscar comandos e sessões",
|
||||
"palette.empty": "Nenhum resultado encontrado",
|
||||
@@ -534,6 +537,8 @@ export const dict = {
|
||||
"dialog.project.edit.worktree.startup": "Script de inicialização do espaço de trabalho",
|
||||
"dialog.project.edit.worktree.startup.description": "Executa após criar um novo espaço de trabalho (worktree).",
|
||||
"dialog.project.edit.worktree.startup.placeholder": "Ex.: bun install",
|
||||
"dialog.usageExceeded.dontShowAgain": "Não mostrar novamente",
|
||||
|
||||
"context.breakdown.title": "Detalhamento do contexto",
|
||||
"context.breakdown.note":
|
||||
'Detalhamento aproximado dos tokens de entrada. "Outros" inclui definições de ferramentas e sobrecarga.',
|
||||
@@ -544,6 +549,8 @@ export const dict = {
|
||||
"context.breakdown.other": "Outros",
|
||||
"context.systemPrompt.title": "Prompt do sistema",
|
||||
"context.rawMessages.title": "Mensagens brutas",
|
||||
"context.export.session": "Exportar sessão",
|
||||
|
||||
"context.stats.session": "Sessão",
|
||||
"context.stats.messages": "Mensagens",
|
||||
"context.stats.provider": "Provedor",
|
||||
@@ -610,6 +617,11 @@ export const dict = {
|
||||
"toast.session.unshare.success.description": "Sessão deixou de ser compartilhada com sucesso!",
|
||||
"toast.session.unshare.failed.title": "Falha ao parar de compartilhar sessão",
|
||||
"toast.session.unshare.failed.description": "Ocorreu um erro ao parar de compartilhar a sessão",
|
||||
"toast.session.export.success.title": "Sessão exportada",
|
||||
"toast.session.export.success.description": "Sessão salva em {{filename}}",
|
||||
"toast.session.export.failed.title": "Falha ao exportar sessão",
|
||||
"toast.session.export.failed.description": "Ocorreu um erro ao exportar a sessão",
|
||||
|
||||
"toast.session.listFailed.title": "Falha ao carregar sessões para {{project}}",
|
||||
"toast.update.title": "Atualização disponível",
|
||||
"toast.update.description": "Uma nova versão do OpenCode ({{version}}) está disponível para instalação.",
|
||||
@@ -796,6 +808,7 @@ export const dict = {
|
||||
"common.moreOptions": "Mais opções",
|
||||
"common.learnMore": "Saiba mais",
|
||||
"common.rename": "Renomear",
|
||||
"common.export": "Exportar",
|
||||
"common.reset": "Redefinir",
|
||||
"common.archive": "Arquivar",
|
||||
"common.delete": "Excluir",
|
||||
|
||||
@@ -193,6 +193,9 @@ export const dict = {
|
||||
"command.session.unshare": "Ukini dijeljenje sesije",
|
||||
"command.session.unshare.description": "Zaustavi dijeljenje ove sesije",
|
||||
|
||||
"command.session.export": "Izvezi sesiju",
|
||||
"command.session.export.description": "Izvezi cijeli zapis sesije u JSON formatu",
|
||||
|
||||
"palette.search.placeholder": "Pretraži datoteke, komande i sesije",
|
||||
"palette.search.placeholder.home": "Pretraži komande i sesije",
|
||||
"palette.empty": "Nema rezultata",
|
||||
@@ -565,6 +568,8 @@ export const dict = {
|
||||
"dialog.project.edit.worktree.startup.description": "Pokreće se nakon kreiranja novog radnog prostora (worktree).",
|
||||
"dialog.project.edit.worktree.startup.placeholder": "npr. bun install",
|
||||
|
||||
"dialog.usageExceeded.dontShowAgain": "Nemoj više prikazivati",
|
||||
|
||||
"context.breakdown.title": "Razlaganje konteksta",
|
||||
"context.breakdown.note":
|
||||
'Približna raspodjela ulaznih tokena. "Ostalo" uključuje definicije alata i dodatno opterećenje.',
|
||||
@@ -577,6 +582,8 @@ export const dict = {
|
||||
"context.systemPrompt.title": "Sistemski prompt",
|
||||
"context.rawMessages.title": "Sirove poruke",
|
||||
|
||||
"context.export.session": "Izvezi sesiju",
|
||||
|
||||
"context.stats.session": "Sesija",
|
||||
"context.stats.messages": "Poruke",
|
||||
"context.stats.provider": "Provajder",
|
||||
@@ -655,6 +662,11 @@ export const dict = {
|
||||
"toast.session.unshare.failed.title": "Neuspjelo ukidanje dijeljenja",
|
||||
"toast.session.unshare.failed.description": "Došlo je do greške prilikom ukidanja dijeljenja",
|
||||
|
||||
"toast.session.export.success.title": "Sesija izvezena",
|
||||
"toast.session.export.success.description": "Sesija je sačuvana kao {{filename}}",
|
||||
"toast.session.export.failed.title": "Izvoz sesije nije uspio",
|
||||
"toast.session.export.failed.description": "Došlo je do greške prilikom izvoza sesije",
|
||||
|
||||
"toast.session.listFailed.title": "Neuspjelo učitavanje sesija za {{project}}",
|
||||
|
||||
"toast.update.title": "Dostupno ažuriranje",
|
||||
@@ -856,6 +868,7 @@ export const dict = {
|
||||
"common.moreOptions": "Više opcija",
|
||||
"common.learnMore": "Saznaj više",
|
||||
"common.rename": "Preimenuj",
|
||||
"common.export": "Izvezi",
|
||||
"common.reset": "Vrati na početno stanje",
|
||||
"common.archive": "Arhiviraj",
|
||||
"common.delete": "Izbriši",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -92,6 +92,9 @@ export const dict = {
|
||||
"command.session.unshare": "Stop deling af session",
|
||||
"command.session.unshare.description": "Stop med at dele denne session",
|
||||
|
||||
"command.session.export": "Eksportér session",
|
||||
"command.session.export.description": "Eksportér hele sessionsudskriften som JSON",
|
||||
|
||||
"palette.search.placeholder": "Søg i filer, kommandoer og sessioner",
|
||||
"palette.search.placeholder.home": "Søg i kommandoer og sessioner",
|
||||
"palette.empty": "Ingen resultater fundet",
|
||||
@@ -446,6 +449,8 @@ export const dict = {
|
||||
"dialog.project.edit.worktree.startup": "Opstartsscript for arbejdsområde",
|
||||
"dialog.project.edit.worktree.startup.description": "Køres efter oprettelse af et nyt arbejdsområde (worktree).",
|
||||
"dialog.project.edit.worktree.startup.placeholder": "f.eks. bun install",
|
||||
"dialog.usageExceeded.dontShowAgain": "Vis ikke igen",
|
||||
|
||||
"context.breakdown.title": "Kontekstfordeling",
|
||||
"context.breakdown.note":
|
||||
'Omtrentlig fordeling af input-tokens. "Andre" inkluderer værktøjsdefinitioner og overhead.',
|
||||
@@ -458,6 +463,8 @@ export const dict = {
|
||||
"context.systemPrompt.title": "Systemprompt",
|
||||
"context.rawMessages.title": "Rå beskeder",
|
||||
|
||||
"context.export.session": "Eksportér session",
|
||||
|
||||
"context.stats.session": "Session",
|
||||
"context.stats.messages": "Beskeder",
|
||||
"context.stats.provider": "Udbyder",
|
||||
@@ -535,6 +542,11 @@ export const dict = {
|
||||
"toast.session.unshare.failed.title": "Kunne ikke stoppe deling af session",
|
||||
"toast.session.unshare.failed.description": "Der opstod en fejl under stop af sessionsdeling",
|
||||
|
||||
"toast.session.export.success.title": "Session eksporteret",
|
||||
"toast.session.export.success.description": "Sessionen blev gemt i {{filename}}",
|
||||
"toast.session.export.failed.title": "Kunne ikke eksportere session",
|
||||
"toast.session.export.failed.description": "Der opstod en fejl under eksport af sessionen",
|
||||
|
||||
"toast.session.listFailed.title": "Kunne ikke indlæse sessioner for {{project}}",
|
||||
|
||||
"toast.update.title": "Opdatering tilgængelig",
|
||||
@@ -733,6 +745,7 @@ export const dict = {
|
||||
"common.moreOptions": "Flere muligheder",
|
||||
"common.learnMore": "Lær mere",
|
||||
"common.rename": "Omdøb",
|
||||
"common.export": "Eksportér",
|
||||
"common.reset": "Nulstil",
|
||||
"common.archive": "Arkivér",
|
||||
"common.delete": "Slet",
|
||||
|
||||
@@ -89,6 +89,9 @@ export const dict = {
|
||||
"command.session.share.description": "Diese Sitzung teilen und URL in die Zwischenablage kopieren",
|
||||
"command.session.unshare": "Teilen der Sitzung aufheben",
|
||||
"command.session.unshare.description": "Teilen dieser Sitzung beenden",
|
||||
"command.session.export": "Sitzung exportieren",
|
||||
"command.session.export.description": "Das vollständige Transkript der Sitzung als JSON exportieren",
|
||||
|
||||
"palette.search.placeholder": "Dateien, Befehle und Sitzungen durchsuchen",
|
||||
"palette.search.placeholder.home": "Befehle und Sitzungen durchsuchen",
|
||||
"palette.empty": "Keine Ergebnisse gefunden",
|
||||
@@ -425,6 +428,8 @@ export const dict = {
|
||||
"dialog.project.edit.worktree.startup.description":
|
||||
"Wird nach dem Erstellen eines neuen Arbeitsbereichs (Worktree) ausgeführt.",
|
||||
"dialog.project.edit.worktree.startup.placeholder": "z. B. bun install",
|
||||
"dialog.usageExceeded.dontShowAgain": "Nicht mehr anzeigen",
|
||||
|
||||
"context.breakdown.title": "Kontext-Aufschlüsselung",
|
||||
"context.breakdown.note":
|
||||
'Ungefähre Aufschlüsselung der Eingabe-Token. "Andere" beinhaltet Werkzeugdefinitionen und Overhead.',
|
||||
@@ -435,6 +440,8 @@ export const dict = {
|
||||
"context.breakdown.other": "Andere",
|
||||
"context.systemPrompt.title": "System-Prompt",
|
||||
"context.rawMessages.title": "Rohdaten der Nachrichten",
|
||||
"context.export.session": "Sitzung exportieren",
|
||||
|
||||
"context.stats.session": "Sitzung",
|
||||
"context.stats.messages": "Nachrichten",
|
||||
"context.stats.provider": "Anbieter",
|
||||
@@ -501,6 +508,11 @@ export const dict = {
|
||||
"toast.session.unshare.success.description": "Teilen der Sitzung erfolgreich aufgehoben!",
|
||||
"toast.session.unshare.failed.title": "Aufheben des Teilens fehlgeschlagen",
|
||||
"toast.session.unshare.failed.description": "Beim Aufheben des Teilens ist ein Fehler aufgetreten",
|
||||
"toast.session.export.success.title": "Sitzung exportiert",
|
||||
"toast.session.export.success.description": "Sitzung unter {{filename}} gespeichert",
|
||||
"toast.session.export.failed.title": "Sitzung konnte nicht exportiert werden",
|
||||
"toast.session.export.failed.description": "Beim Exportieren der Sitzung ist ein Fehler aufgetreten",
|
||||
|
||||
"toast.session.listFailed.title": "Sitzungen für {{project}} konnten nicht geladen werden",
|
||||
"toast.update.title": "Update verfügbar",
|
||||
"toast.update.description": "Eine neue Version von OpenCode ({{version}}) ist zur Installation verfügbar.",
|
||||
@@ -686,6 +698,7 @@ export const dict = {
|
||||
"common.moreOptions": "Weitere Optionen",
|
||||
"common.learnMore": "Mehr erfahren",
|
||||
"common.rename": "Umbenennen",
|
||||
"common.export": "Exportieren",
|
||||
"common.reset": "Zurücksetzen",
|
||||
"common.archive": "Archivieren",
|
||||
"common.delete": "Löschen",
|
||||
|
||||
@@ -3,12 +3,83 @@ import {
|
||||
createDesktopNativeBundle,
|
||||
DESKTOP_NATIVE_ENGLISH,
|
||||
DESKTOP_NATIVE_KEYS,
|
||||
DESKTOP_NATIVE_LABELS,
|
||||
DESKTOP_NATIVE_LOCALES,
|
||||
DESKTOP_NATIVE_LOCALE_TAGS,
|
||||
detectDesktopNativeLocale,
|
||||
DESKTOP_NATIVE_MAX_PAYLOAD_BYTES,
|
||||
formatDesktopNativeMessage,
|
||||
parseDesktopNativeBundle,
|
||||
} from "./desktop-native"
|
||||
|
||||
describe("desktop native translations", () => {
|
||||
test("uses native language names independent of the active locale", () => {
|
||||
expect(DESKTOP_NATIVE_LOCALES.map((locale) => DESKTOP_NATIVE_LABELS[locale])).toEqual([
|
||||
"English",
|
||||
"简体中文",
|
||||
"繁體中文",
|
||||
"한국어",
|
||||
"Deutsch",
|
||||
"Español",
|
||||
"Français",
|
||||
"Dansk",
|
||||
"日本語",
|
||||
"Polski",
|
||||
"Русский",
|
||||
"Українська",
|
||||
"Bosanski",
|
||||
"العربية",
|
||||
"Norsk",
|
||||
"Português (Brasil)",
|
||||
"ไทย",
|
||||
"Türkçe",
|
||||
"हिन्दी",
|
||||
"Nederlands",
|
||||
"Bahasa Indonesia",
|
||||
"Tiếng Việt",
|
||||
"Italiano",
|
||||
"اردو",
|
||||
"پنجابی",
|
||||
"Azərbaycanca",
|
||||
"Suomi",
|
||||
"Svenska",
|
||||
"አማርኛ",
|
||||
"Български",
|
||||
"বাংলা",
|
||||
"Català",
|
||||
"Čeština",
|
||||
"ދިވެހި",
|
||||
"རྫོང་ཁ",
|
||||
"Ελληνικά",
|
||||
"Eesti",
|
||||
"فارسی",
|
||||
"Føroyskt",
|
||||
"Hrvatski",
|
||||
"Magyar",
|
||||
"Հայերեն",
|
||||
"Íslenska",
|
||||
"ქართული",
|
||||
"ខ្មែរ",
|
||||
"ລາວ",
|
||||
"Lietuvių",
|
||||
"Latviešu",
|
||||
"Македонски",
|
||||
"Монгол",
|
||||
"Bahasa Melayu",
|
||||
"မြန်မာ",
|
||||
"नेपाली",
|
||||
"Română",
|
||||
"සිංහල",
|
||||
"Slovenčina",
|
||||
"Slovenščina",
|
||||
"Shqip",
|
||||
"Српски",
|
||||
"Тоҷикӣ",
|
||||
"Türkmençe",
|
||||
"Oʻzbekcha",
|
||||
])
|
||||
})
|
||||
|
||||
test("accepts the exact typed bundle", () => {
|
||||
const bundle = createDesktopNativeBundle("en", (key) => DESKTOP_NATIVE_ENGLISH[key])
|
||||
expect(parseDesktopNativeBundle(bundle)).toEqual(bundle)
|
||||
@@ -39,3 +110,42 @@ describe("desktop native translations", () => {
|
||||
expect(formatDesktopNativeMessage("{{known}} {{unknown}}", { known: "yes" })).toBe("yes {{unknown}}")
|
||||
})
|
||||
})
|
||||
|
||||
describe("desktop native locale detection", () => {
|
||||
test("follows preference order and skips invalid or unsupported tags", () => {
|
||||
expect(detectDesktopNativeLocale(["not_a_locale", "fr-FR"])).toBe("fr")
|
||||
expect(detectDesktopNativeLocale(["eo", "de-DE"])).toBe("de")
|
||||
})
|
||||
|
||||
test("uses Unicode likely subtags for script-sensitive bundles", () => {
|
||||
expect(detectDesktopNativeLocale(["zh-TW"])).toBe("zht")
|
||||
expect(detectDesktopNativeLocale(["zh-SG"])).toBe("zh")
|
||||
expect(detectDesktopNativeLocale(["pa-PK"])).toBe("pa")
|
||||
expect(detectDesktopNativeLocale(["pa-IN", "fr"])).toBe("fr")
|
||||
expect(detectDesktopNativeLocale(["az-Cyrl", "de"])).toBe("de")
|
||||
expect(detectDesktopNativeLocale(["sr-Cyrl"])).toBe("sr")
|
||||
expect(detectDesktopNativeLocale(["sr-Latn", "en"])).toBe("en")
|
||||
expect(detectDesktopNativeLocale(["uz-Latn"])).toBe("uz")
|
||||
})
|
||||
|
||||
test("recognizes Norwegian language tags", () => {
|
||||
expect(detectDesktopNativeLocale(["no"])).toBe("no")
|
||||
expect(detectDesktopNativeLocale(["nb-NO"])).toBe("no")
|
||||
expect(detectDesktopNativeLocale(["nn-NO"])).toBe("no")
|
||||
})
|
||||
})
|
||||
|
||||
describe("desktop native ICU data", () => {
|
||||
test("accepts every locale in standard Intl formatters", () => {
|
||||
for (const locale of DESKTOP_NATIVE_LOCALES) {
|
||||
const tag = DESKTOP_NATIVE_LOCALE_TAGS[locale]
|
||||
expect(() => new Intl.Locale(tag), `${locale} locale`).not.toThrow()
|
||||
expect(() => new Intl.NumberFormat(tag), `${locale} number`).not.toThrow()
|
||||
expect(() => new Intl.DateTimeFormat(tag), `${locale} date`).not.toThrow()
|
||||
expect(() => new Intl.PluralRules(tag), `${locale} plural`).not.toThrow()
|
||||
expect(() => new Intl.ListFormat(tag), `${locale} list`).not.toThrow()
|
||||
expect(() => new Intl.DisplayNames(tag, { type: "language" }), `${locale} names`).not.toThrow()
|
||||
expect(() => new Intl.Segmenter(tag), `${locale} segmenter`).not.toThrow()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -27,10 +27,200 @@ export const DESKTOP_NATIVE_LOCALES = [
|
||||
"az",
|
||||
"fi",
|
||||
"sv",
|
||||
"am",
|
||||
"bg",
|
||||
"bn",
|
||||
"ca",
|
||||
"cs",
|
||||
"dv",
|
||||
"dz",
|
||||
"el",
|
||||
"et",
|
||||
"fa",
|
||||
"fo",
|
||||
"hr",
|
||||
"hu",
|
||||
"hy",
|
||||
"is",
|
||||
"ka",
|
||||
"km",
|
||||
"lo",
|
||||
"lt",
|
||||
"lv",
|
||||
"mk",
|
||||
"mn",
|
||||
"ms",
|
||||
"my",
|
||||
"ne",
|
||||
"ro",
|
||||
"si",
|
||||
"sk",
|
||||
"sl",
|
||||
"sq",
|
||||
"sr",
|
||||
"tg",
|
||||
"tk",
|
||||
"uz",
|
||||
] as const
|
||||
|
||||
export type DesktopNativeLocale = (typeof DESKTOP_NATIVE_LOCALES)[number]
|
||||
|
||||
export const DESKTOP_NATIVE_LABELS: Record<DesktopNativeLocale, string> = {
|
||||
en: "English",
|
||||
zh: "简体中文",
|
||||
zht: "繁體中文",
|
||||
ko: "한국어",
|
||||
de: "Deutsch",
|
||||
es: "Español",
|
||||
fr: "Français",
|
||||
da: "Dansk",
|
||||
ja: "日本語",
|
||||
pl: "Polski",
|
||||
ru: "Русский",
|
||||
uk: "Українська",
|
||||
bs: "Bosanski",
|
||||
ar: "العربية",
|
||||
no: "Norsk",
|
||||
br: "Português (Brasil)",
|
||||
th: "ไทย",
|
||||
tr: "Türkçe",
|
||||
hi: "हिन्दी",
|
||||
nl: "Nederlands",
|
||||
id: "Bahasa Indonesia",
|
||||
vi: "Tiếng Việt",
|
||||
it: "Italiano",
|
||||
ur: "اردو",
|
||||
pa: "پنجابی",
|
||||
az: "Azərbaycanca",
|
||||
fi: "Suomi",
|
||||
sv: "Svenska",
|
||||
am: "አማርኛ",
|
||||
bg: "Български",
|
||||
bn: "বাংলা",
|
||||
ca: "Català",
|
||||
cs: "Čeština",
|
||||
dv: "ދިވެހި",
|
||||
dz: "རྫོང་ཁ",
|
||||
el: "Ελληνικά",
|
||||
et: "Eesti",
|
||||
fa: "فارسی",
|
||||
fo: "Føroyskt",
|
||||
hr: "Hrvatski",
|
||||
hu: "Magyar",
|
||||
hy: "Հայերեն",
|
||||
is: "Íslenska",
|
||||
ka: "ქართული",
|
||||
km: "ខ្មែរ",
|
||||
lo: "ລາວ",
|
||||
lt: "Lietuvių",
|
||||
lv: "Latviešu",
|
||||
mk: "Македонски",
|
||||
mn: "Монгол",
|
||||
ms: "Bahasa Melayu",
|
||||
my: "မြန်မာ",
|
||||
ne: "नेपाली",
|
||||
ro: "Română",
|
||||
si: "සිංහල",
|
||||
sk: "Slovenčina",
|
||||
sl: "Slovenščina",
|
||||
sq: "Shqip",
|
||||
sr: "Српски",
|
||||
tg: "Тоҷикӣ",
|
||||
tk: "Türkmençe",
|
||||
uz: "Oʻzbekcha",
|
||||
}
|
||||
|
||||
export const DESKTOP_NATIVE_LOCALE_TAGS: Record<DesktopNativeLocale, string> = {
|
||||
en: "en",
|
||||
zh: "zh-Hans",
|
||||
zht: "zh-Hant",
|
||||
ko: "ko",
|
||||
de: "de",
|
||||
es: "es",
|
||||
fr: "fr",
|
||||
da: "da",
|
||||
ja: "ja",
|
||||
pl: "pl",
|
||||
ru: "ru",
|
||||
uk: "uk",
|
||||
bs: "bs",
|
||||
ar: "ar",
|
||||
no: "nb-NO",
|
||||
br: "pt-BR",
|
||||
th: "th",
|
||||
tr: "tr",
|
||||
hi: "hi-IN",
|
||||
nl: "nl-NL",
|
||||
id: "id-ID",
|
||||
vi: "vi-VN",
|
||||
it: "it-IT",
|
||||
ur: "ur-PK",
|
||||
pa: "pa-Arab-PK",
|
||||
az: "az-Latn-AZ",
|
||||
fi: "fi-FI",
|
||||
sv: "sv-SE",
|
||||
am: "am-ET",
|
||||
bg: "bg-BG",
|
||||
bn: "bn-BD",
|
||||
ca: "ca-AD",
|
||||
cs: "cs-CZ",
|
||||
dv: "dv-MV",
|
||||
dz: "dz-BT",
|
||||
el: "el-GR",
|
||||
et: "et-EE",
|
||||
fa: "fa-IR",
|
||||
fo: "fo-FO",
|
||||
hr: "hr-HR",
|
||||
hu: "hu-HU",
|
||||
hy: "hy-AM",
|
||||
is: "is-IS",
|
||||
ka: "ka-GE",
|
||||
km: "km-KH",
|
||||
lo: "lo-LA",
|
||||
lt: "lt-LT",
|
||||
lv: "lv-LV",
|
||||
mk: "mk-MK",
|
||||
mn: "mn-MN",
|
||||
ms: "ms-MY",
|
||||
my: "my-MM",
|
||||
ne: "ne-NP",
|
||||
ro: "ro-RO",
|
||||
si: "si-LK",
|
||||
sk: "sk-SK",
|
||||
sl: "sl-SI",
|
||||
sq: "sq-AL",
|
||||
sr: "sr-Cyrl-RS",
|
||||
tg: "tg-Cyrl-TJ",
|
||||
tk: "tk-Latn-TM",
|
||||
uz: "uz-Latn-UZ",
|
||||
}
|
||||
|
||||
export function detectDesktopNativeLocale(languages: readonly string[]): DesktopNativeLocale {
|
||||
for (const language of languages) {
|
||||
const source = locale(language)
|
||||
if (!source) continue
|
||||
if (["no", "nb", "nn"].includes(source.language)) return "no"
|
||||
const match = DESKTOP_NATIVE_LOCALES.find((candidate) => {
|
||||
const target = locale(DESKTOP_NATIVE_LOCALE_TAGS[candidate])
|
||||
return target?.language === source.language && target.script === source.script
|
||||
})
|
||||
if (match) return match
|
||||
}
|
||||
return "en"
|
||||
}
|
||||
|
||||
export function desktopNativePluralCategories(locale: DesktopNativeLocale) {
|
||||
return new Intl.PluralRules(DESKTOP_NATIVE_LOCALE_TAGS[locale]).resolvedOptions().pluralCategories
|
||||
}
|
||||
|
||||
function locale(value: string) {
|
||||
try {
|
||||
return new Intl.Locale(value).maximize()
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
export const DESKTOP_NATIVE_ENGLISH = {
|
||||
"desktop.menu.app": "OpenCode",
|
||||
"desktop.menu.file": "File",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -95,6 +95,8 @@ export const dict = {
|
||||
"command.session.share.description": "Share this session and copy the URL to clipboard",
|
||||
"command.session.unshare": "Unshare session",
|
||||
"command.session.unshare.description": "Stop sharing this session",
|
||||
"command.session.export": "Export session",
|
||||
"command.session.export.description": "Export the full session transcript as JSON",
|
||||
|
||||
"palette.search.placeholder": "Search files, commands, and sessions",
|
||||
"palette.search.placeholder.home": "Search commands and sessions",
|
||||
@@ -478,6 +480,7 @@ export const dict = {
|
||||
"dialog.releaseNotes.action.next": "Next",
|
||||
"dialog.releaseNotes.action.hideFuture": "Don't show these in the future",
|
||||
"dialog.releaseNotes.media.alt": "Release preview",
|
||||
"dialog.usageExceeded.dontShowAgain": "Don't show again",
|
||||
|
||||
"context.breakdown.title": "Context Breakdown",
|
||||
"context.breakdown.note": 'Approximate breakdown of input tokens. "Other" includes tool definitions and overhead.',
|
||||
@@ -489,6 +492,7 @@ export const dict = {
|
||||
|
||||
"context.systemPrompt.title": "System Prompt",
|
||||
"context.rawMessages.title": "Raw messages",
|
||||
"context.export.session": "Export session",
|
||||
|
||||
"context.stats.session": "Session",
|
||||
"context.stats.messages": "Messages",
|
||||
@@ -568,6 +572,11 @@ export const dict = {
|
||||
"toast.session.unshare.failed.title": "Failed to unshare session",
|
||||
"toast.session.unshare.failed.description": "An error occurred while unsharing the session",
|
||||
|
||||
"toast.session.export.success.title": "Session exported",
|
||||
"toast.session.export.success.description": "Saved session to {{filename}}",
|
||||
"toast.session.export.failed.title": "Failed to export session",
|
||||
"toast.session.export.failed.description": "An error occurred while exporting the session",
|
||||
|
||||
"toast.session.listFailed.title": "Failed to load sessions for {{project}}",
|
||||
"toast.project.reloadFailed.title": "Failed to reload {{project}}",
|
||||
|
||||
@@ -802,6 +811,7 @@ export const dict = {
|
||||
"common.moreOptions": "More options",
|
||||
"common.learnMore": "Learn more",
|
||||
"common.rename": "Rename",
|
||||
"common.export": "Export",
|
||||
"common.reset": "Reset",
|
||||
"common.archive": "Archive",
|
||||
"common.delete": "Delete",
|
||||
|
||||
@@ -193,6 +193,9 @@ export const dict = {
|
||||
"command.session.unshare": "Dejar de compartir sesión",
|
||||
"command.session.unshare.description": "Dejar de compartir esta sesión",
|
||||
|
||||
"command.session.export": "Exportar sesión",
|
||||
"command.session.export.description": "Exportar la transcripción completa de la sesión como JSON",
|
||||
|
||||
"palette.search.placeholder": "Buscar archivos, comandos y sesiones",
|
||||
"palette.search.placeholder.home": "Buscar comandos y sesiones",
|
||||
"palette.empty": "No se encontraron resultados",
|
||||
@@ -567,6 +570,8 @@ export const dict = {
|
||||
"Se ejecuta después de crear un nuevo espacio de trabajo (árbol de trabajo).",
|
||||
"dialog.project.edit.worktree.startup.placeholder": "p. ej. bun install",
|
||||
|
||||
"dialog.usageExceeded.dontShowAgain": "No volver a mostrar",
|
||||
|
||||
"context.breakdown.title": "Desglose del contexto",
|
||||
"context.breakdown.note":
|
||||
'Desglose aproximado de tokens de entrada. "Otro" incluye definiciones de herramientas y sobrecarga.',
|
||||
@@ -579,6 +584,8 @@ export const dict = {
|
||||
"context.systemPrompt.title": "Prompt del sistema",
|
||||
"context.rawMessages.title": "Mensajes en bruto",
|
||||
|
||||
"context.export.session": "Exportar sesión",
|
||||
|
||||
"context.stats.session": "Sesión",
|
||||
"context.stats.messages": "Mensajes",
|
||||
"context.stats.provider": "Proveedor",
|
||||
@@ -657,6 +664,11 @@ export const dict = {
|
||||
"toast.session.unshare.failed.title": "Fallo al dejar de compartir sesión",
|
||||
"toast.session.unshare.failed.description": "Ocurrió un error al dejar de compartir la sesión",
|
||||
|
||||
"toast.session.export.success.title": "Sesión exportada",
|
||||
"toast.session.export.success.description": "Sesión guardada en {{filename}}",
|
||||
"toast.session.export.failed.title": "No se pudo exportar la sesión",
|
||||
"toast.session.export.failed.description": "Se produjo un error al exportar la sesión",
|
||||
|
||||
"toast.session.listFailed.title": "Fallo al cargar sesiones para {{project}}",
|
||||
|
||||
"toast.update.title": "Actualización disponible",
|
||||
@@ -861,6 +873,7 @@ export const dict = {
|
||||
"common.moreOptions": "Más opciones",
|
||||
"common.learnMore": "Más información",
|
||||
"common.rename": "Renombrar",
|
||||
"common.export": "Exportar",
|
||||
"common.reset": "Restablecer",
|
||||
"common.archive": "Archivar",
|
||||
"common.delete": "Eliminar",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -85,6 +85,9 @@ export const dict = {
|
||||
"command.session.share.description": "Jaa tämä istunto ja kopioi URL-osoite leikepöydälle",
|
||||
"command.session.unshare": "Peru istunnon jakaminen",
|
||||
"command.session.unshare.description": "Lopeta tämän istunnon jakaminen",
|
||||
"command.session.export": "Vie istunto",
|
||||
"command.session.export.description": "Vie istunnon koko transkriptio JSON-muodossa",
|
||||
|
||||
"palette.search.placeholder": "Hae tiedostoja, komentoja ja istuntoja",
|
||||
"palette.search.placeholder.home": "Hae komentoja ja istuntoja",
|
||||
"palette.empty": "Tuloksia ei löytynyt",
|
||||
@@ -429,6 +432,8 @@ export const dict = {
|
||||
"dialog.releaseNotes.action.next": "Seuraava",
|
||||
"dialog.releaseNotes.action.hideFuture": "Älä näytä näitä jatkossa",
|
||||
"dialog.releaseNotes.media.alt": "Julkaisun esikatselu",
|
||||
"dialog.usageExceeded.dontShowAgain": "Älä näytä uudelleen",
|
||||
|
||||
"context.breakdown.title": "Kontekstin erittely",
|
||||
"context.breakdown.note":
|
||||
'Syötetokenien arvioitu jakautuminen. "Muut" sisältää työkalumäärittelyt ja muun oheissisällön.',
|
||||
@@ -439,6 +444,8 @@ export const dict = {
|
||||
"context.breakdown.other": "Muut",
|
||||
"context.systemPrompt.title": "Järjestelmäkehote",
|
||||
"context.rawMessages.title": "Raakaviestit",
|
||||
"context.export.session": "Vie istunto",
|
||||
|
||||
"context.stats.session": "Istunto",
|
||||
"context.stats.messages": "Viestit",
|
||||
"context.stats.provider": "Palveluntarjoaja",
|
||||
@@ -505,6 +512,11 @@ export const dict = {
|
||||
"toast.session.unshare.success.description": "Istunnon jakaminen peruutettu onnistuneesti!",
|
||||
"toast.session.unshare.failed.title": "Istunnon jakamisen peruuttaminen epäonnistui",
|
||||
"toast.session.unshare.failed.description": "Virhe peruttaessa istunnon jakamista",
|
||||
"toast.session.export.success.title": "Istunto viety",
|
||||
"toast.session.export.success.description": "Istunto tallennettu tiedostoon {{filename}}",
|
||||
"toast.session.export.failed.title": "Istunnon vieminen epäonnistui",
|
||||
"toast.session.export.failed.description": "Istuntoa vietäessä tapahtui virhe",
|
||||
|
||||
"toast.session.listFailed.title": "Projektin {{project}} istuntojen lataaminen epäonnistui",
|
||||
"toast.project.reloadFailed.title": "Projektin {{project}} lataaminen uudelleen epäonnistui",
|
||||
"toast.update.title": "Päivitys saatavilla",
|
||||
@@ -722,6 +734,7 @@ export const dict = {
|
||||
"common.moreOptions": "Lisää vaihtoehtoja",
|
||||
"common.learnMore": "Lue lisää",
|
||||
"common.rename": "Nimeä uudelleen",
|
||||
"common.export": "Vie",
|
||||
"common.reset": "Palauta",
|
||||
"common.archive": "Arkistoi",
|
||||
"common.delete": "Poista",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -186,6 +186,9 @@ export const dict = {
|
||||
"command.session.share.description": "Partager cette session et copier l'URL dans le presse-papiers",
|
||||
"command.session.unshare": "Ne plus partager la session",
|
||||
"command.session.unshare.description": "Arrêter de partager cette session",
|
||||
"command.session.export": "Exporter la session",
|
||||
"command.session.export.description": "Exporter la transcription intégrale de la session au format JSON",
|
||||
|
||||
"palette.search.placeholder": "Rechercher des fichiers, des commandes et des sessions",
|
||||
"palette.search.placeholder.home": "Rechercher des commandes et des sessions",
|
||||
"palette.empty": "Aucun résultat trouvé",
|
||||
@@ -540,6 +543,8 @@ export const dict = {
|
||||
"dialog.project.edit.worktree.startup.description":
|
||||
"S'exécute après la création d'un nouvel espace de travail (arbre de travail).",
|
||||
"dialog.project.edit.worktree.startup.placeholder": "p. ex. bun install",
|
||||
"dialog.usageExceeded.dontShowAgain": "Ne plus afficher",
|
||||
|
||||
"context.breakdown.title": "Répartition du contexte",
|
||||
"context.breakdown.note":
|
||||
"Répartition approximative des jetons d'entrée. \"Autre\" inclut les définitions d'outils et les données annexes.",
|
||||
@@ -550,6 +555,8 @@ export const dict = {
|
||||
"context.breakdown.other": "Autre",
|
||||
"context.systemPrompt.title": "Invite système",
|
||||
"context.rawMessages.title": "Messages bruts",
|
||||
"context.export.session": "Exporter la session",
|
||||
|
||||
"context.stats.session": "Session",
|
||||
"context.stats.messages": "Messages",
|
||||
"context.stats.provider": "Fournisseur",
|
||||
@@ -617,6 +624,11 @@ export const dict = {
|
||||
"toast.session.unshare.failed.title": "Échec de la désactivation du partage",
|
||||
"toast.session.unshare.failed.description":
|
||||
"Une erreur s'est produite lors de la désactivation du partage de la session",
|
||||
"toast.session.export.success.title": "Session exportée",
|
||||
"toast.session.export.success.description": "Session enregistrée dans {{filename}}",
|
||||
"toast.session.export.failed.title": "Échec de l’exportation de la session",
|
||||
"toast.session.export.failed.description": "Une erreur s’est produite lors de l’exportation de la session",
|
||||
|
||||
"toast.session.listFailed.title": "Échec du chargement des sessions pour {{project}}",
|
||||
"toast.update.title": "Mise à jour disponible",
|
||||
"toast.update.description":
|
||||
@@ -799,6 +811,7 @@ export const dict = {
|
||||
"common.moreOptions": "Plus d'options",
|
||||
"common.learnMore": "En savoir plus",
|
||||
"common.rename": "Renommer",
|
||||
"common.export": "Exporter",
|
||||
"common.reset": "Réinitialiser",
|
||||
"common.archive": "Archiver",
|
||||
"common.delete": "Supprimer",
|
||||
|
||||
@@ -185,6 +185,9 @@ export const dict = {
|
||||
"command.session.share.description": "इस सेशन को साझा करें और URL को क्लिपबोर्ड पर कॉपी करें",
|
||||
"command.session.unshare": "सेशन साझा करना बंद करें",
|
||||
"command.session.unshare.description": "इस सेशन को साझा करना बंद करें",
|
||||
"command.session.export": "सेशन निर्यात करें",
|
||||
"command.session.export.description": "सेशन की पूरी ट्रांसक्रिप्ट को JSON के रूप में निर्यात करें",
|
||||
|
||||
"palette.search.placeholder": "फ़ाइलें, कमांड और सेशन खोजें",
|
||||
"palette.search.placeholder.home": "कमांड और सेशन खोजें",
|
||||
"palette.empty": "कोई परिणाम नहीं मिला",
|
||||
@@ -543,6 +546,8 @@ export const dict = {
|
||||
"dialog.releaseNotes.action.next": "अगला",
|
||||
"dialog.releaseNotes.action.hideFuture": "भविष्य में इन्हें न दिखाएँ",
|
||||
"dialog.releaseNotes.media.alt": "रिलीज़ पूर्वावलोकन",
|
||||
"dialog.usageExceeded.dontShowAgain": "फिर से न दिखाएँ",
|
||||
|
||||
"context.breakdown.title": "कॉन्टेक्स्ट ब्रेकडाउन",
|
||||
"context.breakdown.note": 'इनपुट टोकन का अनुमानित विभाजन। "अन्य" में टूल की परिभाषाएँ और अतिरिक्त खर्च शामिल हैं।',
|
||||
"context.breakdown.system": "सिस्टम",
|
||||
@@ -552,6 +557,8 @@ export const dict = {
|
||||
"context.breakdown.other": "अन्य",
|
||||
"context.systemPrompt.title": "सिस्टम प्रॉम्प्ट",
|
||||
"context.rawMessages.title": "मूल संदेश",
|
||||
"context.export.session": "सेशन निर्यात करें",
|
||||
|
||||
"context.stats.session": "सेशन",
|
||||
"context.stats.messages": "संदेश",
|
||||
"context.stats.provider": "प्रोवाइडर",
|
||||
@@ -618,6 +625,11 @@ export const dict = {
|
||||
"toast.session.unshare.success.description": "सेशन सफलतापूर्वक अनशेयर किया गया!",
|
||||
"toast.session.unshare.failed.title": "सेशन को अनशेयर करने में विफल",
|
||||
"toast.session.unshare.failed.description": "सेशन को अनशेयर करते समय एक त्रुटि उत्पन्न हुई",
|
||||
"toast.session.export.success.title": "सेशन निर्यात किया गया",
|
||||
"toast.session.export.success.description": "सेशन को {{filename}} में सहेजा गया",
|
||||
"toast.session.export.failed.title": "सेशन निर्यात करने में विफल",
|
||||
"toast.session.export.failed.description": "सेशन निर्यात करते समय एक त्रुटि हुई",
|
||||
|
||||
"toast.session.listFailed.title": "{{project}} के लिए सेशन लोड करने में विफल",
|
||||
"toast.project.reloadFailed.title": "{{project}} को पुनः लोड करने में विफल",
|
||||
"toast.update.title": "उपलब्ध अद्यतन",
|
||||
@@ -833,6 +845,7 @@ export const dict = {
|
||||
"common.moreOptions": "अधिक विकल्प",
|
||||
"common.learnMore": "और अधिक जानें",
|
||||
"common.rename": "नाम बदलें",
|
||||
"common.export": "निर्यात करें",
|
||||
"common.reset": "रीसेट करें",
|
||||
"common.archive": "संग्रहित करें",
|
||||
"common.delete": "हटाएँ",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -193,6 +193,9 @@ export const dict = {
|
||||
"command.session.unshare": "Hentikan berbagi",
|
||||
"command.session.unshare.description": "Hentikan berbagi sesi ini",
|
||||
|
||||
"command.session.export": "Ekspor sesi",
|
||||
"command.session.export.description": "Ekspor transkrip sesi lengkap sebagai JSON",
|
||||
|
||||
"palette.search.placeholder": "Cari berkas, perintah, dan sesi",
|
||||
"palette.search.placeholder.home": "Cari perintah dan sesi",
|
||||
"palette.empty": "Hasil tidak ditemukan",
|
||||
@@ -576,6 +579,8 @@ export const dict = {
|
||||
"dialog.releaseNotes.action.hideFuture": "Jangan tampilkan ini di masa depan",
|
||||
"dialog.releaseNotes.media.alt": "Pratinjau rilis",
|
||||
|
||||
"dialog.usageExceeded.dontShowAgain": "Jangan tampilkan lagi",
|
||||
|
||||
"context.breakdown.title": "Rincian Konteks",
|
||||
"context.breakdown.note": 'Perkiraan rincian token masukan. "Lainnya" mencakup definisi alat dan beban tambahan.',
|
||||
"context.breakdown.system": "Sistem",
|
||||
@@ -587,6 +592,8 @@ export const dict = {
|
||||
"context.systemPrompt.title": "Prompt Sistem",
|
||||
"context.rawMessages.title": "Pesan mentah",
|
||||
|
||||
"context.export.session": "Ekspor sesi",
|
||||
|
||||
"context.stats.session": "Sesi",
|
||||
"context.stats.messages": "Pesan",
|
||||
"context.stats.provider": "Penyedia",
|
||||
@@ -664,6 +671,11 @@ export const dict = {
|
||||
"toast.session.unshare.failed.title": "Gagal menghentikan berbagi sesi",
|
||||
"toast.session.unshare.failed.description": "Terjadi kesalahan saat menghentikan berbagi sesi",
|
||||
|
||||
"toast.session.export.success.title": "Sesi diekspor",
|
||||
"toast.session.export.success.description": "Sesi disimpan ke {{filename}}",
|
||||
"toast.session.export.failed.title": "Gagal mengekspor sesi",
|
||||
"toast.session.export.failed.description": "Terjadi kesalahan saat mengekspor sesi",
|
||||
|
||||
"toast.session.listFailed.title": "Gagal memuat sesi untuk {{project}}",
|
||||
"toast.project.reloadFailed.title": "Gagal memuat ulang {{project}}",
|
||||
|
||||
@@ -897,6 +909,7 @@ export const dict = {
|
||||
"common.moreOptions": "Opsi lainnya",
|
||||
"common.learnMore": "Pelajari lebih lanjut",
|
||||
"common.rename": "Ganti nama",
|
||||
"common.export": "Ekspor",
|
||||
"common.reset": "Atur ulang",
|
||||
"common.archive": "Arsipkan",
|
||||
"common.delete": "Hapus",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -86,6 +86,9 @@ export const dict = {
|
||||
"command.session.share.description": "Condividi questa sessione e copia l'URL negli appunti",
|
||||
"command.session.unshare": "Annulla condivisione sessione",
|
||||
"command.session.unshare.description": "Interrompi la condivisione di questa sessione",
|
||||
"command.session.export": "Esporta sessione",
|
||||
"command.session.export.description": "Esporta la trascrizione completa della sessione in formato JSON",
|
||||
|
||||
"palette.search.placeholder": "Cerca file, comandi e sessioni",
|
||||
"palette.search.placeholder.home": "Cerca comandi e sessioni",
|
||||
"palette.empty": "Nessun risultato trovato",
|
||||
@@ -447,6 +450,8 @@ export const dict = {
|
||||
"dialog.releaseNotes.action.next": "Avanti",
|
||||
"dialog.releaseNotes.action.hideFuture": "Non mostrarli in futuro",
|
||||
"dialog.releaseNotes.media.alt": "Anteprima delle novità",
|
||||
"dialog.usageExceeded.dontShowAgain": "Non mostrare più",
|
||||
|
||||
"context.breakdown.title": "Ripartizione del contesto",
|
||||
"context.breakdown.note":
|
||||
'Ripartizione approssimativa dei token di input. "Altro" include le definizioni degli strumenti e i dati aggiuntivi.',
|
||||
@@ -457,6 +462,8 @@ export const dict = {
|
||||
"context.breakdown.other": "Altro",
|
||||
"context.systemPrompt.title": "Prompt di sistema",
|
||||
"context.rawMessages.title": "Messaggi non elaborati",
|
||||
"context.export.session": "Esporta sessione",
|
||||
|
||||
"context.stats.session": "Sessione",
|
||||
"context.stats.messages": "Messaggi",
|
||||
"context.stats.provider": "Provider",
|
||||
@@ -524,6 +531,11 @@ export const dict = {
|
||||
"toast.session.unshare.failed.title": "Impossibile annullare la condivisione della sessione",
|
||||
"toast.session.unshare.failed.description":
|
||||
"Si è verificato un errore durante l'annullamento della condivisione della sessione",
|
||||
"toast.session.export.success.title": "Sessione esportata",
|
||||
"toast.session.export.success.description": "Sessione salvata in {{filename}}",
|
||||
"toast.session.export.failed.title": "Impossibile esportare la sessione",
|
||||
"toast.session.export.failed.description": "Si è verificato un errore durante l’esportazione della sessione",
|
||||
|
||||
"toast.session.listFailed.title": "Impossibile caricare le sessioni per {{project}}",
|
||||
"toast.project.reloadFailed.title": "Impossibile ricaricare {{project}}",
|
||||
"toast.update.title": "Aggiornamento disponibile",
|
||||
@@ -743,6 +755,7 @@ export const dict = {
|
||||
"common.moreOptions": "Altre opzioni",
|
||||
"common.learnMore": "Saperne di più",
|
||||
"common.rename": "Rinomina",
|
||||
"common.export": "Esporta",
|
||||
"common.reset": "Ripristina",
|
||||
"common.archive": "Archivia",
|
||||
"common.delete": "Elimina",
|
||||
|
||||
@@ -184,6 +184,9 @@ export const dict = {
|
||||
"command.session.share.description": "このセッションを共有しURLをクリップボードにコピー",
|
||||
"command.session.unshare": "セッションの共有を停止",
|
||||
"command.session.unshare.description": "このセッションの共有を停止",
|
||||
"command.session.export": "セッションをエクスポート",
|
||||
"command.session.export.description": "セッションの全記録を JSON としてエクスポート",
|
||||
|
||||
"palette.search.placeholder": "ファイル、コマンド、セッションを検索",
|
||||
"palette.search.placeholder.home": "コマンドとセッションを検索",
|
||||
"palette.empty": "結果が見つかりません",
|
||||
@@ -533,6 +536,8 @@ export const dict = {
|
||||
"dialog.project.edit.worktree.startup.description":
|
||||
"新しいワークスペース (ワークツリー) を作成した後に実行されます。",
|
||||
"dialog.project.edit.worktree.startup.placeholder": "例: bun install",
|
||||
"dialog.usageExceeded.dontShowAgain": "今後表示しない",
|
||||
|
||||
"context.breakdown.title": "コンテキストの内訳",
|
||||
"context.breakdown.note": '入力トークンのおおよその内訳です。"その他"にはツールの定義やオーバーヘッドが含まれます。',
|
||||
"context.breakdown.system": "システム",
|
||||
@@ -542,6 +547,8 @@ export const dict = {
|
||||
"context.breakdown.other": "その他",
|
||||
"context.systemPrompt.title": "システムプロンプト",
|
||||
"context.rawMessages.title": "生のメッセージ",
|
||||
"context.export.session": "セッションをエクスポート",
|
||||
|
||||
"context.stats.session": "セッション",
|
||||
"context.stats.messages": "メッセージ",
|
||||
"context.stats.provider": "プロバイダー",
|
||||
@@ -608,6 +615,11 @@ export const dict = {
|
||||
"toast.session.unshare.success.description": "セッションの共有解除に成功しました!",
|
||||
"toast.session.unshare.failed.title": "セッションの共有解除に失敗しました",
|
||||
"toast.session.unshare.failed.description": "セッションの共有解除中にエラーが発生しました",
|
||||
"toast.session.export.success.title": "セッションをエクスポートしました",
|
||||
"toast.session.export.success.description": "セッションを {{filename}} に保存しました",
|
||||
"toast.session.export.failed.title": "セッションのエクスポートに失敗しました",
|
||||
"toast.session.export.failed.description": "セッションのエクスポート中にエラーが発生しました",
|
||||
|
||||
"toast.session.listFailed.title": "{{project}}のセッション読み込みに失敗しました",
|
||||
"toast.update.title": "アップデートが利用可能です",
|
||||
"toast.update.description": "OpenCodeの新しいバージョン ({{version}}) がインストール可能です。",
|
||||
@@ -781,6 +793,7 @@ export const dict = {
|
||||
"common.moreOptions": "その他のオプション",
|
||||
"common.learnMore": "詳細",
|
||||
"common.rename": "名前を変更",
|
||||
"common.export": "エクスポート",
|
||||
"common.reset": "リセット",
|
||||
"common.archive": "アーカイブ",
|
||||
"common.delete": "削除",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -81,6 +81,9 @@ export const dict = {
|
||||
"command.session.share.description": "이 세션을 공유하고 URL을 클립보드에 복사",
|
||||
"command.session.unshare": "세션 공유 중지",
|
||||
"command.session.unshare.description": "이 세션 공유 중지",
|
||||
"command.session.export": "세션 내보내기",
|
||||
"command.session.export.description": "전체 세션 기록을 JSON으로 내보내기",
|
||||
|
||||
"palette.search.placeholder": "파일, 명령어 및 세션 검색",
|
||||
"palette.search.placeholder.home": "명령어 및 세션 검색",
|
||||
"palette.empty": "결과 없음",
|
||||
@@ -336,6 +339,8 @@ export const dict = {
|
||||
"dialog.project.edit.worktree.startup": "작업 공간 시작 스크립트",
|
||||
"dialog.project.edit.worktree.startup.description": "새 작업 공간(작업 트리)을 만든 뒤 실행됩니다.",
|
||||
"dialog.project.edit.worktree.startup.placeholder": "예: bun install",
|
||||
"dialog.usageExceeded.dontShowAgain": "다시 표시하지 않기",
|
||||
|
||||
"context.breakdown.title": "컨텍스트 분석",
|
||||
"context.breakdown.note": '입력 토큰의 대략적인 분석입니다. "기타"에는 도구 정의 및 오버헤드가 포함됩니다.',
|
||||
"context.breakdown.system": "시스템",
|
||||
@@ -345,6 +350,8 @@ export const dict = {
|
||||
"context.breakdown.other": "기타",
|
||||
"context.systemPrompt.title": "시스템 프롬프트",
|
||||
"context.rawMessages.title": "원시 메시지",
|
||||
"context.export.session": "세션 내보내기",
|
||||
|
||||
"context.stats.session": "세션",
|
||||
"context.stats.messages": "메시지",
|
||||
"context.stats.provider": "공급자",
|
||||
@@ -410,6 +417,11 @@ export const dict = {
|
||||
"toast.session.unshare.success.description": "세션 공유가 성공적으로 해제되었습니다!",
|
||||
"toast.session.unshare.failed.title": "세션 공유 해제 실패",
|
||||
"toast.session.unshare.failed.description": "세션 공유를 해제하는 동안 오류가 발생했습니다",
|
||||
"toast.session.export.success.title": "세션을 내보냈습니다",
|
||||
"toast.session.export.success.description": "세션 저장 위치: {{filename}}",
|
||||
"toast.session.export.failed.title": "세션 내보내기 실패",
|
||||
"toast.session.export.failed.description": "세션을 내보내는 동안 오류가 발생했습니다",
|
||||
|
||||
"toast.session.listFailed.title": "{{project}}에 대한 세션을 로드하지 못했습니다",
|
||||
"toast.update.title": "업데이트 가능",
|
||||
"toast.update.description": "OpenCode의 새 버전({{version}})을 설치할 수 있습니다.",
|
||||
@@ -544,6 +556,7 @@ export const dict = {
|
||||
"common.moreOptions": "더 많은 옵션",
|
||||
"common.learnMore": "더 알아보기",
|
||||
"common.rename": "이름 바꾸기",
|
||||
"common.export": "내보내기",
|
||||
"common.reset": "초기화",
|
||||
"common.archive": "보관",
|
||||
"common.delete": "삭제",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -178,6 +178,9 @@ export const dict = {
|
||||
"command.session.share.description": "Deel deze sessie en kopieer de URL naar het klembord",
|
||||
"command.session.unshare": "Sessie niet meer delen",
|
||||
"command.session.unshare.description": "Stop met het delen van deze sessie",
|
||||
"command.session.export": "Sessie exporteren",
|
||||
"command.session.export.description": "Het volledige sessietranscript exporteren als JSON",
|
||||
|
||||
"palette.search.placeholder": "Zoek naar bestanden, opdrachten en sessies",
|
||||
"palette.search.placeholder.home": "Opdrachten en sessies zoeken",
|
||||
"palette.empty": "Geen resultaten gevonden",
|
||||
@@ -539,6 +542,8 @@ export const dict = {
|
||||
"dialog.releaseNotes.action.next": "Volgende",
|
||||
"dialog.releaseNotes.action.hideFuture": "Laat deze in de toekomst niet zien",
|
||||
"dialog.releaseNotes.media.alt": "Releasevoorbeeld",
|
||||
"dialog.usageExceeded.dontShowAgain": "Niet meer weergeven",
|
||||
|
||||
"context.breakdown.title": "Contextanalyse",
|
||||
"context.breakdown.note":
|
||||
'Geschatte uitsplitsing van invoertokens. "Overig" omvat gereedschapsdefinities en overhead.',
|
||||
@@ -549,6 +554,8 @@ export const dict = {
|
||||
"context.breakdown.other": "Overig",
|
||||
"context.systemPrompt.title": "Systeemprompt",
|
||||
"context.rawMessages.title": "Ruwe berichten",
|
||||
"context.export.session": "Sessie exporteren",
|
||||
|
||||
"context.stats.session": "Sessie",
|
||||
"context.stats.messages": "Berichten",
|
||||
"context.stats.provider": "Aanbieder",
|
||||
@@ -616,6 +623,11 @@ export const dict = {
|
||||
"toast.session.unshare.failed.title": "Kan het delen van de sessie niet ongedaan maken",
|
||||
"toast.session.unshare.failed.description":
|
||||
"Er is een fout opgetreden bij het ongedaan maken van het delen van de sessie",
|
||||
"toast.session.export.success.title": "Sessie geëxporteerd",
|
||||
"toast.session.export.success.description": "Sessie opgeslagen in {{filename}}",
|
||||
"toast.session.export.failed.title": "Kan sessie niet exporteren",
|
||||
"toast.session.export.failed.description": "Er is een fout opgetreden tijdens het exporteren van de sessie",
|
||||
|
||||
"toast.session.listFailed.title": "Kan sessies voor {{project}} niet laden",
|
||||
"toast.project.reloadFailed.title": "Kan {{project}} niet opnieuw laden",
|
||||
"toast.update.title": "Update beschikbaar",
|
||||
@@ -832,6 +844,7 @@ export const dict = {
|
||||
"common.moreOptions": "Meer opties",
|
||||
"common.learnMore": "Meer informatie",
|
||||
"common.rename": "Hernoemen",
|
||||
"common.export": "Exporteren",
|
||||
"common.reset": "Opnieuw instellen",
|
||||
"common.archive": "Archiveren",
|
||||
"common.delete": "Verwijderen",
|
||||
|
||||
@@ -191,6 +191,9 @@ export const dict = {
|
||||
"command.session.unshare": "Slutt å dele sesjon",
|
||||
"command.session.unshare.description": "Slutt å dele denne sesjonen",
|
||||
|
||||
"command.session.export": "Eksporter sesjon",
|
||||
"command.session.export.description": "Eksporter hele sesjonsutskriften som JSON",
|
||||
|
||||
"palette.search.placeholder": "Søk i filer, kommandoer og sesjoner",
|
||||
"palette.search.placeholder.home": "Søk i kommandoer og sesjoner",
|
||||
"palette.empty": "Ingen resultater funnet",
|
||||
@@ -470,6 +473,8 @@ export const dict = {
|
||||
"dialog.project.edit.worktree.startup.description": "Kjører etter at et nytt arbeidsområde (worktree) er opprettet.",
|
||||
"dialog.project.edit.worktree.startup.placeholder": "f.eks. bun install",
|
||||
|
||||
"dialog.usageExceeded.dontShowAgain": "Ikke vis igjen",
|
||||
|
||||
"context.breakdown.title": "Kontekstfordeling",
|
||||
"context.breakdown.note":
|
||||
'Omtrentlig fordeling av inndatatokener. "Annet" inkluderer verktøydefinisjoner og overhead.',
|
||||
@@ -482,6 +487,8 @@ export const dict = {
|
||||
"context.systemPrompt.title": "Systemprompt",
|
||||
"context.rawMessages.title": "Rå meldinger",
|
||||
|
||||
"context.export.session": "Eksporter sesjon",
|
||||
|
||||
"context.stats.session": "Sesjon",
|
||||
"context.stats.messages": "Meldinger",
|
||||
"context.stats.provider": "Leverandør",
|
||||
@@ -559,6 +566,11 @@ export const dict = {
|
||||
"toast.session.unshare.failed.title": "Kunne ikke stoppe deling av sesjon",
|
||||
"toast.session.unshare.failed.description": "Det oppstod en feil da delingen av sesjonen skulle stoppes",
|
||||
|
||||
"toast.session.export.success.title": "Sesjon eksportert",
|
||||
"toast.session.export.success.description": "Sesjonen ble lagret i {{filename}}",
|
||||
"toast.session.export.failed.title": "Kunne ikke eksportere sesjon",
|
||||
"toast.session.export.failed.description": "Det oppstod en feil under eksport av sesjonen",
|
||||
|
||||
"toast.session.listFailed.title": "Kunne ikke laste sesjoner for {{project}}",
|
||||
|
||||
"toast.update.title": "Oppdatering tilgjengelig",
|
||||
@@ -721,6 +733,7 @@ export const dict = {
|
||||
"common.moreOptions": "Flere alternativer",
|
||||
"common.learnMore": "Lær mer",
|
||||
"common.rename": "Gi nytt navn",
|
||||
"common.export": "Eksporter",
|
||||
"common.reset": "Tilbakestill",
|
||||
"common.archive": "Arkiver",
|
||||
"common.delete": "Slett",
|
||||
|
||||
@@ -184,6 +184,9 @@ export const dict = {
|
||||
"command.session.share.description": "اس سیشن نو شیئر کرو تے URL نو کلپ بورڈ تے کاپی کرو",
|
||||
"command.session.unshare": "سیشن شیئر کرنا بند کرو",
|
||||
"command.session.unshare.description": "اس سیشن نو شیئر کرنا بند کرو",
|
||||
"command.session.export": "سیشن برآمد کرو",
|
||||
"command.session.export.description": "سیشن دی پوری نقل JSON دی شکل وچ برآمد کرو",
|
||||
|
||||
"palette.search.placeholder": "فائلاں، کمانڈز، تے سیشنز تلاش کرو",
|
||||
"palette.search.placeholder.home": "سرچ کمانڈز تے سیشنز",
|
||||
"palette.empty": "کوئی نتیجہ نئیں ملیا",
|
||||
@@ -544,6 +547,8 @@ export const dict = {
|
||||
"dialog.releaseNotes.action.next": "اگلا",
|
||||
"dialog.releaseNotes.action.hideFuture": "ایہہ اگوں نہ وکھاؤ",
|
||||
"dialog.releaseNotes.media.alt": "ریلیز پیش نظارہ",
|
||||
"dialog.usageExceeded.dontShowAgain": "دوبارہ نہ وکھاؤ",
|
||||
|
||||
"context.breakdown.title": "کانٹیکسٹ دی ونڈ",
|
||||
"context.breakdown.note": 'ان پٹ ٹوکناں دی لگ بھگ ونڈ۔ "ہور" وچ ٹول دیاں تعریفاں تے وادھو خرچ شامل نیں۔',
|
||||
"context.breakdown.system": "نظام",
|
||||
@@ -553,6 +558,8 @@ export const dict = {
|
||||
"context.breakdown.other": "ہور",
|
||||
"context.systemPrompt.title": "سسٹم پرامپٹ",
|
||||
"context.rawMessages.title": "خام سنیہے",
|
||||
"context.export.session": "سیشن برآمد کرو",
|
||||
|
||||
"context.stats.session": "سیشن",
|
||||
"context.stats.messages": "سنیہے",
|
||||
"context.stats.provider": "پرووائیڈر",
|
||||
@@ -619,6 +626,11 @@ export const dict = {
|
||||
"toast.session.unshare.success.description": "سیشن دا شیئر کامیابی نال بند ہو گیا!",
|
||||
"toast.session.unshare.failed.title": "سیشن ان شیئر کرن چ ناکام رہیا",
|
||||
"toast.session.unshare.failed.description": "سیشن ان شیئر کردے ویلے کوئی غلطی ہو گئی",
|
||||
"toast.session.export.success.title": "سیشن برآمد ہو گیا",
|
||||
"toast.session.export.success.description": "سیشن نوں \u2068{{filename}}\u2069 وچ محفوظ کر دتا گیا",
|
||||
"toast.session.export.failed.title": "سیشن برآمد کرن چ ناکامی ہوئی",
|
||||
"toast.session.export.failed.description": "سیشن برآمد کردے ویلے اک غلطی ہو گئی",
|
||||
|
||||
"toast.session.listFailed.title": "{{project}} لئی سیشن لوڈ کرن چ ناکام رہیا",
|
||||
"toast.project.reloadFailed.title": "{{project}} دوبارہ لوڈ نئیں ہو سکیا",
|
||||
"toast.update.title": "اپ ڈیٹ دستیاب اے",
|
||||
@@ -831,6 +843,7 @@ export const dict = {
|
||||
"common.moreOptions": "ہور اختیارات",
|
||||
"common.learnMore": "ہور جانو",
|
||||
"common.rename": "ناں بدلو",
|
||||
"common.export": "برآمد کرو",
|
||||
"common.reset": "ری سیٹ کرو",
|
||||
"common.archive": "آرکائیو کرو",
|
||||
"common.delete": "مکاؤ",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { desktopNativePluralCategories } from "./desktop-native"
|
||||
|
||||
const appLocales = [
|
||||
"ar",
|
||||
@@ -28,19 +29,51 @@ const appLocales = [
|
||||
"az",
|
||||
"fi",
|
||||
"sv",
|
||||
"am",
|
||||
"bg",
|
||||
"bn",
|
||||
"ca",
|
||||
"cs",
|
||||
"dv",
|
||||
"dz",
|
||||
"el",
|
||||
"et",
|
||||
"fa",
|
||||
"fo",
|
||||
"hr",
|
||||
"hu",
|
||||
"hy",
|
||||
"is",
|
||||
"ka",
|
||||
"km",
|
||||
"lo",
|
||||
"lt",
|
||||
"lv",
|
||||
"mk",
|
||||
"mn",
|
||||
"ms",
|
||||
"my",
|
||||
"ne",
|
||||
"ro",
|
||||
"si",
|
||||
"sk",
|
||||
"sl",
|
||||
"sq",
|
||||
"sr",
|
||||
"tg",
|
||||
"tk",
|
||||
"uz",
|
||||
] as const
|
||||
const desktopLocales = appLocales
|
||||
const pluralCategories: Partial<Record<(typeof appLocales)[number], readonly string[]>> = {
|
||||
ar: ["zero", "two", "few", "many"],
|
||||
br: ["many"],
|
||||
bs: ["few"],
|
||||
es: ["many"],
|
||||
fr: ["many"],
|
||||
it: ["many"],
|
||||
pl: ["few", "many"],
|
||||
ru: ["few", "many"],
|
||||
uk: ["few", "many"],
|
||||
}
|
||||
const pluralCategories = new Map(
|
||||
appLocales.map(
|
||||
(locale) =>
|
||||
[
|
||||
locale,
|
||||
desktopNativePluralCategories(locale).filter((category) => category !== "one" && category !== "other"),
|
||||
] as const,
|
||||
),
|
||||
)
|
||||
|
||||
const domains = [
|
||||
{
|
||||
@@ -63,7 +96,7 @@ const domains = [
|
||||
},
|
||||
] as const
|
||||
|
||||
describe.skipIf(!!process.env.CI)("i18n parity", () => {
|
||||
describe("i18n parity", () => {
|
||||
test("non-English locales have every English key and required plural variants", async () => {
|
||||
for (const domain of domains) {
|
||||
const source = await dictionary(domain.source)
|
||||
@@ -74,7 +107,7 @@ describe.skipIf(!!process.env.CI)("i18n parity", () => {
|
||||
.filter((key) => !Object.hasOwn(source, key))
|
||||
.sort()
|
||||
const expected = pluralFamilies(source)
|
||||
.flatMap((key) => (pluralCategories[locale] ?? []).map((category) => `${key}.${category}`))
|
||||
.flatMap((key) => (pluralCategories.get(locale) ?? []).map((category) => `${key}.${category}`))
|
||||
.sort()
|
||||
expect({ domain: domain.name, locale, missing, extra }).toEqual({
|
||||
domain: domain.name,
|
||||
@@ -95,7 +128,7 @@ describe.skipIf(!!process.env.CI)("i18n parity", () => {
|
||||
(key) => Object.hasOwn(target, key) && placeholders(source[key]).join() !== placeholders(target[key]).join(),
|
||||
)
|
||||
const pluralMismatched = pluralFamilies(source).flatMap((key) =>
|
||||
(pluralCategories[locale] ?? [])
|
||||
(pluralCategories.get(locale) ?? [])
|
||||
.map((category) => `${key}.${category}`)
|
||||
.filter((variant) => placeholders(source[`${key}.other`]).join() !== placeholders(target[variant]).join()),
|
||||
)
|
||||
@@ -144,12 +177,12 @@ describe("i18n plural parity", () => {
|
||||
for (const locale of domain.locales) {
|
||||
const target = await dictionary(domain.target(locale))
|
||||
const missing = families.flatMap((key) =>
|
||||
(pluralCategories[locale] ?? [])
|
||||
(pluralCategories.get(locale) ?? [])
|
||||
.map((category) => `${key}.${category}`)
|
||||
.filter((variant) => !Object.hasOwn(target, variant)),
|
||||
)
|
||||
const mismatched = families.flatMap((key) =>
|
||||
(pluralCategories[locale] ?? [])
|
||||
(pluralCategories.get(locale) ?? [])
|
||||
.map((category) => `${key}.${category}`)
|
||||
.filter(
|
||||
(variant) =>
|
||||
|
||||
@@ -185,6 +185,9 @@ export const dict = {
|
||||
"command.session.share.description": "Udostępnij tę sesję i skopiuj adres URL do schowka",
|
||||
"command.session.unshare": "Przestań udostępniać sesję",
|
||||
"command.session.unshare.description": "Zatrzymaj udostępnianie tej sesji",
|
||||
"command.session.export": "Eksportuj sesję",
|
||||
"command.session.export.description": "Eksportuj pełny zapis sesji w formacie JSON",
|
||||
|
||||
"palette.search.placeholder": "Szukaj plików, poleceń i sesji",
|
||||
"palette.search.placeholder.home": "Szukaj poleceń i sesji",
|
||||
"palette.empty": "Brak wyników",
|
||||
@@ -535,6 +538,8 @@ export const dict = {
|
||||
"dialog.project.edit.worktree.startup.description":
|
||||
"Uruchamiany po utworzeniu nowej przestrzeni roboczej (worktree).",
|
||||
"dialog.project.edit.worktree.startup.placeholder": "np. bun install",
|
||||
"dialog.usageExceeded.dontShowAgain": "Nie pokazuj ponownie",
|
||||
|
||||
"context.breakdown.title": "Podział kontekstu",
|
||||
"context.breakdown.note": 'Przybliżony podział tokenów wejściowych. "Inne" obejmuje definicje narzędzi i narzut.',
|
||||
"context.breakdown.system": "System",
|
||||
@@ -544,6 +549,8 @@ export const dict = {
|
||||
"context.breakdown.other": "Inne",
|
||||
"context.systemPrompt.title": "Prompt systemowy",
|
||||
"context.rawMessages.title": "Surowe wiadomości",
|
||||
"context.export.session": "Eksportuj sesję",
|
||||
|
||||
"context.stats.session": "Sesja",
|
||||
"context.stats.messages": "Wiadomości",
|
||||
"context.stats.provider": "Dostawca",
|
||||
@@ -610,6 +617,11 @@ export const dict = {
|
||||
"toast.session.unshare.success.description": "Udostępnianie sesji zostało pomyślnie zatrzymane!",
|
||||
"toast.session.unshare.failed.title": "Nie udało się zatrzymać udostępniania sesji",
|
||||
"toast.session.unshare.failed.description": "Wystąpił błąd podczas zatrzymywania udostępniania sesji",
|
||||
"toast.session.export.success.title": "Sesja wyeksportowana",
|
||||
"toast.session.export.success.description": "Sesję zapisano jako {{filename}}",
|
||||
"toast.session.export.failed.title": "Nie udało się wyeksportować sesji",
|
||||
"toast.session.export.failed.description": "Wystąpił błąd podczas eksportowania sesji",
|
||||
|
||||
"toast.session.listFailed.title": "Nie udało się załadować sesji dla {{project}}",
|
||||
"toast.update.title": "Dostępna aktualizacja",
|
||||
"toast.update.description": "Nowa wersja OpenCode ({{version}}) jest teraz dostępna do instalacji.",
|
||||
@@ -797,6 +809,7 @@ export const dict = {
|
||||
"common.moreOptions": "Więcej opcji",
|
||||
"common.learnMore": "Dowiedz się więcej",
|
||||
"common.rename": "Zmień nazwę",
|
||||
"common.export": "Eksportuj",
|
||||
"common.reset": "Resetuj",
|
||||
"common.archive": "Archiwizuj",
|
||||
"common.delete": "Usuń",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -192,6 +192,9 @@ export const dict = {
|
||||
"command.session.unshare": "Отменить публикацию",
|
||||
"command.session.unshare.description": "Прекратить публикацию сессии",
|
||||
|
||||
"command.session.export": "Экспортировать сессию",
|
||||
"command.session.export.description": "Экспортировать полную историю сессии в формате JSON",
|
||||
|
||||
"palette.search.placeholder": "Поиск файлов, команд и сессий",
|
||||
"palette.search.placeholder.home": "Поиск команд и сессий",
|
||||
"palette.empty": "Ничего не найдено",
|
||||
@@ -564,6 +567,8 @@ export const dict = {
|
||||
"dialog.project.edit.worktree.startup.description":
|
||||
"Запускается после создания нового рабочего пространства (worktree).",
|
||||
"dialog.project.edit.worktree.startup.placeholder": "например, bun install",
|
||||
"dialog.usageExceeded.dontShowAgain": "Больше не показывать",
|
||||
|
||||
"context.breakdown.title": "Разбивка контекста",
|
||||
"context.breakdown.note":
|
||||
'Приблизительная разбивка входных токенов. "Другое" включает определения инструментов и накладные расходы.',
|
||||
@@ -576,6 +581,8 @@ export const dict = {
|
||||
"context.systemPrompt.title": "Системный промпт",
|
||||
"context.rawMessages.title": "Исходные сообщения",
|
||||
|
||||
"context.export.session": "Экспортировать сессию",
|
||||
|
||||
"context.stats.session": "Сессия",
|
||||
"context.stats.messages": "Сообщения",
|
||||
"context.stats.provider": "Провайдер",
|
||||
@@ -653,6 +660,11 @@ export const dict = {
|
||||
"toast.session.unshare.failed.title": "Не удалось отменить публикацию",
|
||||
"toast.session.unshare.failed.description": "Произошла ошибка при отмене публикации",
|
||||
|
||||
"toast.session.export.success.title": "Сессия экспортирована",
|
||||
"toast.session.export.success.description": "Сессия сохранена в файл {{filename}}",
|
||||
"toast.session.export.failed.title": "Не удалось экспортировать сессию",
|
||||
"toast.session.export.failed.description": "Произошла ошибка при экспорте сессии",
|
||||
|
||||
"toast.session.listFailed.title": "Не удалось загрузить сессии для {{project}}",
|
||||
|
||||
"toast.update.title": "Доступно обновление",
|
||||
@@ -858,6 +870,7 @@ export const dict = {
|
||||
"common.moreOptions": "Дополнительные опции",
|
||||
"common.learnMore": "Подробнее",
|
||||
"common.rename": "Переименовать",
|
||||
"common.export": "Экспортировать",
|
||||
"common.reset": "Сбросить",
|
||||
"common.archive": "Архивировать",
|
||||
"common.delete": "Удалить",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user